Commit b6734d5f authored by Fachri's avatar Fachri

Retur PK

parent eaae2cf4
This diff is collapsed.
import { CONFIG } from 'src/global-config';
import { ReturFakturPkListView } from 'src/sections/faktur/returPk/view';
const metadata = { title: `Retur Faktur PM - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<ReturFakturPkListView />
</>
);
}
......@@ -50,6 +50,8 @@ const OverviewFakturPmPage = lazy(() => import('src/pages/faktur/fakturPm'));
const OverviewReturFakturPmPage = lazy(() => import('src/pages/faktur/returPm'));
const OverviewReturRekamFakturPmPage = lazy(() => import('src/pages/faktur/returRekamPm'));
// eslint-disable-next-line import/no-unresolved
const OverviewReturFakturPkPage = lazy(() => import('src/pages/faktur/returPk'));
// Overview
const IndexPage = lazy(() => import('src/pages/dashboard'));
......@@ -183,6 +185,7 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'pm', element: <OverviewFakturPmPage /> },
{ path: 'retur/pm', element: <OverviewReturFakturPmPage /> },
{ path: 'retur-pm/:id/:type', element: <OverviewReturRekamFakturPmPage /> },
{ path: 'retur/pk', element: <OverviewReturFakturPkPage /> },
],
},
];
// import { zodResolver } from '@hookform/resolvers/zod';
// import Grid from '@mui/material/Grid';
// import { useQueryClient } from '@tanstack/react-query';
// import type { GridRowSelectionModel } from 'node_modules/@mui/x-data-grid/esm/models/gridRowSelectionModel';
// 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 type { TUploadFakturPMRequest } from '../../types/types';
// import usePrepopulatedPM from '../../hooks/usePrepopulatedPM';
// interface ModalPrepopulatedFakturProps {
// dataSelected?: GridRowSelectionModel;
// setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
// tableApiRef?: React.MutableRefObject<any>;
// isOpenDialogUpload: boolean;
// setIsOpenDialogUpload: (v: boolean) => void;
// successMessage?: string;
// onConfirmUpload?: () => Promise<void> | void;
// }
// const ModalPrepolulatedPM: React.FC<ModalPrepopulatedFakturProps> = ({
// dataSelected,
// setSelectionModel,
// tableApiRef,
// isOpenDialogUpload,
// setIsOpenDialogUpload,
// successMessage = 'Data berhasil diupload',
// onConfirmUpload,
// }) => {
// const queryClient = useQueryClient();
// const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// const {
// numberOfData,
// setNumberOfData,
// numberOfDataFail,
// numberOfDataProcessed,
// numberOfDataSuccess,
// processSuccess,
// processFail,
// resetToDefault,
// status,
// } = useDialogProgressBar();
// const schema = z.object({
// masaTahunPajak: z.string().min(1, 'Masa tahun pajak wajib diisi'), // karena DatePicker output string
// });
// const methods = useForm({
// mode: 'all',
// resolver: zodResolver(schema),
// });
// const { handleSubmit } = methods;
// const { mutateAsync, isPending } = usePrepopulatedPM({
// onSuccess: () => {
// enqueueSnackbar('Berhasil unduh faktur', { variant: 'success' });
// },
// onError: (err) => {
// enqueueSnackbar(err.message || 'Gagal unduh faktur', { variant: 'error' });
// },
// });
// const onSubmit = async (values: { masaTahunPajak: string }) => {
// const [masaPajak, tahunPajak] = values.masaTahunPajak.split('/');
// await mutateAsync({
// fgPermintaan: 1,
// requestFakturMasukan: {
// masaPajak,
// tahunPajak,
// },
// });
// };
// const handleCloseModal = () => {
// setIsOpenDialogUpload(false);
// resetToDefault();
// };
// const onSubmit = async (values: { masaTahunPajak: string }) => {
// try {
// setIsOpenDialogProgressBar(true);
// // contoh payload (sesuaikan dengan API kamu)
// const payload: TUploadFakturPMRequest = {
// fgPermintaan: 2,
// masaTahunPajak: values.masaTahunPajak,
// // tambahkan field lain jika perlu
// };
// await mutateAsync(payload);
// } finally {
// queryClient.invalidateQueries({ queryKey: ['upload-prepop-faktur-pm'] });
// }
// };
// return (
// <>
// <FormProvider {...methods}>
// <DialogUmum
// isOpen={isOpenDialogUpload}
// onClose={handleCloseModal}
// title="Apakah Anda yakin ingin mengubah data menjadi Credit?"
// maxWidth="sm"
// >
// <Grid size={{ md: 12 }}>
// <Field.DatePicker
// name="masaTahunPajak"
// label="Masa Pajak"
// slotProps={{ textField: { helperText: '' } }}
// views={['year', 'month']} // urutan views tampil sesuai array
// openTo="month" // buka langsung ke bulan
// format="MM/YYYY" // untuk menampilkan di input
// />
// </Grid>
// <Stack direction="row" justifyContent="end" spacing="16px">
// <LoadingButton
// onClick={handleCloseModal}
// variant="text"
// sx={{
// fontSize: '14px',
// }}
// size="medium"
// >
// Tutup
// </LoadingButton>
// <LoadingButton
// // onClick={handleSubmit(handleSubmit)}
// type="button"
// variant="text"
// size="medium"
// sx={{
// fontSize: '14px',
// }}
// // loading={isLoading}
// >
// 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 ModalPrepolulatedPM;
import { zodResolver } from '@hookform/resolvers/zod';
import Grid from '@mui/material/Grid';
import { useQueryClient } from '@tanstack/react-query';
......
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" />;
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 === null) {
return (
<Chip
label="Belum diKreditkan"
size="small"
variant="outlined"
sx={{
borderColor: '#2e7d32',
color: '#2e7d32',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
}
return null;
})();
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}
{componentCredit}
</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 ModalCancelReturPK: 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),
});
})
);
};
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 ModalCancelReturPK;
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 ModalCetakReturPK: React.FC<ModalCetakReturPMProps> = ({ payload, isOpen, onClose }) => {
const [rows, setRows] = useState<Row[]>([]);
useEffect(() => {
if (!payload) return;
const formattedRows: Row[] = [
{ label: 'NPWP Pembeli', value: payload.npwppembeli },
{ label: 'Nama Pembeli', value: payload.namapembeli || '-' },
{ label: 'Nomor Retur', value: payload.nomorretur || '-' },
{
label: 'Nomor Faktur Pajak Direktur',
value: payload.nomorfakturdiretur,
},
{
label: 'Tanggal Retur',
value: payload.tanggalretur ? dayjs.utc(payload.tanggalretur).format('DD/MM/YYYY') : '-',
},
{
label: 'Masa Retur',
value: payload.masaretur,
},
{
label: 'Tahun Retur',
value: payload.tahunretur,
//
},
{
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: dayjs.utc(payload.tanggalapproval).format('DD/MM/YYYY') || '-',
},
{
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: dayjs.utc(payload.created_at).format('DD/MM/YYYY') || '-',
},
{
label: 'User Pengubah',
value: payload.updated_by || '-',
},
{
label: 'Tanggal Ubah',
value: dayjs.utc(payload.updated_at).format('DD/MM/YYYY') || '-',
},
];
setRows(formattedRows);
}, [payload]);
return (
<DialogUmum
maxWidth="lg"
isOpen={isOpen}
onClose={onClose}
title="Detail Retur Faktur Pajak Keluaran"
>
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
<ListDetailBuilder rows={rows} />
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakReturPK;
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 usePrepopulatedReturPK from '../../hooks/usePrepopulatedReturPK';
interface ModalPrepopulatedFakturProps {
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 ModalPrepolulatedReturPK: React.FC<ModalPrepopulatedFakturProps> = ({
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 } = usePrepopulatedReturPK({
onSuccess: () => {
processSuccess();
enqueueSnackbar(successMessage, { variant: 'success' });
},
onError: (err) => {
processFail();
enqueueSnackbar(err.message || 'Gagal unduh faktur', {
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 as any);
await onConfirmUpload?.();
handleCloseModal();
setIsOpenDialogProgressBar(false);
} 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 ModalPrepolulatedReturPK;
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;
This source diff could not be displayed because it is too large. You can view the blob instead.
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 = {
fakturReturPK: {
all: (params: any) => [appRootKey, 'ReturfakturPk', params],
detail: (params: any) => [appRootKey, 'ReturfakturPk', 'detail', params],
},
};
export default queryKey;
import { useMutation } from '@tanstack/react-query';
import type { TCancelResponse, TCancelReturPKRequest } from '../types/types';
import fakturApi from '../utils/api';
const useCancelReturPK = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelReturPKRequest>({
mutationKey: ['cancel-retur-pk'],
mutationFn: (payload) => fakturApi.cancelReturPK(payload),
...props,
});
export default useCancelReturPK;
import { isEmpty } from 'lodash';
import { useQuery } from '@tanstack/react-query';
import queryKey from '../constant/queryKey';
import type { TableReturFakturPKResult } from '../types/types';
import fakturApi from '../utils/api';
export type TGetFakturPMApiWrapped = {
data: TableReturFakturPKResult[];
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 useGetReturFakturPK = ({ 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<TGetFakturPMApiWrapped>({
queryKey: isListRequest
? ['faktur-pk', page, limit, advanced, sortingMode, sortingMethod, restKey]
: ['faktur-pk', restKey], // supaya cache beda antara list dan search tunggal
queryFn: async () => {
const res: any = await fakturApi.getReturPK({ 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 TableReturFakturPKResult[],
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 useGetReturFakturPKById = (id: string, options = {}) =>
useQuery({
queryKey: queryKey.fakturReturPK.detail(id),
queryFn: async () => {
const res = await fakturApi.getReturPKById(id);
if (!res) throw new Error('Data tidak ditemukan');
return res;
},
enabled: !!id,
refetchOnWindowFocus: false,
...options,
});
export default useGetReturFakturPK;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import type { TBaseResponseAPI, TPrepopulatedReturPKRequest } from '../types/types';
import fakturApi from '../utils/api';
// Hook menerima UseMutationOptions supaya bisa inject onSuccess / onError dari pemanggil
const usePrepopulatedReturPK = (
options?: UseMutationOptions<TBaseResponseAPI<any>, Error, TPrepopulatedReturPKRequest>
) =>
useMutation<TBaseResponseAPI<any>, Error, TPrepopulatedReturPKRequest>({
mutationKey: ['prepopulated-faktur-retur-pk'],
mutationFn: (payload) => fakturApi.prepopulatedReturPK(payload),
...options,
});
export default usePrepopulatedReturPK;
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: [] },
},
})),
}));
export type TBaseResponseAPI<T> = {
status: string;
message: string;
data: T;
time: string;
code: number;
metaPage: TBaseResponseMetaPage;
total?: number;
};
type TBaseResponseMetaPage = {
pageNum: number | null;
rowPerPage: number | null;
totalRow: number;
};
export type TableReturFakturPK = {
id: number;
masapajak: string;
tahunpajak: string;
masaretur: string;
tahunretur: string;
tanggalfaktur: string;
tanggalretur: string;
tanggalapproval: string | null;
nomorfakturdiretur: string;
nomorretur: string;
npwppembeli: string;
npwppenjual: string;
namapembeli: string;
namapenjual: string;
statuspembeli: string;
statusretur: string;
nilaireturdpp: string;
nilaireturppn: string;
nilaireturppnbm: string;
nilaireturdpplain: string;
approvalsign: string | null;
errormsg: string | null;
created_by: string;
updated_by: string;
created_at: string;
updated_at: string;
internal_id: string;
userid: string;
// optional / nullable fields from response
idperekam: number | null;
agregat: string | null;
agregatpenjual: string | null;
agregatpembeli: string | null;
nomorformdokumen: string | null;
agregatnomorformdokumen: string | null;
};
export type TableReturFakturPKResult = TableReturFakturPK[];
export type TValidateFakturPMRequest = {
id: number;
nomorFaktur: string;
};
export interface TPostReturPMObjekFaktur {
brgJasa: 'GOODS' | 'SERVICES';
kdBrgJasa: string;
namaBrgJasa: string;
satuanBrgJasa: string;
hargaSatuan: number;
jmlBrgJasa: number;
returJmlBrgJasa: number;
totalHarga: number;
diskon: number;
returDiskon: number;
cekDppLain: number;
dpp: number;
returDpp: number;
dppLain: number;
returDppLain: number;
tarifPpn: number;
ppn: number;
returPpn: number;
tarifPpnbm: number;
ppnbm: number;
returPpnbm: number;
}
export interface TPostReturPMRequest {
masaPajakRetur: string; // contoh: "10"
nilaiReturDpp: number;
nilaiReturDppLain: number;
nilaiReturDiskon: number;
nilaiReturPpn: number;
nilaireturppnbm: number;
tahunPajakRetur: number; // contoh: 2025
tanggalRetur: string; // format: DDMMYYYY (contoh: "25102025")
objekFaktur: TPostReturPMObjekFaktur[];
isCreditable: 0 | 1;
nomorFakturDiretur: string;
npwpPembeli: string;
npwpPenjual: string;
}
export type TCancelReturPKRequest = {
id: string;
};
// types/prepopulated.ts
export interface TRequestFakturMasukan {
tahunPajak: string;
masaPajak: string;
}
export interface TPrepopulatedPMRequest {
fgPermintaan: number; // 1
requestFakturMasukan: TRequestFakturMasukan;
}
export type ActionItem = {
title: string;
icon: React.ReactNode;
func?: () => void;
disabled?: boolean;
};
export type TCancelRequest = {
id: string | number;
};
export type TCancelResponse = TBaseResponseAPI<{
id: string | number;
}>;
export interface TPrepopulatedReturPKRequest {
masaPajak: string;
tahunPajak: string;
}
import type {
TableReturFakturPKResult,
TBaseResponseAPI,
TCancelReturPKRequest,
TPrepopulatedReturPKRequest,
} from '../types/types';
import unifikasiClient from './unifikasiClient';
const fakturApi = () => {};
// API untuk get list table
fakturApi.getReturPK = async (config: any) => {
const {
data: { message, metaPage, data },
status: statusCode,
} = await unifikasiClient.get<TBaseResponseAPI<TableReturFakturPKResult>>('/IF_TXR_005', {
...config,
});
if (statusCode !== 200) {
throw new Error(message);
}
return { total: metaPage ? Number(metaPage.totalRow) : 0, data };
};
fakturApi.getReturPKById = async (id: string) => {
const res = await unifikasiClient.get('/IF_TXR_005', { params: { id } });
const {
data: { status, message, data },
status: statusCode,
} = res;
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('getRetur PK failed:', { statusCode, status, message });
throw new Error(message || 'Gagal mengambil data FakturReturPK');
}
const dnData = Array.isArray(data) ? data[0] : data;
return dnData;
};
fakturApi.prepopulatedReturPK = async (payload: TPrepopulatedReturPKRequest) => {
const {
data: { status, message, data },
status: statusCode,
} = await unifikasiClient.post('/IF_TXR_005/rpk', payload);
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('prepopulated Retur PK failed:', { statusCode, status, message });
throw new Error(message || 'Gagal melakukan prepopulated Faktur Retur PK');
}
return data;
};
fakturApi.cancelReturPK = async (payload: TCancelReturPKRequest) => {
const {
data: { status, message, data },
status: statusCode,
} = await unifikasiClient.post('/IF_TXR_005/batal', payload);
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('Cancel Retur PK failed:', { statusCode, status, message });
throw new Error(message || 'Gagal mengcancel Retur pajak Keluaran');
}
return data;
};
export default fakturApi;
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'),
};
};
// utils/searchParams.ts
import dayjs from 'dayjs';
type FormValues = {
filter: 'perioderetur' | 'periodemasa' | 'nomorFaktur' | 'nomorRetur' | 'npwp' | 'customDate';
periodeRetur?: string | null; // Dayjs ISO or Date-like
periode?: string | null;
nomorFaktur?: string;
nomorRetur?: string;
npwp?: string;
};
export function buildSearchParams(values: FormValues) {
const out: Record<string, any> = {};
// common pagination defaults (consumer will add page & limit)
// out.page = 1;
// out.limit = 10;
if (values.filter === 'perioderetur') {
// expects: perioderetur=<ISO date quoted> & periodeRetur=MM/YYYY
// assume values.periodeRetur contains a Dayjs/ISO string or Date
if (values.periodeRetur) {
const iso = dayjs(values.periodeRetur).toISOString();
out.perioderetur = `"${iso}"`; // note: example had quotes encoded -> keep quotes for parity
out.periodeRetur = dayjs(values.periodeRetur).format('MM/YYYY');
}
} else if (values.filter === 'periodemasa') {
if (values.periode) {
const iso = dayjs(values.periode).toISOString();
out.periodemasa = `"${iso}"`;
out.periode = dayjs(values.periode).format('MM/YYYY');
}
} else if (values.filter === 'nomorFaktur') {
if (values.nomorFaktur && values.nomorFaktur.trim() !== '') {
out.nomorFaktur = values.nomorFaktur.trim();
}
} else if (values.filter === 'nomorRetur') {
if (values.nomorRetur && values.nomorRetur.trim() !== '') {
out.nomorRetur = values.nomorRetur.trim();
}
} else if (values.filter === 'npwp') {
if (values.npwp && values.npwp.trim() !== '') {
out.npwp = values.npwp.trim();
}
}
return out;
}
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;
import dayjs from 'dayjs';
import { MIN_THN_PAJAK } from '../constant';
export const currentYear = dayjs().year();
export const getHighestStartingYear = (thnAwalUnifikasi: any) =>
Math.max(MIN_THN_PAJAK, thnAwalUnifikasi);
export const selectedInitialMonth = ({ thnAwalUnifikasi, masaAwalUnifikasi }: any) => {
const highestYear = getHighestStartingYear(thnAwalUnifikasi);
return highestYear > thnAwalUnifikasi ? '01' : masaAwalUnifikasi;
};
export const determineStartingMonth = ({ thnPajak, thnAwalUnifikasi, masaAwalUnifikasi }: any) => {
const highestYear = getHighestStartingYear(thnAwalUnifikasi);
const initialMonth = selectedInitialMonth({ thnAwalUnifikasi, masaAwalUnifikasi });
return thnPajak >= highestYear && thnPajak <= currentYear ? initialMonth : '';
};
// utils/date.ts
export const parseApiDateOnly = (iso?: string): Date | undefined => {
if (!iso) return undefined;
const [y, m, d] = iso.split('T')[0].split('-');
return new Date(+y, +m - 1, +d);
};
export * from './retur-pk-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