Commit f3ec3847 authored by Fachri's avatar Fachri

Faktur PM and Adjust Leftside Bar

parent fe2af0c4
// check-rsc-vuln.js
const fs = require('fs');
const path = require('path');
const vulnerableRanges = [
{ name: 'react-server-dom-webpack', versions: ['19.0.0', '19.1.0', '19.1.1', '19.2.0'] },
{ name: 'react-server-dom-parcel', versions: ['19.0.0', '19.1.0', '19.1.1', '19.2.0'] },
{ name: 'react-server-dom-turbopack', versions: ['19.0.0', '19.1.0', '19.1.1', '19.2.0'] },
];
function checkPackageJson(dir) {
const packagePath = path.join(dir, 'package.json');
if (!fs.existsSync(packagePath)) {
console.log('❌ package.json tidak ditemukan di folder ini.');
return;
}
const pkg = JSON.parse(fs.readFileSync(packagePath));
console.log('🔍 Mengecek dependency...');
let foundVulnerability = false;
vulnerableRanges.forEach(({ name, versions }) => {
const version =
(pkg.dependencies && pkg.dependencies[name]) ||
(pkg.devDependencies && pkg.devDependencies[name]);
if (version) {
const cleaned = version.replace(/^[^\d]*/, ''); // Remove ^ ~ etc
if (versions.includes(cleaned)) {
foundVulnerability = true;
console.log(`❌ RENTAN: ${name}@${cleaned} — mengandung celah CVE-2025-55182`);
} else {
console.log(`✔ Aman: ${name}@${cleaned}`);
}
}
});
if (!foundVulnerability) {
console.log('\n✅ Tidak ditemukan versi yang rentan. Proyek kamu AMAN.');
} else {
console.log(
'\n⚠ ACTION: Segera jalankan update:\n npm install react-server-dom-webpack@latest'
);
}
}
checkPackageJson(process.cwd());
const { execSync } = require('child_process');
console.log('🔍 Deep scanning React versions...');
try {
const output = execSync(`npm ls react --all --json`, { encoding: 'utf8' });
const tree = JSON.parse(output);
let foundVulnerable = false;
function scan(node) {
if (!node.dependencies) return;
for (const depName in node.dependencies) {
const dep = node.dependencies[depName];
if (depName === 'react') {
const version = dep.version;
if (
(version && version.startsWith('18.') && version < '18.3.1') ||
(version.startsWith('19.') && version < '19.0.0')
) {
console.log(`❌ VULNERABLE: react@${version} found at ${dep.path}`);
foundVulnerable = true;
}
}
scan(dep);
}
}
scan(tree);
if (!foundVulnerable) {
console.log('✅ Tidak ada React versi rentan di seluruh node_modules');
}
} catch (e) {
console.error('Gagal scanning:', e.message);
}
This diff is collapsed.
This diff is collapsed.
......@@ -11,7 +11,7 @@ export function dashboardLayoutVars(theme: Theme) {
return {
'--layout-transition-easing': 'linear',
'--layout-transition-duration': '120ms',
'--layout-nav-mini-width': '88px',
'--layout-nav-mini-width': '110px',
'--layout-nav-vertical-width': '300px',
'--layout-nav-horizontal-height': '64px',
'--layout-dashboard-content-pt': theme.spacing(1),
......
......@@ -130,5 +130,6 @@ const NavRoot = styled('div', {
duration: 'var(--layout-transition-duration)',
}),
[theme.breakpoints.up(layoutQuery)]: { display: 'flex' },
color: '#000080',
})
);
......@@ -69,53 +69,22 @@ export const navData: NavSectionProps['data'] = [
subheader: 'Overview',
items: [{ title: 'Beranda', path: paths.dashboard.root, icon: ICONS.dashboard }],
},
/**
* Management
*/
{
subheader: '',
items: [
{
title: 'e-Bupot Unifikasi',
path: paths.unifikasi.dn,
title: 'e-Faktur',
path: paths.faktur.pk,
icon: ICONS.blank,
children: [
{ title: 'Bupot Unifikasi', path: paths.unifikasi.dn },
{ title: 'Bupot Non Residen', path: paths.unifikasi.nr },
{ title: 'Bupot Disetor Sendiri', path: paths.unifikasi.ssp },
{ title: 'Bupot Digunggung', path: paths.unifikasi.digunggung },
{ title: 'Dokumen Dipersamakan', path: paths.unifikasi.dipersamakan },
],
},
],
},
{
subheader: '',
items: [
{
title: 'e-Bupot 21/26',
path: paths.pph21.bulanan,
icon: ICONS.blank,
children: [
{ title: 'Bupot Bulanan', path: paths.pph21.bulanan },
{ title: 'Bupot Final/Tidak Final', path: paths.pph21.bupotFinal },
{ title: 'Bupot Tahunan A1', path: paths.pph21.tahunan },
{ title: 'Bupot Pasal 26', path: paths.pph21.bupot26 },
],
},
],
title: 'Dashbooard e-Faktur',
path: paths.faktur.pk,
// children: [],
},
{
subheader: '',
items: [
{
title: 'e-Faktur',
path: paths.faktur.pk,
icon: ICONS.blank,
children: [
{
title: 'Faktur',
path: paths.faktur.pk,
children: [
{ title: 'Pajak Keluaran', path: paths.faktur.pk },
{ title: 'Pajak Masukan', path: paths.faktur.pm },
......@@ -127,8 +96,8 @@ export const navData: NavSectionProps['data'] = [
title: 'Dokumen Lain',
path: paths.faktur.dlk,
children: [
{ title: 'Dokumen Lain Keluaran', path: paths.faktur.dlk },
{ title: 'Dokumen Lain Masukan', path: paths.faktur.dlm },
{ title: 'Pajak Keluaran', path: paths.faktur.dlk },
{ title: 'Pajak Masukan', path: paths.faktur.dlm },
{ title: 'Retur Dokumen Lain Keluaran', path: paths.faktur.returDlk },
{ title: 'Retur Dokumen Lain Masukan', path: paths.faktur.returDlm },
],
......@@ -137,6 +106,60 @@ export const navData: NavSectionProps['data'] = [
},
],
},
{
subheader: '',
items: [
{
title: 'eBupot',
path: paths.faktur.pk,
icon: ICONS.blank,
children: [
{ title: 'BPPU', path: paths.unifikasi.dn },
{ title: 'BPNR', path: paths.unifikasi.nr },
{ title: 'Penyetoran Sendiri', path: paths.unifikasi.ssp },
{ title: 'Pemotongan Secara Digunggung', path: paths.unifikasi.digunggung },
{ title: 'Dokumen Dipersamakan', path: paths.unifikasi.dipersamakan },
{ title: 'Bukti Pemotongan Bulanan Pegawai Tetap', path: paths.pph21.bulanan },
{ title: 'BP 21 - Bukti Pemotongan Selain Pegawai Tetap', path: paths.pph21.bupotFinal },
{ title: 'BP 26 - Bukti Pemotongan Wajib Pajak Luar Negeri', path: paths.pph21.bupot26 },
{ title: 'BP A1 - Bukti Pemotongan A1 Masa Pajak Terakhir', path: paths.pph21.tahunan },
],
},
],
},
// {
// subheader: '',
// items: [
// {
// title: 'eBupot',
// path: paths.unifikasi.dn,
// icon: ICONS.blank,
// children: [
// { title: 'BPPU', path: paths.unifikasi.dn },
// { title: 'BPNR', path: paths.unifikasi.nr },
// { title: 'Penyetoran Sendiri', path: paths.unifikasi.ssp },
// { title: 'Pemotongan Secara Digunggung', path: paths.unifikasi.digunggung },
// { title: 'Dokumen Dipersamakan', path: paths.unifikasi.dipersamakan },
// ],
// },
// ],
// },
// {
// subheader: '',
// items: [
// {
// title: 'e-Bupot 21/26',
// path: paths.pph21.bulanan,
// icon: ICONS.blank,
// children: [
// { title: 'Bupot Bulanan', path: paths.pph21.bulanan },
// { title: 'Bupot Final/Tidak Final', path: paths.pph21.bupotFinal },
// { title: 'Bupot Tahunan A1', path: paths.pph21.tahunan },
// { title: 'Bupot Pasal 26', path: paths.pph21.bupot26 },
// ],
// },
// ],
// },
];
/**
......
import { CONFIG } from 'src/global-config';
import { FakturPmListView } from 'src/sections/faktur/fakturPm/view';
const metadata = { title: `Faktur - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FakturPmListView />
</>
);
}
......@@ -44,6 +44,8 @@ const OverviewUnifikasiRekamDokumenDipersamakanPage = lazy(
const OverviewFakturPkPage = lazy(() => import('src/pages/faktur/fakturPk'));
const OverviewFakturPkRekamPage = lazy(() => import('src/pages/faktur/fakturRekamPk'));
const OverviewFakturPmPage = lazy(() => import('src/pages/faktur/fakturPm'));
// Overview
const IndexPage = lazy(() => import('src/pages/dashboard'));
......@@ -158,8 +160,8 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'bupot-final/rekam', element: <OverviewBupotFinalTdkFinalRekamPage /> },
{ path: 'bupot-final/:id/:type', element: <OverviewBupotFinalTdkFinalRekamPage /> },
{ path: 'tahunan', element: <OverviewBupotA1Page /> },
{ path:'tahunan/rekam', element:<OverviewBupotA1RekamPage/>},
{ path:'tahunan/:id/:type', element:<OverviewBupotA1RekamPage/>},
{ path: 'tahunan/rekam', element: <OverviewBupotA1RekamPage /> },
{ path: 'tahunan/:id/:type', element: <OverviewBupotA1RekamPage /> },
{ path: 'bupot-26', element: <OverviewBupotPasal26Page /> },
{ path: 'bupot-26/rekam', element: <OverviewBupotPasal26RekamPage /> },
{ path: 'bupot-26/:id/:type', element: <OverviewBupotPasal26RekamPage /> },
......@@ -173,6 +175,7 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'pk', element: <OverviewFakturPkPage /> },
{ path: 'pk/new', element: <OverviewFakturPkRekamPage /> },
{ path: 'pk/:id/:type', element: <OverviewFakturPkRekamPage /> },
{ path: 'pm', element: <OverviewFakturPmPage /> },
],
},
];
......@@ -403,18 +403,6 @@ const DokumenTransaksi: React.FC<DokumenTransaksiProps> = ({ fakturData, isLoadi
)}
<Grid size={{ md: 6 }}>
{/* <Field.Select
name="fgPengganti"
helperText=""
label="Jenis Faktur"
disabled={Boolean(fakturData)}
>
{jenisFakturOptions.map((opt) => (
<MenuItem key={opt.id} value={opt.id}>
{opt.label}
</MenuItem>
))}
</Field.Select> */}
<Field.Select
name="fgPengganti"
helperText=""
......@@ -471,35 +459,6 @@ const DokumenTransaksi: React.FC<DokumenTransaksiProps> = ({ fakturData, isLoadi
</Grid>
)}
{/* <Grid size={{ md: 6 }}>
<RHFNumeric
name="nomorFaktur"
label="Nomor Faktur"
allowDecimalValue={false}
allowNegativeValue={false}
disableFormat
onChange={(e: any) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 17);
setValue('nomorFaktur', value, { shouldDirty: true });
}}
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
InputProps={{
endAdornment: (
<>
{isLoading && <CircularProgress size={20} sx={{ mr: 1 }} />}
{!isLoading && fakturData && <CheckCircleRounded color="success" />}
</>
),
}}
disabled={fgUangMuka === PAYMENT_SINGLE_PAYMENT}
sx={{
'& .MuiInputBase-root.Mui-disabled': {
backgroundColor: '#f6f6f6',
},
}}
/>
</Grid> */}
<Grid size={{ md: 6 }}>
<Field.DatePicker
name="tanggalFaktur"
......
// 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) => ({
// 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',
// totalDiskon: '0',
// totalDpp: String(raw.totaldpp || '0'),
// totalDppLain: String(raw.totaldpplain || '0'),
// totalPpn: String(raw.totalppn || '0'),
// totalPpnbm: String(raw.totalppnbm || '0'),
// });
const formatDate = (iso: string) => {
if (!iso) return '';
const d = new Date(iso);
......
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, DoneRounded, CloseRounded } from '@mui/icons-material';
import { IconButton, Tooltip } from '@mui/material';
type Props = {
value?: string;
revNo?: number;
fgpelunasan?: string | boolean;
fguangmuka?: string | boolean;
valid?: string;
credit?: string | null;
};
const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka, credit, valid }) => {
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 === null) {
return (
<Chip
label="Belum diKreditkan"
size="small"
variant="outlined"
sx={{
borderColor: '#2e7d32',
color: '#2e7d32',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
}
return null;
})();
const componentValid = (() => {
if (valid === 'Valid') {
return (
<Tooltip title="Valid">
<IconButton
size="small"
sx={{
backgroundColor: '#34C759',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: '#34C759',
},
}}
>
<DoneRounded style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
);
} else {
return (
<Tooltip title="Invalid">
<IconButton
size="small"
sx={{
backgroundColor: '#DC2626',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: '#DC2626',
},
}}
>
<CloseRounded 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') {
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 === 'AMENDED') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<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)',
}}
/>
<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 === '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}
{componentValid}
</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 useCancelFakturPk from '../../hooks/useCancelFakturPK';
interface ModalCancelFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalCancelFakturPM: React.FC<ModalCancelFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Data berhasil dibatalkan',
onConfirmCancel,
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useCancelFakturPk({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
// setSelectionModel?.(undefined);
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmCancel?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal membantalkan data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['faktur', 'pk'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogCancel, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin 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 ModalCancelFakturPM;
import React, { useEffect, useState } from 'react';
import { enqueueSnackbar } from 'notistack';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogContent from '@mui/material/DialogContent';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import useCetakPdfFakturPM from '../../hooks/useCetakFakturPM';
interface ModalCetakPdfFakturPMProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
const ModalCetakFakturPM: React.FC<ModalCetakPdfFakturPMProps> = ({ payload, isOpen, onClose }) => {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
console.log(payload, 'ini dari cetak');
const { mutateAsync } = useCetakPdfFakturPM({
onError: (error: any) => {
enqueueSnackbar(error?.message || 'Gagal memuat PDF', { variant: 'error' });
setLoading(false);
},
onSuccess: (res: any) => {
const fileUrl = res?.url || res?.data?.url;
if (!fileUrl) {
enqueueSnackbar('URL PDF tidak ditemukan di respons API', { variant: 'warning' });
setLoading(false);
return;
}
setPdfUrl(fileUrl);
setLoading(false);
enqueueSnackbar(res?.MsgStatus || 'PDF berhasil dibentuk', { variant: 'success' });
// console.log('Ok');
},
});
useEffect(() => {
const runCetak = async () => {
if (!isOpen || !payload) return;
setLoading(true);
setPdfUrl(null);
try {
// Payload sudah lengkap dari parent (sudah ada namaObjekPajak, pasalPPh, statusPPh)
// const normalized = normalizePayloadCetakPdf(payload);
// console.log('📤 Payload final cetak PDF:', normalized);
await mutateAsync({ id: payload.id });
} catch (err) {
console.error('❌ Error cetak PDF:', err);
enqueueSnackbar('Gagal generate PDF', { variant: 'error' });
setLoading(false);
}
};
runCetak();
}, [isOpen, payload, mutateAsync]);
return (
<DialogUmum maxWidth="lg" isOpen={isOpen} onClose={onClose} title="Detail Pajak Keluaran">
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
{loading && (
<Box display="flex" justifyContent="center" alignItems="center" height="60vh">
<CircularProgress />
</Box>
)}
{!loading && pdfUrl && (
<iframe
src={pdfUrl}
style={{
width: '100%',
height: '80vh',
border: 'none',
borderRadius: 8,
}}
title="Preview PDF Faktur PK"
/>
)}
{!loading && !pdfUrl && (
<Box textAlign="center" color="text.secondary" py={4}>
PDF tidak tersedia untuk ditampilkan.
</Box>
)}
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakFakturPM;
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 useConfirmationWaitingFakturPM from '../../hooks/useConfirmationWaitingFakturPM';
import type { TValidateFakturPMRequest } from '../../types/types';
// type ConfirmationVariant = 'CANCEL' | 'PENGGANTI';
interface ModalConfirmationWaitingFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
variant: 'CANCEL' | 'PENGGANTI';
}
/** normalize berbagai bentuk selection menjadi array id */
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// kalau berbentuk map/object {'1': true, '2': true}
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalConfirmationWaitingFaktur: React.FC<ModalConfirmationWaitingFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Proses berhasil',
onConfirmCancel,
variant,
}) => {
const queryClient = useQueryClient();
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// gunakan hook mutation; pemanggil modal bisa inject onSuccess/onError jika ingin
const { mutateAsync } = useConfirmationWaitingFakturPM({
onSuccess: () => {
processSuccess();
},
onError: () => {
processFail();
},
});
const MODAL_CONFIG = {
CANCEL: {
title: 'Apakah Anda yakin akan melakukan pembatalan?',
description: '',
actionTitle: 'Batalkan',
successMessage: 'Faktur berhasil dibatalkan',
},
PENGGANTI: {
title: 'Apakah Anda yakin akan melakukan pengganti?',
description: '',
actionTitle: 'Konfirmasi',
successMessage: 'Faktur berhasil dikonfirmasi',
},
} as const;
const cfg = MODAL_CONFIG[variant];
const normalizeNomorFaktur = (val?: string | null) => {
if (!val) return 'INV#';
return val.startsWith('INV#') ? val : `INV#${val}`;
};
const doRequests = async (ids: (string | number)[]) =>
Promise.allSettled(
ids.map(async (id) => {
let nomorFaktur = 'INV#';
// ambil row dulu
try {
const row = tableApiRef?.current?.getRow?.(id);
if (row?.nomorfaktur) {
nomorFaktur = normalizeNomorFaktur(String(row.nomorfaktur));
}
} catch {
// ignore
}
const payload: TValidateFakturPMRequest = {
id: Number(id),
nomorFaktur,
};
return mutateAsync(payload);
})
);
const clearSelection = () => {
// clear grid selection via apiRef if available
try {
tableApiRef?.current?.setRowSelectionModel?.([]);
} catch {
// ignore if API differs
}
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
resetToDefault();
};
const onSubmit = async () => {
try {
const ids = normalizeSelection(dataSelected);
if (!ids.length) {
enqueueSnackbar('Tidak ada data terpilih', { variant: 'warning' });
return;
}
setNumberOfData(ids.length);
setIsOpenDialogProgressBar(true);
const settled = await doRequests(ids);
// count results
const successCount = settled.filter((r) => r.status === 'fulfilled').length;
const failCount = settled.length - successCount;
// show feedback
if (successCount > 0) {
enqueueSnackbar(successMessage, { variant: 'success' });
}
if (failCount > 0) {
enqueueSnackbar(`${failCount} item gagal diproses`, { variant: 'error' });
}
await onConfirmCancel?.();
// close + reset
handleCloseModal();
clearSelection();
// invalidasi queries (sesuaikan queryKey dengan kebutuhan)
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
} catch (err: any) {
enqueueSnackbar(err?.message || 'Proses gagal', { variant: 'error' });
} finally {
setIsOpenDialogProgressBar(false);
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogCancel, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="sm"
title={cfg.title}
description=""
actionTitle="Ya"
isOpen={isOpenDialogCancel}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={onSubmit}
/>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalConfirmationWaitingFaktur;
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 = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
faktuPK: {
all: (params: any) => [appRootKey, 'fakturPk', params],
detail: (params: any) => [appRootKey, 'fakturPk', '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;
import { useMutation } from '@tanstack/react-query';
import type { TCancelRequest, TCancelResponse } from '../types/types';
import fakturApi from '../utils/api';
const useCancelFakturPk = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelRequest>({
mutationKey: ['cancel-faktur-pk'],
mutationFn: (payload) => fakturApi.cancel(payload),
...props,
});
export default useCancelFakturPk;
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
const useCetakPdfFakturPM = (options?: any) =>
useMutation({
mutationFn: async (params: any) => fakturApi.cetakPdfDetail(params),
...options,
});
export default useCetakPdfFakturPM;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import type { TBaseResponseAPI, TValidateFakturPMRequest } from '../types/types';
import fakturApi from '../utils/api';
// Hook menerima UseMutationOptions sehingga pemanggil bisa inject onSuccess/onError dsb.
const useConfirmationWaitingFakturPM = (
options?: UseMutationOptions<TBaseResponseAPI<null>, Error, TValidateFakturPMRequest>
) =>
useMutation<TBaseResponseAPI<null>, Error, TValidateFakturPMRequest>({
mutationKey: ['confirmation-waiting-faktur-pm'],
mutationFn: (payload) => fakturApi.confirmationWaitingFakturPM(payload),
...options,
});
export default useConfirmationWaitingFakturPM;
import { isEmpty } from 'lodash';
import { useQuery } from '@tanstack/react-query';
import queryKey from '../constant/queryKey';
import type { TGetListDataTableFakturPMResult } from '../types/types';
import fakturApi from '../utils/api';
export type TGetFakturPMApiWrapped = {
data: TGetListDataTableFakturPMResult[];
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 useGetFakturPM = ({ params }: { params: any }) => {
const normalized = normalizeParams(params);
const { page, limit, advanced, sortingMode, sortingMethod, ...rest } = normalized;
const isListRequest =
page !== undefined ||
limit !== undefined ||
advanced !== undefined ||
sortingMode ||
sortingMethod;
return useQuery<TGetFakturPMApiWrapped>({
queryKey: isListRequest
? ['faktur-pk', page, limit, advanced, sortingMode, sortingMethod]
: ['faktur-pk', rest], // supaya cache beda antara list dan search tunggal
queryFn: async () => {
const res: any = await fakturApi.getFakturPM({ 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 TGetListDataTableFakturPMResult[],
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 useGetFakturPKById = (id: string, options = {}) =>
useQuery({
queryKey: queryKey.faktuPK.detail(id),
queryFn: async () => {
const res = await fakturApi.getFakturPMById(id);
if (!res) throw new Error('Data tidak ditemukan');
return res;
},
enabled: !!id,
refetchOnWindowFocus: false,
...options,
});
export default useGetFakturPM;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import type { TBaseResponseAPI, TPrepopulatedPMRequest } from '../types/types';
import fakturApi from '../utils/api';
// Hook menerima UseMutationOptions supaya bisa inject onSuccess / onError dari pemanggil
const usePrepopulatedPM = (
options?: UseMutationOptions<TBaseResponseAPI<any>, Error, TPrepopulatedPMRequest>
) =>
useMutation<TBaseResponseAPI<any>, Error, TPrepopulatedPMRequest>({
mutationKey: ['prepopulated-faktur-pm'],
mutationFn: (payload) => fakturApi.prepopulatedPM(payload),
...options,
});
export default usePrepopulatedPM;
import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import type { TPostFakturPKRequest } from '../types/types';
import fakturApi from '../utils/api';
/**
* Mapping nilai radio ke boolean flag fgUangMuka & fgPelunasan
*/
const getPaymentFlags = (value: string) => {
switch (value) {
case 'uang_muka':
return { fgUangMuka: true, fgPelunasan: false };
case 'pelunasan':
return { fgUangMuka: false, fgPelunasan: true };
default:
// single_payment atau lainnya
return { fgUangMuka: false, fgPelunasan: false };
}
};
/**
* Transformasi data form menjadi payload untuk endpoint IF_TXR_001/create
*/
const transformParams = (formData: any): TPostFakturPKRequest => {
const {
id, // internal id
fgUangMuka, // string dari radio
nomorFaktur,
nomorFakturDiganti,
detailTransaksi,
idKeteranganTambahan,
keteranganTambahan,
masaPajak,
tahunPajak,
refDoc,
referensi,
npwpPembeli,
idLainPembeli,
kdNegaraPembeli,
nikPaspPembeli,
namaPembeli,
tkuPembeli,
alamatPembeli,
emailPembeli,
keterangan1,
keterangan2,
keterangan3,
keterangan4,
keterangan5,
objekFaktur,
jumlahUangMuka,
totalDpp,
totalDppLain,
totalPpn,
totalPpnbm,
tanggalFaktur,
fgPengganti,
capKetTambahan,
uangMukaDpp,
uangMukaDppLain,
uangMukaPpn,
uangMukaPpnbm,
} = formData;
let finalNpwpPembeli = npwpPembeli;
let finalNikPaspPembeli = nikPaspPembeli;
if (idLainPembeli === '2' || idLainPembeli === '3') {
// jika Paspor / ID Lain → nikPaspPembeli ambil dari npwpPembeli
finalNikPaspPembeli = npwpPembeli || '';
// npwpPembeli HARUS menjadi 16 digit nol
finalNpwpPembeli = '0000000000000000';
}
// 🔁 Konversi nilai radio ke boolean flag
const { fgUangMuka: flagUangMuka, fgPelunasan: flagPelunasan } = getPaymentFlags(fgUangMuka);
const totalDiskon = Array.isArray(objekFaktur)
? objekFaktur.reduce((sum, item) => sum + Number(item.diskon ?? 0), 0)
: 0;
return {
id: id ?? null,
fgUangMuka: flagUangMuka,
fgPelunasan: flagPelunasan,
nomorFaktur: nomorFaktur ?? '',
nomorFakturDiganti: nomorFakturDiganti ?? '',
detailTransaksi: detailTransaksi ?? '',
idKeteranganTambahan: idKeteranganTambahan ?? '',
keteranganTambahan: keteranganTambahan ?? '',
masaPajak: String(masaPajak ?? dayjs().month() + 1).padStart(2, '0'),
tahunPajak: String(tahunPajak ?? dayjs().year()),
refDoc: refDoc ?? '',
referensi: referensi ?? '',
// npwpPembeli: npwpPembeli ?? '',
npwpPembeli: finalNpwpPembeli,
idLainPembeli: idLainPembeli ?? '',
kdNegaraPembeli: kdNegaraPembeli ?? 'IDN',
// nikPaspPembeli: nikPaspPembeli ?? '',
nikPaspPembeli: finalNikPaspPembeli,
namaPembeli: namaPembeli ?? '',
tkuPembeli: tkuPembeli ?? '',
alamatPembeli: alamatPembeli ?? '',
emailPembeli: emailPembeli ?? '',
keterangan1: keterangan1 ?? '',
keterangan2: keterangan2 ?? '',
keterangan3: keterangan3 ?? '',
keterangan4: keterangan4 ?? '',
keterangan5: keterangan5 ?? '',
objekFaktur: Array.isArray(objekFaktur)
? objekFaktur.map((item) => ({
brgJasa: item.brgJasa ?? '',
kdBrgJasa: item.kdBrgJasa ?? '',
namaBrgJasa: item.namaBrgJasa ?? '',
satuanBrgJasa: item.satuanBrgJasa ?? '',
hargaSatuan: Number(item.hargaSatuan ?? 0),
jmlBrgJasa: Number(item.jmlBrgJasa ?? 0),
totalHarga: Number(item.totalHarga ?? 0),
diskon: Number(item.diskon ?? 0),
// cekDppLain: item.cekDppLain,
cekDppLain: item.cekDppLain !== undefined ? item.cekDppLain : null, // biarkan kosong, jangan ubah
dpp: Number(item.dpp ?? 0),
dppLain: Number(item.dppLain ?? 0),
tarifPpn: Number(item.tarifPpn ?? 11),
ppn: Number(item.ppn ?? 0),
tarifPpnbm: Number(item.tarifPpnbm ?? 0),
ppnbm: Number(item.ppnbm ?? 0),
}))
: [],
jumlahUangMuka:
fgUangMuka === 'uang_muka'
? Number(jumlahUangMuka ?? 0)
: fgUangMuka === 'pelunasan'
? Number(uangMukaDpp ?? 0)
: 0,
// jumlahUangMuka: Number(totalDpp ?? 0),
totalDpp: Number(totalDpp ?? 0),
totalDppLain: Number(totalDppLain ?? 0),
totalPpn: Number(totalPpn ?? 0),
totalPpnbm: Number(totalPpnbm ?? 0),
uangMukaDpp: Number(uangMukaDpp ?? 0),
uangMukaDppLain: Number(uangMukaDppLain ?? 0),
uangMukaPpn: Number(uangMukaPpn ?? 0),
uangMukaPpnbm: Number(uangMukaPpnbm ?? 0),
tanggalFaktur: tanggalFaktur
? dayjs(String(tanggalFaktur)).format('DDMMYYYY')
: dayjs().format('DDMMYYYY'),
fgPengganti: String(fgPengganti ?? '0'),
capKetTambahan: capKetTambahan ?? '',
totalDiskon: String(totalDiskon),
};
};
/**
* Hook untuk menyimpan data Faktur PK ke endpoint IF_TXR_001/create
*/
const useSaveFakturPk = (props?: any) =>
useMutation({
mutationKey: ['save-fakturpk'],
mutationFn: async (params: any) => {
const payload = transformParams(params);
return fakturApi.saveFakturPK(payload);
},
...props,
});
export default useSaveFakturPk;
// hooks/useUpload.ts
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
const useUpload = (props?: any) =>
useMutation({
mutationKey: ['upload-faktur'],
mutationFn: (payload: { id: string | number }) => fakturApi.upload(payload),
...props,
});
export default useUpload;
import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
import type { TBaseResponseAPI, TUploadFakturPMRequest } from '../types/types';
import fakturApi from '../utils/api';
// Hook menerima UseMutationOptions sehingga pemanggil bisa inject onSuccess/onError dsb.
const useUploadPMPrepop = (
options?: UseMutationOptions<TBaseResponseAPI<null>, Error, TUploadFakturPMRequest>
) =>
useMutation<TBaseResponseAPI<null>, Error, TUploadFakturPMRequest>({
mutationKey: ['upload-prepop-faktur-pm'],
mutationFn: (payload) => fakturApi.uploadPMPrepop(payload),
...options,
});
export default useUploadPMPrepop;
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 TGetListDataTableFakturPM = {
id: number;
fguangmuka: boolean;
fgpelunasan: boolean;
nomorfaktur: string;
detailtransaksi: string;
idketerangantambahan: string;
keterangantambahan: string;
masapajak: string;
tahunpajak: string;
refdoc: string;
referensi: string;
npwppenjual: string;
namatokopenjual: string;
npwppembeli: string;
idlainpembeli: string;
kdnegarapembeli: string;
nikpasppembeli: string;
namapembeli: string;
tkupembeli: string;
alamatpembeli: string | null;
emailpembeli: string;
totaldpp: string;
totaldpplain: string;
jumlahuangmuka: string;
totalppn: string;
totalppnbm: string;
tempatpenandatangan: string | null;
tanggalfaktur: string;
npwpnikpenandatangan: string | null;
passphrasepenandatangan: string | null;
userid: string;
kanal: string | null;
approvalsign: string | null;
idfaktur: string;
tanggalapproval: string | null;
statusfaktur: string;
kodeapproval: string | null;
created_by: string;
updated_by: string;
created_at: string;
updated_at: string;
testing1: string | null;
testing2: string | null;
detailkredittransaksi: string | null;
tahunkreditpajak: string;
masakreditpajak: string;
statuspembeli: string;
buyerstatus: string;
internal_id: string;
fgpengganti: string;
fgbatal: string;
errormsg: string | null;
keterangan1: string;
keterangan2: string;
keterangan3: string;
keterangan4: string;
keterangan5: string;
tkupenjual: string;
uangmukadpp: string | null;
uangmukadpplain: string | null;
uangmukappn: string | null;
uangmukappnbm: string | null;
jumlahpelunasan: string | null;
};
export type TGetListDataTableFakturPMResult = TGetListDataTableFakturPM[];
export type TValidateFakturPMRequest = {
id: number;
nomorFaktur: string;
};
export interface TKonfirmasiFakturMasukan {
konfirmasiPengkreditan: 'CREDITED' | 'UNCREDITED' | 'INVALID' | string;
nomorFaktur: string;
masaKredit: string; // "12"
tahunKredit: string; // "2025"
npwpPembeli: string;
npwpPenjual: string;
}
export interface TUploadFakturPMRequest {
fgPermintaan: number; // 2
konfirmasiFakturMasukan: TKonfirmasiFakturMasukan;
}
// 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 TPostUpload = {
id: string;
};
export type TDeleteRequest = {
id: string;
};
export type TCancelRequest = {
id: string | number;
revokeFlag?: boolean; // format: DDMMYYYY
tglPembatalan?: string;
};
export type TCancelResponse = TBaseResponseAPI<{
id: string | number;
statusBatal?: string;
message?: string;
}>;
import axios from 'axios';
import type {
TBaseResponseAPI,
TCancelRequest,
TCancelResponse,
TGetListDataTableFakturPMResult,
TPrepopulatedPMRequest,
TUploadFakturPMRequest,
TValidateFakturPMRequest,
} from '../types/types';
import unifikasiClient from './unifikasiClient';
import { normalizePayloadCetakPdfDetail } from './normalizePayloadCetakPdf';
const fakturApi = () => {};
const axiosCetakPdf = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API_URL_CETAK,
headers: {
Authorization: `Basic ${window.btoa('admin:ortax123')}`,
password: '',
},
});
// API untuk get list table
fakturApi.getFakturPM = async (config: any) => {
const {
data: { message, metaPage, data },
status: statusCode,
} = await unifikasiClient.get<TBaseResponseAPI<TGetListDataTableFakturPMResult>>('/IF_TXR_015', {
...config,
});
if (statusCode !== 200) {
throw new Error(message);
}
return { total: metaPage ? Number(metaPage.totalRow) : 0, data };
};
fakturApi.getFakturPMById = async (id: string) => {
const res = await unifikasiClient.get('/IF_TXR_015', { params: { id } });
const {
data: { status, message, data },
status: statusCode,
} = res;
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('getFakturPM failed:', { statusCode, status, message });
throw new Error(message || 'Gagal mengambil data FakturPM');
}
const dnData = Array.isArray(data) ? data[0] : data;
return dnData;
};
fakturApi.confirmationWaitingFakturPM = async (payload: TValidateFakturPMRequest) => {
const {
data: { status, message, data },
status: statusCode,
} = await unifikasiClient.post('/IF_TXR_062', payload);
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('validateFakturPM failed:', { statusCode, status, message });
throw new Error(message || 'Gagal memvalidasi Faktur Pajak');
}
return data;
};
fakturApi.uploadPMPrepop = async (payload: TUploadFakturPMRequest) => {
const {
data: { status, message, data },
status: statusCode,
} = await unifikasiClient.post('/IF_TXR_015/upload', payload);
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('validateFakturPM failed:', { statusCode, status, message });
throw new Error(message || 'Gagal memvalidasi Faktur Pajak');
}
return data;
};
fakturApi.cetakPdfDetail = async (payload: { id: string }) => {
// 1️⃣ Ambil data faktur dari Unifikasi
const fakturRaw = await fakturApi.getFakturPMById(payload.id);
console.log();
// 2️⃣ Normalize agar sesuai format yang dibutuhkan API cetak PDF
const payloadCetak = normalizePayloadCetakPdfDetail(fakturRaw);
// 3️⃣ Call API PDF
const response = await axiosCetakPdf.post('/report/ctas/faktur/pk', payloadCetak);
const body = response.data;
// 4️⃣ Validasi response
if (
!response ||
response.status !== 200 ||
body.status === 'fail' ||
body.status === 'error' ||
body.status === '0'
) {
throw new Error(
body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi'
);
}
return body;
};
fakturApi.prepopulatedPM = async (payload: TPrepopulatedPMRequest) => {
const {
data: { status, message, data },
status: statusCode,
} = await unifikasiClient.post('/IF_TXR_015/prepopulated', payload);
if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('prepopulatedPM failed:', { statusCode, status, message });
throw new Error(message || 'Gagal melakukan prepopulated Faktur PM');
}
return data;
};
fakturApi.cancel = async ({ id, revokeFlag }: TCancelRequest): Promise<TCancelResponse> => {
const {
data: { status, message, data, code, time, metaPage, total },
} = await unifikasiClient.post('/IF_TXR_006', {
id,
revokeFlag,
});
console.log('Cancel Digunggung response:', { code, message, status });
if (code === 0) {
throw new Error(message || 'Gagal membatalkan data');
}
return {
status,
message,
data,
code,
time,
metaPage,
total,
};
};
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'),
};
};
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 : '';
};
This diff is collapsed.
export * from './faktur-pm-list-view';
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