Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Sign in
Toggle navigation
C
ctas-box
Project overview
Project overview
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Container Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Fachri
ctas-box
Commits
d89056f3
Commit
d89056f3
authored
Oct 23, 2025
by
Fachri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
bpu non residen
parent
9ffe9ff5
Changes
41
Hide whitespace changes
Inline
Side-by-side
Showing
41 changed files
with
2610 additions
and
1514 deletions
+2610
-1514
src/main.tsx
src/main.tsx
+4
-0
src/pages/dashboard/index.tsx
src/pages/dashboard/index.tsx
+0
-6
src/pages/unifikasi/unifikasiNr.tsx
src/pages/unifikasi/unifikasiNr.tsx
+2
-2
src/routes/paths.ts
src/routes/paths.ts
+0
-25
src/routes/sections/dashboard.tsx
src/routes/sections/dashboard.tsx
+1
-0
src/sections/bupot-unifikasi/bupot-dn/components/rekamDn/Identitas.tsx
...bupot-unifikasi/bupot-dn/components/rekamDn/Identitas.tsx
+1
-173
src/sections/bupot-unifikasi/bupot-dn/components/rekamDn/PphDipotong.tsx
...pot-unifikasi/bupot-dn/components/rekamDn/PphDipotong.tsx
+5
-269
src/sections/bupot-unifikasi/bupot-dn/hooks/useAdvancedFilterDn.tsx
...ns/bupot-unifikasi/bupot-dn/hooks/useAdvancedFilterDn.tsx
+0
-192
src/sections/bupot-unifikasi/bupot-dn/hooks/useGetDn.tsx
src/sections/bupot-unifikasi/bupot-dn/hooks/useGetDn.tsx
+0
-18
src/sections/bupot-unifikasi/bupot-dn/view/dnRekamView.tsx
src/sections/bupot-unifikasi/bupot-dn/view/dnRekamView.tsx
+44
-30
src/sections/bupot-unifikasi/bupot-nr/components/CustomColumnsButton.tsx
...pot-unifikasi/bupot-nr/components/CustomColumnsButton.tsx
+2
-1
src/sections/bupot-unifikasi/bupot-nr/components/CustomFilterButton.tsx
...upot-unifikasi/bupot-nr/components/CustomFilterButton.tsx
+1
-507
src/sections/bupot-unifikasi/bupot-nr/components/CustomToolbar.tsx
...ons/bupot-unifikasi/bupot-nr/components/CustomToolbar.tsx
+3
-2
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalCancelNr.tsx
...ot-unifikasi/bupot-nr/components/dialog/ModalCancelNr.tsx
+228
-0
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalCetakPdfNr.tsx
...-unifikasi/bupot-nr/components/dialog/ModalCetakPdfNr.tsx
+3
-14
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalDeleteNr.tsx
...ot-unifikasi/bupot-nr/components/dialog/ModalDeleteNr.tsx
+5
-7
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalUploadNr.tsx
...ot-unifikasi/bupot-nr/components/dialog/ModalUploadNr.tsx
+9
-9
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/DokumenReferensi.tsx
...nifikasi/bupot-nr/components/rekamNr/DokumenReferensi.tsx
+59
-0
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/Identitas.tsx
...bupot-unifikasi/bupot-nr/components/rekamNr/Identitas.tsx
+231
-0
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/PanduanDnRekam.tsx
...-unifikasi/bupot-nr/components/rekamNr/PanduanDnRekam.tsx
+159
-0
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/PphDipotong.tsx
...pot-unifikasi/bupot-nr/components/rekamNr/PphDipotong.tsx
+674
-0
src/sections/bupot-unifikasi/bupot-nr/constant/queryKey.tsx
src/sections/bupot-unifikasi/bupot-nr/constant/queryKey.tsx
+8
-8
src/sections/bupot-unifikasi/bupot-nr/hooks/useAdvancedFilterNr.tsx
...ns/bupot-unifikasi/bupot-nr/hooks/useAdvancedFilterNr.tsx
+170
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useCancelDn.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useCancelDn.tsx
+0
-12
src/sections/bupot-unifikasi/bupot-nr/hooks/useCancelNr.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useCancelNr.tsx
+12
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useCetakPdfNr.tsx
...sections/bupot-unifikasi/bupot-nr/hooks/useCetakPdfNr.tsx
+11
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useDeleteNr.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useDeleteNr.tsx
+12
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetKodeObjekPajakNr.tsx
...bupot-unifikasi/bupot-nr/hooks/useGetKodeObjekPajakNr.tsx
+12
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetNegara.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetNegara.tsx
+14
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetNr.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetNr.tsx
+72
-64
src/sections/bupot-unifikasi/bupot-nr/hooks/usePphDipotong.tsx
...ections/bupot-unifikasi/bupot-nr/hooks/usePphDipotong.tsx
+4
-3
src/sections/bupot-unifikasi/bupot-nr/hooks/useSaveDn.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useSaveDn.tsx
+0
-102
src/sections/bupot-unifikasi/bupot-nr/hooks/useSaveNr.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useSaveNr.tsx
+133
-0
src/sections/bupot-unifikasi/bupot-nr/hooks/useUpload.tsx
src/sections/bupot-unifikasi/bupot-nr/hooks/useUpload.tsx
+3
-3
src/sections/bupot-unifikasi/bupot-nr/types/types.ts
src/sections/bupot-unifikasi/bupot-nr/types/types.ts
+167
-0
src/sections/bupot-unifikasi/bupot-nr/utils/api.tsx
src/sections/bupot-unifikasi/bupot-nr/utils/api.tsx
+49
-35
src/sections/bupot-unifikasi/bupot-nr/utils/normalizePayloadCetakPdf.ts
...upot-unifikasi/bupot-nr/utils/normalizePayloadCetakPdf.ts
+4
-4
src/sections/bupot-unifikasi/bupot-nr/view/index.ts
src/sections/bupot-unifikasi/bupot-nr/view/index.ts
+0
-0
src/sections/bupot-unifikasi/bupot-nr/view/nr-list-view.tsx
src/sections/bupot-unifikasi/bupot-nr/view/nr-list-view.tsx
+58
-28
src/sections/bupot-unifikasi/bupot-nr/view/nrRekamView.tsx
src/sections/bupot-unifikasi/bupot-nr/view/nrRekamView.tsx
+385
-0
src/sections/bupot-unifikasi/bupot-nr/workers/normalizeNr.worker.js
...ns/bupot-unifikasi/bupot-nr/workers/normalizeNr.worker.js
+65
-0
No files found.
src/main.tsx
View file @
d89056f3
...
...
@@ -6,6 +6,10 @@ import { LicenseInfo } from '@mui/x-license';
import
App
from
'
./app
'
;
import
{
routesSection
}
from
'
./routes/sections
'
;
import
{
ErrorBoundary
}
from
'
./routes/components
'
;
import
dayjs
from
'
dayjs
'
;
import
customParseFormat
from
'
dayjs/plugin/customParseFormat
'
;
dayjs
.
extend
(
customParseFormat
);
// ----------------------------------------------------------------------
LicenseInfo
.
setLicenseKey
(
...
...
src/pages/dashboard/index.tsx
View file @
d89056f3
import
{
CONFIG
}
from
'
src/global-config
'
;
import
{
DashboardView
}
from
'
src/sections/dashboard/view
'
;
// import { OverviewAppView } from 'src/sections/overview/app/view';
// ----------------------------------------------------------------------
const
metadata
=
{
title
:
`Dashboard -
${
CONFIG
.
appName
}
`
};
export
default
function
OverviewAppPage
()
{
return
(
<>
<
title
>
{
metadata
.
title
}
</
title
>
{
/* <OverviewAppView /> */
}
{
/* aaa */
}
<
DashboardView
/>
</>
);
...
...
src/pages/unifikasi/unifikasiNr.tsx
View file @
d89056f3
import
{
CONFIG
}
from
'
src/global-config
'
;
import
{
NrListView
}
from
'
src/sections/bupot-unifikasi/bupot-nr/view/nr-list-view
'
;
//
import { NrListView } from 'src/sections/bupot-unifikasi/bupot-nr/view';
import
{
NrListView
}
from
'
src/sections/bupot-unifikasi/bupot-nr/view
'
;
const
metadata
=
{
title
:
`E-Bupot Unifikasi-
${
CONFIG
.
appName
}
`
};
...
...
src/routes/paths.ts
View file @
d89056f3
...
...
@@ -143,30 +143,5 @@ export const paths = {
edit
:
(
id
:
string
)
=>
`
${
ROOTS
.
DASHBOARD
}
/user/
${
id
}
/edit`
,
demo
:
{
edit
:
`
${
ROOTS
.
DASHBOARD
}
/user/
${
MOCK_ID
}
/edit`
},
},
product
:
{
root
:
`
${
ROOTS
.
DASHBOARD
}
/product`
,
new
:
`
${
ROOTS
.
DASHBOARD
}
/product/new`
,
details
:
(
id
:
string
)
=>
`
${
ROOTS
.
DASHBOARD
}
/product/
${
id
}
`
,
edit
:
(
id
:
string
)
=>
`
${
ROOTS
.
DASHBOARD
}
/product/
${
id
}
/edit`
,
demo
:
{
details
:
`
${
ROOTS
.
DASHBOARD
}
/product/
${
MOCK_ID
}
`
,
edit
:
`
${
ROOTS
.
DASHBOARD
}
/product/
${
MOCK_ID
}
/edit`
,
},
},
invoice
:
{
root
:
`
${
ROOTS
.
DASHBOARD
}
/invoice`
,
new
:
`
${
ROOTS
.
DASHBOARD
}
/invoice/new`
,
details
:
(
id
:
string
)
=>
`
${
ROOTS
.
DASHBOARD
}
/invoice/
${
id
}
`
,
edit
:
(
id
:
string
)
=>
`
${
ROOTS
.
DASHBOARD
}
/invoice/
${
id
}
/edit`
,
demo
:
{
details
:
`
${
ROOTS
.
DASHBOARD
}
/invoice/
${
MOCK_ID
}
`
,
edit
:
`
${
ROOTS
.
DASHBOARD
}
/invoice/
${
MOCK_ID
}
/edit`
,
},
},
order
:
{
root
:
`
${
ROOTS
.
DASHBOARD
}
/order`
,
details
:
(
id
:
string
)
=>
`
${
ROOTS
.
DASHBOARD
}
/order/
${
id
}
`
,
demo
:
{
details
:
`
${
ROOTS
.
DASHBOARD
}
/order/
${
MOCK_ID
}
`
},
},
},
};
src/routes/sections/dashboard.tsx
View file @
d89056f3
...
...
@@ -122,6 +122,7 @@ export const dashboardRoutes: RouteObject[] = [
{
path
:
'
dn/:id/:type
'
,
element
:
<
OverviewUnifikasiRekamDnPage
/>
},
{
path
:
'
nr
'
,
element
:
<
OverviewUnifikasiNrPage
/>
},
{
path
:
'
nr/new
'
,
element
:
<
OverviewUnifikasiRekamNrPage
/>
},
{
path
:
'
nr/:id/:type
'
,
element
:
<
OverviewUnifikasiRekamNrPage
/>
},
{
path
:
'
ssp
'
,
element
:
<
OverviewUnifikasiSspPage
/>
},
{
path
:
'
ssp/new
'
,
element
:
<
OverviewUnifikasiRekamSspPage
/>
},
{
path
:
'
digunggung
'
,
element
:
<
OverviewUnifikasiDigunggungPage
/>
},
...
...
src/sections/bupot-unifikasi/bupot-dn/components/rekamDn/Identitas.tsx
View file @
d89056f3
// import Box from '@mui/material/Box';
// import Button from '@mui/material/Button';
// import Grid from '@mui/material/Grid';
// import dayjs from 'dayjs';
// import React, { useEffect, useState } from 'react';
// import { useFormContext } from 'react-hook-form';
// import { Field } from 'src/components/hook-form';
// type IdentitasProps = {
// isPengganti: boolean;
// existingDn?: any; // data penuh dari API
// };
// const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => {
// const { setValue, watch, getValues } = useFormContext();
// const tanggalPemotongan = watch('tglPemotongan');
// const maxKeterangan = 5;
// const [jumlahKeterangan, setJumlahKeterangan] = useState<number>(0);
// // 🧩 Auto isi tahun & masa pajak berdasarkan tanggalPemotongan
// useEffect(() => {
// if (tanggalPemotongan) {
// const date = dayjs(tanggalPemotongan);
// setValue('thnPajak', date.format('YYYY'));
// setValue('msPajak', date.format('MM'));
// } else {
// setValue('thnPajak', '');
// setValue('msPajak', '');
// }
// }, [tanggalPemotongan, setValue]);
// useEffect(() => {
// // ambil nilai form saat ini (setelah reset di parent)
// const currentValues = getValues();
// const arr = [
// currentValues.keterangan1,
// currentValues.keterangan2,
// currentValues.keterangan3,
// currentValues.keterangan4,
// currentValues.keterangan5,
// ];
// const count = arr.filter((k) => !!k && k.trim() !== '').length;
// console.log('🧠 Detected keterangan count:', count, arr);
// // kalau ada field terisi, render sebanyak itu
// if (count > 0) {
// setJumlahKeterangan(count);
// }
// }, [existingDn, getValues]);
// // ➕ Tambah field
// const handleTambah = () => {
// if (jumlahKeterangan < maxKeterangan) {
// setJumlahKeterangan((prev) => prev + 1);
// }
// };
// // ➖ Hapus field terakhir
// const handleHapus = () => {
// if (jumlahKeterangan > 0) {
// setValue(`keterangan${jumlahKeterangan}`, '');
// setJumlahKeterangan((prev) => prev - 1);
// }
// };
// console.log(existingDn);
// return (
// <>
// <Grid container rowSpacing={2} alignItems="center" columnSpacing={2} sx={{ mb: 4 }}>
// {/* 📅 Tanggal & Masa Pajak */}
// <Grid size={{ md: 6 }}>
// <Field.DatePicker
// name="tglPemotongan"
// label="Tanggal Pemotongan"
// format="DD/MM/YYYY"
// maxDate={dayjs()}
// />
// </Grid>
// <Grid size={{ md: 3 }}>
// <Field.DatePicker name="thnPajak" label="Tahun Pajak" view="year" format="YYYY" />
// </Grid>
// <Grid size={{ md: 3 }}>
// <Field.DatePicker name="msPajak" label="Masa Pajak" view="month" format="MM" />
// </Grid>
// {/* 🧾 NPWP dan NITKU */}
// <Grid size={{ md: 6 }}>
// <Field.Text
// name="idDipotong"
// label="NPWP"
// onChange={(e) => {
// const value = e.target.value.replace(/\D/g, '').slice(0, 16);
// setValue('idDipotong', value, { shouldValidate: true, shouldDirty: true });
// setValue('nitku', value.length === 16 ? value + '000000' : value, {
// shouldValidate: true,
// shouldDirty: true,
// });
// }}
// />
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Text
// name="nitku"
// label="NITKU"
// onChange={(e) => {
// const value = e.target.value.replace(/\D/g, '').slice(0, 22);
// setValue('nitku', value, { shouldValidate: true, shouldDirty: true });
// }}
// />
// </Grid>
// {/* 👤 Nama dan Email */}
// <Grid size={{ md: 6 }}>
// <Field.Text name="namaDipotong" label="Nama" />
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Text name="email" label="Email (optional)" />
// </Grid>
// </Grid>
// {/* ✏️ Tombol Tambah / Hapus Keterangan */}
// <Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
// <Box
// sx={{
// borderRadius: '18px',
// border: jumlahKeterangan >= maxKeterangan ? '1px solid #eee' : '1px solid #2e7d3280',
// color: jumlahKeterangan >= maxKeterangan ? '#eee' : '#2e7d3280',
// p: '0px 10px',
// }}
// >
// <Button disabled={jumlahKeterangan >= maxKeterangan} onClick={handleTambah}>
// Tambah Keterangan
// </Button>
// </Box>
// <Box
// sx={{
// borderRadius: '18px',
// border: jumlahKeterangan === 0 ? '1px solid #eee' : '1px solid #f44336',
// color: jumlahKeterangan === 0 ? '#eee' : '#f44336',
// p: '0px 10px',
// }}
// >
// <Button disabled={jumlahKeterangan === 0} onClick={handleHapus}>
// Hapus Keterangan
// </Button>
// </Box>
// </Box>
// {/* 🗒️ Input Keterangan Tambahan */}
// <Box sx={{ mb: 3 }}>
// {Array.from({ length: jumlahKeterangan }).map((_, i) => (
// <Grid size={{ md: 12 }} key={i}>
// <Field.Text
// sx={{ mb: 2 }}
// name={`keterangan${i + 1}`}
// label={`Keterangan Tambahan ${i + 1}`}
// />
// </Grid>
// ))}
// </Box>
// </>
// );
// };
// export default Identitas;
import
Box
from
'
@mui/material/Box
'
;
import
Button
from
'
@mui/material/Button
'
;
import
Grid
from
'
@mui/material/Grid
'
;
...
...
@@ -360,7 +188,7 @@ const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => {
{
/* 🗒️ Input Keterangan Tambahan */
}
<
Box
sx=
{
{
mb
:
3
}
}
>
{
Array
.
from
({
length
:
jumlahKeterangan
}).
map
((
_
,
i
)
=>
(
<
Grid
size=
{
{
md
:
12
}
}
key=
{
i
}
>
<
Grid
size=
{
{
md
:
12
}
}
key=
{
`keterangan${i + 1}`
}
>
<
Field
.
Text
sx=
{
{
mb
:
2
}
}
name=
{
`keterangan${i + 1}`
}
...
...
src/sections/bupot-unifikasi/bupot-dn/components/rekamDn/PphDipotong.tsx
View file @
d89056f3
/* eslint-disable react-hooks/exhaustive-deps */
// import Divider from '@mui/material/Divider';
// import Grid from '@mui/material/Grid';
// import MenuItem from '@mui/material/MenuItem';
// import React, { useEffect, useMemo, useRef, useState } from 'react';
// import { Field } from 'src/components/hook-form';
// import { TGetListDataKOPDn } from '../../types/types';
// import { useFormContext } from 'react-hook-form';
// import {
// FG_FASILITAS_DN,
// FG_FASILITAS_MASTER_KEY,
// FG_FASILITAS_TEXT,
// TARIF_0,
// } from '../../constant';
// import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
// type PPHDipotongProps = {
// kodeObjectPajak: TGetListDataKOPDn[];
// isFormPrefilled?: boolean; // true kalau mode edit
// };
// const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongProps) => {
// const { watch, setValue, getValues } = useFormContext<Record<string, any>>();
// const selectedKode = watch('kdObjPjk');
// const fgFasilitas = watch('fgFasilitas');
// const jmlBruto = watch('jmlBruto');
// const tarifWatched = watch('tarif');
// const kodeLoaded = Array.isArray(kodeObjectPajak) && kodeObjectPajak.length > 0;
// // ---- state & refs to control timing & user-interaction
// const [isPrefillDone, setIsPrefillDone] = useState(!isFormPrefilled);
// const initialCapturedRef = useRef<null | { kd?: any; fg?: any; tarif?: any; noDok?: any }>(null);
// const hasUserInteractedRef = useRef(false);
// const prevKdRef = useRef<string | undefined>(undefined);
// const prevFgRef = useRef<string | undefined>(undefined);
// // kode object pajak selected
// const kodeObjekPajakSelected = useMemo(
// () => kodeObjectPajak.find((item) => item.kode === selectedKode),
// [kodeObjectPajak, selectedKode]
// );
// // 1) Wait for both: parent says prefilled AND kodeObjectPajak data available
// useEffect(() => {
// if (!isFormPrefilled) {
// // create mode => ready immediately
// setIsPrefillDone(true);
// return;
// }
// // edit mode -> wait until kode data available (so we can decide default tariff correctly)
// if (isFormPrefilled && kodeLoaded && !initialCapturedRef.current) {
// // capture initial values once (so we can detect user interaction later)
// initialCapturedRef.current = {
// kd: getValues('kdObjPjk'),
// fg: getValues('fgFasilitas'),
// tarif: getValues('tarif'),
// noDok: getValues('noDokLainnya'),
// };
// // mark prefill done
// setIsPrefillDone(true);
// }
// }, [isFormPrefilled, kodeLoaded, getValues]);
// // 2) detect user interactions: if kd or fg changes from previous value -> mark interacted
// useEffect(() => {
// if (prevKdRef.current !== undefined && prevKdRef.current !== selectedKode) {
// hasUserInteractedRef.current = true;
// }
// prevKdRef.current = selectedKode;
// }, [selectedKode]);
// useEffect(() => {
// if (prevFgRef.current !== undefined && prevFgRef.current !== fgFasilitas) {
// hasUserInteractedRef.current = true;
// }
// prevFgRef.current = fgFasilitas;
// }, [fgFasilitas]);
// // also mark user interaction if tarif or noDok changes (user typed)
// useEffect(() => {
// if (!initialCapturedRef.current) return;
// const initial = initialCapturedRef.current;
// const curTarif = getValues('tarif');
// const curNoDok = getValues('noDokLainnya');
// // if user changed tarif or nodok from captured initial -> interaction
// if (
// (initial.tarif !== undefined && String(initial.tarif) !== String(curTarif)) ||
// (initial.noDok !== undefined && String(initial.noDok) !== String(curNoDok))
// ) {
// hasUserInteractedRef.current = true;
// }
// }, [tarifWatched, getValues]);
// // === Perhitungan otomatis pph dipotong (selalu aktif)
// useEffect(() => {
// const fg = getValues('fgFasilitas');
// const bruto = Number(getValues('jmlBruto') || 0);
// const tarif = Number(getValues('tarif') || 0);
// const pph = !fg ? 0 : TARIF_0.includes(fg) ? 0 : (bruto * tarif) / 100;
// const currentPph = Number(getValues('pphDipotong') || 0);
// if (currentPph !== pph) {
// setValue('pphDipotong', pph, { shouldValidate: true });
// }
// // dependencies: we rely on watch for reactive updates
// }, [jmlBruto, tarifWatched, fgFasilitas, getValues, setValue]);
// // ensure initial PPh is set as soon as prefill is done
// useEffect(() => {
// if (!isPrefillDone) return;
// const fg = getValues('fgFasilitas');
// const bruto = Number(getValues('jmlBruto') || 0);
// const tarif = Number(getValues('tarif') || 0);
// const initialPph = !fg ? 0 : TARIF_0.includes(fg) ? 0 : (bruto * tarif) / 100;
// setValue('pphDipotong', initialPph, { shouldValidate: true });
// }, [isPrefillDone, getValues, setValue]);
// // === Update tarif ketika kode objek pajak berubah
// useEffect(() => {
// if (!isPrefillDone) return;
// if (!selectedKode || !kodeObjekPajakSelected) return;
// const kodeTarif = Number(kodeObjekPajakSelected.tarif) || 0;
// const currentTarif = getValues('tarif');
// // If form is edit/prefilled and user hasn't interacted -> DO NOT override initial values
// // BUT if user changed kdObjPjk (detected via prevKdRef) -> we should apply kode tariff
// const prevKd = prevKdRef.current;
// const kdChangedByUser = prevKd !== undefined && prevKd !== selectedKode;
// if (isFormPrefilled && !hasUserInteractedRef.current && !kdChangedByUser) {
// // keep initial prefilling as-is
// return;
// }
// // otherwise (create mode OR user changed kd) -> apply kodeTarif
// if (String(currentTarif) !== String(kodeTarif)) {
// setValue('tarif', kodeTarif, { shouldValidate: true });
// }
// }, [selectedKode, kodeObjekPajakSelected, isPrefillDone, isFormPrefilled, getValues, setValue]);
// // === Reaksi terhadap perubahan fasilitas
// useEffect(() => {
// if (!isPrefillDone) return;
// const currentTarif = getValues('tarif');
// const currentNoDok = getValues('noDokLainnya');
// const kodeTarif = Number(kodeObjekPajakSelected?.tarif) || 0;
// // If edit & user hasn't interacted and fg didn't change by user -> keep prefills
// const prevFg = prevFgRef.current;
// const fgChangedByUser = prevFg !== undefined && prevFg !== fgFasilitas;
// if (isFormPrefilled && !hasUserInteractedRef.current && !fgChangedByUser) {
// return;
// }
// if (fgFasilitas === FG_FASILITAS_DN.FASILITAS_LAINNYA) {
// // "lainnya" -> tarif should be cleared/0 to allow manual input
// setValue('tarif', 0, { shouldValidate: true });
// setValue('noDokLainnya', '', { shouldValidate: true });
// return;
// }
// // non-"lainnya" -> set tariff from KOP and clear noDokLainnya
// if (String(currentTarif) !== String(kodeTarif)) {
// setValue('tarif', kodeTarif, { shouldValidate: true });
// }
// if (currentNoDok !== '') {
// setValue('noDokLainnya', '', { shouldValidate: true });
// }
// }, [fgFasilitas, kodeObjekPajakSelected, isPrefillDone, isFormPrefilled, getValues, setValue]);
// // === Filter opsi fasilitas
// const fasilitasOptions = useMemo(() => {
// if (!kodeObjekPajakSelected) return [];
// return Object.values(FG_FASILITAS_DN)
// .map((v) => ({ value: v, label: FG_FASILITAS_TEXT[v] }))
// .filter(
// (opt) =>
// kodeObjekPajakSelected[FG_FASILITAS_MASTER_KEY[opt.value] as keyof TGetListDataKOPDn] ===
// 1
// );
// }, [kodeObjekPajakSelected]);
// return (
// <Grid container rowSpacing={2} columnSpacing={2}>
// <Grid sx={{ mt: 3 }} size={{ md: 6 }}>
// <Field.Select name="kdObjPjk" label="Kode Objek Pajak">
// {kodeObjectPajak.map((item) => (
// <MenuItem key={item.kode} value={item.kode}>
// {`(${item.kode}) ${item.nama}`}
// </MenuItem>
// ))}
// </Field.Select>
// </Grid>
// <Grid size={{ md: 12 }}>
// <Divider sx={{ fontWeight: 'bold' }} textAlign="left">
// Fasilitas Pajak Penghasilan
// </Divider>
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Select name="fgFasilitas" label="Fasilitas">
// {fasilitasOptions.length === 0 ? (
// <MenuItem disabled value="">
// No options
// </MenuItem>
// ) : (
// fasilitasOptions.map((opt) => (
// <MenuItem key={opt.value} value={opt.value}>
// {opt.label}
// </MenuItem>
// ))
// )}
// </Field.Select>
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Text
// name="noDokLainnya"
// label="Nomor Dokumen Lainnya"
// disabled={['9', ''].includes(fgFasilitas)}
// sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
// />
// </Grid>
// <Grid size={{ md: 4 }}>
// <RHFNumeric
// name="jmlBruto"
// label="Jumlah Penghasilan Bruto (Rp)"
// allowNegativeValue={false}
// allowDecimalValue={false}
// />
// </Grid>
// <Grid size={{ md: 4 }}>
// <RHFNumeric
// name="tarif"
// label="Tarif (%)"
// allowDecimalValue
// maxValue={100}
// readOnly={fgFasilitas !== FG_FASILITAS_DN.FASILITAS_LAINNYA}
// disabled={fgFasilitas !== FG_FASILITAS_DN.FASILITAS_LAINNYA}
// />
// </Grid>
// <Grid size={{ md: 4 }}>
// <RHFNumeric
// name="pphDipotong"
// label="PPh Yang Dipotong/Dipungut"
// allowNegativeValue={false}
// allowDecimalValue={false}
// readOnly
// />
// </Grid>
// </Grid>
// );
// };
// export default PphDipotong;
import
Divider
from
'
@mui/material/Divider
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
import
React
,
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
;
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
type
{
TGetListDataKOPDn
}
from
'
../../types/types
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
...
...
@@ -378,6 +110,7 @@ const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongPr
if
(
currentPph
!==
pph
)
{
setValueString
(
'
pphDipotong
'
,
pph
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
jmlBruto
,
tarifWatched
,
fgFasilitas
,
getValues
,
setValue
]);
// === Jalankan perhitungan awal setelah prefill selesai
...
...
@@ -388,6 +121,7 @@ const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongPr
const
tarif
=
Number
(
getValues
(
'
tarif
'
)
||
0
);
const
initialPph
=
!
fg
?
0
:
TARIF_0
.
includes
(
fg
)
?
0
:
(
bruto
*
tarif
)
/
100
;
setValueString
(
'
pphDipotong
'
,
initialPph
);
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
isPrefillDone
,
getValues
]);
// === Update tarif ketika kode objek pajak berubah
...
...
@@ -408,6 +142,7 @@ const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongPr
if
(
String
(
currentTarif
)
!==
String
(
kodeTarif
))
{
setValueString
(
'
tarif
'
,
kodeTarif
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
selectedKode
,
kodeObjekPajakSelected
,
isPrefillDone
,
isFormPrefilled
,
getValues
]);
// === Reaksi terhadap perubahan fasilitas
...
...
@@ -437,6 +172,7 @@ const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongPr
if
(
currentNoDok
!==
''
)
{
setValue
(
'
noDokLainnya
'
,
''
,
{
shouldValidate
:
true
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
fgFasilitas
,
kodeObjekPajakSelected
,
isPrefillDone
,
isFormPrefilled
,
getValues
]);
// === Filter opsi fasilitas
...
...
src/sections/bupot-unifikasi/bupot-dn/hooks/useAdvancedFilterDn.tsx
View file @
d89056f3
// type FilterItem = {
// field: string;
// operator: string;
// value?: string | number | Array<string | number> | null;
// join?: 'AND' | 'OR'; // optional: join connector BEFORE this item (first item usually undefined)
// };
// type BaseParams = Record<string, any>;
// export function useAdvancedFilter() {
// const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
// const dateFields = new Set(['created_at', 'updated_at']);
// const fieldMap: Record<string, string> = {
// noBupot: 'nomorBupot',
// };
// const dbField = (field: string) => fieldMap[field] ?? field;
// const escape = (v: string) => String(v).replace(/'/g, "''");
// const toDbDate = (value: string | Date) => {
// if (value instanceof Date) {
// const y = value.getFullYear();
// const m = String(value.getMonth() + 1).padStart(2, '0');
// const d = String(value.getDate()).padStart(2, '0');
// return `${y}${m}${d}`;
// }
// const digits = String(value).replace(/[^0-9]/g, '');
// if (digits.length >= 8) return digits.slice(0, 8);
// return digits;
// };
// const normalizeOp = (op: string) => op?.toString().trim();
// function buildAdvancedFilter(filters?: FilterItem[] | null) {
// if (!filters || filters.length === 0) return '';
// const exprs: string[] = []; // each item's expression
// const joins: ('AND' | 'OR')[] = []; // join before each expr (for item 0, push nothing/AND by default)
// for (let i = 0; i < filters.length; i++) {
// const f = filters[i];
// if (!f || !f.field) continue;
// const op = normalizeOp(f.operator ?? '');
// const fieldName = dbField(f.field);
// // build expression for this item
// let expr: string | null = null;
// // DATE handling
// if (dateFields.has(fieldName)) {
// const rawVal = f.value;
// if (!rawVal && !/is empty|is not empty/i.test(op)) {
// continue;
// }
// if (/^is$/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd} 00:00:00' AND \"${fieldName}\" <= '${ymd} 23:59:59'`;
// } else if (/is on or after/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd}'`;
// } else if (/is on or before/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" <= '${ymd}'`;
// }
// }
// // EMPTY checks (user requested LOWER("col") IS NULL semantics)
// if (/is empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NULL`;
// } else if (/is not empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NOT NULL`;
// }
// // IS ANY OF handling
// if (!expr && /is any of/i.test(op)) {
// // collect values array
// let values: Array<string | number> = [];
// if (Array.isArray(f.value)) values = f.value as any;
// else if (typeof f.value === 'string')
// values = (f.value as string)
// .split(',')
// .map((s) => s.trim())
// .filter(Boolean);
// else if (f.value != null) values = [f.value as any];
// if ((values || []).length === 0) {
// expr = null;
// } else {
// // special-case fgStatus: need LIKE %val% OR LIKE %val2%
// if (fieldName === 'fgStatus' || fieldName === 'fg_status') {
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") LIKE LOWER('%${s}%')`;
// });
// expr = `(${ors.join(' OR ')})`;
// } else {
// // default: OR of equality (case-insensitive)
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") = '${s}'`;
// });
// expr = `(${ors.join(' OR ')})`;
// }
// }
// }
// // FGSTATUS special single-value is / is not / contains semantics
// if (!expr && (fieldName === 'fgStatus' || fieldName === 'fg_status')) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '' && !/is any of|is empty|is not empty/i.test(op)) {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// if (/^is$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/is not/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") NOT LIKE LOWER('%${valEscaped}%')`;
// }
// }
// }
// // GENERIC text/numeric handling when expr still not set
// if (!expr) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '') {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// // numeric fields: operators (=, >=, <=)
// if (numericFields.has(fieldName) && /^(=|>=|<=)$/.test(op)) {
// expr = `\"${fieldName}\" ${op} '${valEscaped}'`;
// } else if (/^contains$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/^equals$/i.test(op)) {
// // equals should produce IN (wrap single value as IN)
// // attempt to parse CSV if provided
// let values: string[] = [];
// if (Array.isArray(f.value))
// values = (f.value as any[]).map((v) => escape(String(v).toLowerCase()));
// else values = [escape(String(f.value).toLowerCase())];
// expr = `LOWER(\"${fieldName}\") IN (${values.map((v) => `'${v}'`).join(',')})`;
// } else if (/^(>=|<=|=)$/.test(op) && !numericFields.has(fieldName)) {
// expr = `LOWER(\"${fieldName}\") ${op} '${valEscaped}'`;
// } else if (/^(is)$/i.test(op)) {
// // fallback: treat as equals
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// } else {
// // fallback equality
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// }
// }
// }
// if (expr) {
// exprs.push(expr);
// // record join for this item (use provided join or default AND except for first item)
// const joinBefore = (f.join as 'AND' | 'OR') ?? (exprs.length > 1 ? 'AND' : 'AND');
// joins.push(joinBefore);
// }
// }
// // now combine exprs with joins; joins[i] is join BEFORE exprs[i]
// if (exprs.length === 0) return '';
// let out = exprs[0];
// for (let i = 1; i < exprs.length; i++) {
// const j = joins[i] ?? 'AND';
// out = `(${out}) ${j} (${exprs[i]})`;
// }
// return out;
// }
// function buildRequestParams(base: BaseParams = {}, advanced: string) {
// const out: BaseParams = { ...(base ?? {}) };
// if ('noBupot' in out) {
// out.nomorBupot = out.noBupot;
// delete out.noBupot;
// }
// out.advanced = advanced || '';
// return out;
// }
// return { buildAdvancedFilter, buildRequestParams } as const;
// }
// export default useAdvancedFilter;
type
FilterItem
=
{
field
:
string
;
operator
:
string
;
...
...
src/sections/bupot-unifikasi/bupot-dn/hooks/useGetDn.tsx
View file @
d89056f3
...
...
@@ -92,24 +92,6 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
updated_at
:
formatDateToDDMMYYYY
(
params
.
updated_at
),
});
// ---------- normalizer for params request ----------
// const normalisPropsParmasGetDn = (params: any) => {
// const sorting = !isEmpty(params.sortModel)
// ? transformSortModelToSortApiPayload(params.sortModel)
// : {};
// return {
// ...params,
// page: (typeof params.page === 'number' ? params.page : 0) + 1,
// limit: params.pageSize,
// masaPajak: params.msPajak || null,
// tahunPajak: params.thnPajak || null,
// npwp: params.idDipotong || null,
// advanced: isEmpty(params.advanced) ? undefined : params.advanced,
// ...sorting,
// };
// };
const
normalizeParams
=
(
params
:
any
)
=>
{
const
{
page
=
0
,
...
...
src/sections/bupot-unifikasi/bupot-dn/view/dnRekamView.tsx
View file @
d89056f3
...
...
@@ -24,36 +24,50 @@ import useUpload from '../hooks/useUpload';
import
{
useGetDnById
}
from
'
../hooks/useGetDn
'
;
import
ModalUploadDn
from
'
../components/dialog/ModalUploadDn
'
;
const
bpuSchema
=
z
.
object
({
tglPemotongan
:
z
.
string
().
nonempty
(
'
Tanggal Pemotongan harus diisi
'
),
thnPajak
:
z
.
string
().
nonempty
(
'
Tahun Pajak harus diisi
'
),
msPajak
:
z
.
string
().
nonempty
(
'
Masa Pajak harus diisi
'
),
idDipotong
:
z
.
string
()
.
nonempty
(
'
NPWP harus diisi
'
)
.
regex
(
/^
\d{16}
$/
,
'
NPWP harus 16 digit
'
),
nitku
:
z
.
string
()
.
nonempty
(
'
NITKU harus diisi
'
)
.
regex
(
/^
\d{22}
$/
,
'
NITKU harus 22 digit
'
),
namaDipotong
:
z
.
string
().
nonempty
(
'
Nama harus diisi
'
),
email
:
z
.
string
().
email
({
message
:
'
Email tidak valid
'
}).
optional
().
or
(
z
.
literal
(
''
)),
keterangan1
:
z
.
string
().
optional
(),
keterangan2
:
z
.
string
().
optional
(),
keterangan3
:
z
.
string
().
optional
(),
keterangan4
:
z
.
string
().
optional
(),
keterangan5
:
z
.
string
().
optional
(),
kdObjPjk
:
z
.
string
().
nonempty
(
'
Kode Objek Pajak harus diisi
'
),
fgFasilitas
:
z
.
string
().
nonempty
(
'
Fasilitas harus diisi
'
),
noDokLainnya
:
z
.
string
().
nonempty
(
'
No Dokumen Lainnya harus diisi
'
),
jmlBruto
:
z
.
string
().
nonempty
(
'
Jumlah Penghasilan Bruto harus diisi
'
),
tarif
:
z
.
union
([
z
.
string
().
nonempty
(
'
Tarif harus diisi
'
),
z
.
number
()]),
pphDipotong
:
z
.
string
().
nonempty
(
'
PPh Yang Dipotong/Dipungut harus diisi
'
),
namaDok
:
z
.
string
().
nonempty
(
'
Nama Dokumen harus diisi
'
),
nomorDok
:
z
.
string
().
nonempty
(
'
Nomor Dokumen harus diisi
'
),
tglDok
:
z
.
string
().
nonempty
(
'
Tanggal Dokumen harus diisi
'
),
idTku
:
z
.
string
().
nonempty
(
'
Cabang harus diisi
'
),
});
const
bpuSchema
=
z
.
object
({
tglPemotongan
:
z
.
string
().
nonempty
(
'
Tanggal Pemotongan harus diisi
'
),
thnPajak
:
z
.
string
().
nonempty
(
'
Tahun Pajak harus diisi
'
),
msPajak
:
z
.
string
().
nonempty
(
'
Masa Pajak harus diisi
'
),
idDipotong
:
z
.
string
()
.
nonempty
(
'
NPWP harus diisi
'
)
.
regex
(
/^
\d{16}
$/
,
'
NPWP harus 16 digit
'
),
nitku
:
z
.
string
()
.
nonempty
(
'
NITKU harus diisi
'
)
.
regex
(
/^
\d{22}
$/
,
'
NITKU harus 22 digit
'
),
namaDipotong
:
z
.
string
().
nonempty
(
'
Nama harus diisi
'
),
email
:
z
.
string
().
email
({
message
:
'
Email tidak valid
'
}).
optional
().
or
(
z
.
literal
(
''
)),
keterangan1
:
z
.
string
().
optional
(),
keterangan2
:
z
.
string
().
optional
(),
keterangan3
:
z
.
string
().
optional
(),
keterangan4
:
z
.
string
().
optional
(),
keterangan5
:
z
.
string
().
optional
(),
kdObjPjk
:
z
.
string
().
nonempty
(
'
Kode Objek Pajak harus diisi
'
),
fgFasilitas
:
z
.
string
().
nonempty
(
'
Fasilitas harus diisi
'
),
noDokLainnya
:
z
.
string
().
optional
(),
jmlBruto
:
z
.
string
().
nonempty
(
'
Jumlah Penghasilan Bruto harus diisi
'
),
tarif
:
z
.
union
([
z
.
string
().
nonempty
(
'
Tarif harus diisi
'
),
z
.
number
()]),
pphDipotong
:
z
.
string
().
nonempty
(
'
PPh Yang Dipotong/Dipungut harus diisi
'
),
namaDok
:
z
.
string
().
nonempty
(
'
Nama Dokumen harus diisi
'
),
nomorDok
:
z
.
string
().
nonempty
(
'
Nomor Dokumen harus diisi
'
),
tglDok
:
z
.
string
().
nonempty
(
'
Tanggal Dokumen harus diisi
'
),
idTku
:
z
.
string
().
nonempty
(
'
Cabang harus diisi
'
),
})
.
superRefine
((
data
,
ctx
)
=>
{
// Field dianggap DISABLED kalau fgFasilitas kosong ('') atau '9'
const
isDisabled
=
[
''
,
'
9
'
].
includes
(
data
.
fgFasilitas
);
// Jika tidak disabled, berarti aktif → wajib isi
if
(
!
isDisabled
&&
(
!
data
.
noDokLainnya
||
data
.
noDokLainnya
.
trim
()
===
''
))
{
ctx
.
addIssue
({
path
:
[
'
noDokLainnya
'
],
code
:
'
custom
'
,
message
:
'
No Dokumen Lainnya harus diisi
'
,
});
}
});
const
DnRekamView
=
()
=>
{
const
{
id
,
type
}
=
useParams
<
{
id
?:
string
;
type
?:
'
ubah
'
|
'
pengganti
'
|
'
new
'
}
>
();
...
...
src/sections/bupot-unifikasi/bupot-nr/components/CustomColumnsButton.tsx
View file @
d89056f3
import
React
from
'
react
'
;
import
{
GridPreferencePanelsValue
,
useGridApiContext
}
from
'
@mui/x-data-grid-premium
'
;
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
'
;
...
...
src/sections/bupot-unifikasi/bupot-nr/components/CustomFilterButton.tsx
View file @
d89056f3
// import React from 'react';
// import {
// IconButton,
// Popover,
// Stack,
// TextField,
// MenuItem,
// Button,
// Badge,
// Divider,
// IconButton as MuiIconButton,
// Autocomplete,
// Chip,
// } from '@mui/material';
// import { FilterList as FilterListIcon, Close as CloseIcon } from '@mui/icons-material';
// import { GridFilterModel, GridColDef, GridFilterItem } from '@mui/x-data-grid-premium';
// interface StatusOption {
// value: string;
// label: string;
// }
// interface Props {
// columns: GridColDef[];
// filterModel: GridFilterModel;
// setFilterModel: (model: GridFilterModel) => void;
// statusOptions?: StatusOption[]; // for fgStatus label lookup & options
// debounceMs?: number;
// }
// // helper operator lists
// const TEXT_OPS = [
// { value: 'contains', label: 'contains' },
// { value: 'equals', label: 'equals' },
// { value: 'is empty', label: 'is empty' },
// { value: 'is not empty', label: 'is not empty' },
// { value: 'is any of', label: 'is any of' },
// ];
// const NUMERIC_OPS = [
// { value: '=', label: '=' },
// { value: '>=', label: '>=' },
// { value: '<=', label: '<=' },
// ];
// const STATUS_OPS = [
// { value: 'is', label: 'is' },
// { value: 'is not', label: 'is not' },
// { value: 'is any of', label: 'is any of' },
// ];
// const DATE_OPS = [
// { value: 'is', label: 'is' },
// { value: 'is on or after', label: 'is on or after' },
// { value: 'is on or before', label: 'is on or before' },
// ];
// const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
// const dateFields = new Set(['created_at', 'updated_at']);
// function getOperatorOptionsForField(field?: string) {
// if (!field) return TEXT_OPS;
// if (field === 'fgStatus') return STATUS_OPS;
// if (numericFields.has(field)) return NUMERIC_OPS;
// if (dateFields.has(field)) return DATE_OPS;
// // default text-like
// return TEXT_OPS;
// }
// function defaultOperatorForField(field?: string) {
// if (!field) return 'contains';
// if (field === 'fgStatus') return 'is';
// if (numericFields.has(field)) return '=';
// if (dateFields.has(field)) return 'is';
// return 'contains';
// }
// // Memoized component
// export const CustomFilterButton: React.FC<Props> = React.memo(
// ({ columns, filterModel, setFilterModel, statusOptions = [], debounceMs = 400 }) => {
// const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
// // Local copy for editing inside popover (do not flush to parent on open)
// const [localItems, setLocalItems] = React.useState<GridFilterModel['items']>(
// () => filterModel?.items ?? []
// );
// // Keep local items in sync when parent filterModel changes externally
// React.useEffect(() => {
// setLocalItems(filterModel?.items ?? []);
// }, [filterModel?.items]);
// const debounceRef = React.useRef<number | null>(null);
// const clearDebounce = React.useCallback(() => {
// if (debounceRef.current !== null) {
// window.clearTimeout(debounceRef.current);
// debounceRef.current = null;
// }
// }, []);
// const applyImmediate = React.useCallback(
// (items: GridFilterModel['items']) => {
// clearDebounce();
// setFilterModel({ items });
// },
// [clearDebounce, setFilterModel]
// );
// const applyDebounced = React.useCallback(
// (items: GridFilterModel['items']) => {
// clearDebounce();
// debounceRef.current = window.setTimeout(() => {
// setFilterModel({ items });
// debounceRef.current = null;
// }, debounceMs) as unknown as number;
// },
// [clearDebounce, debounceMs, setFilterModel]
// );
// React.useEffect(() => clearDebounce, [clearDebounce]);
// // active filters count badge
// const activeCount =
// Number(
// (filterModel?.items || []).filter((i) => {
// if (!i || !i.field) return false;
// // count only if operator does not require no-value OR (value present) OR operator is 'is any of' with some values
// const op = String(i.operator ?? '').toLowerCase();
// if (op === 'is empty' || op === 'is not empty') return true;
// if (op === 'is any of')
// return Array.isArray(i.value) ? (i.value as any[]).length > 0 : !!i.value;
// return i.value !== undefined && i.value !== '';
// }).length
// ) || 0;
// const getCol = React.useCallback(
// (field?: string) => columns.find((c) => c.field === field),
// [columns]
// );
// // stable ref for the button so anchor stays valid across parent re-renders
// const buttonRef = React.useRef<HTMLButtonElement | null>(null);
// // OPEN: do NOT update parent state here (to avoid parent re-render before popover opens)
// const handleClick = (e: React.MouseEvent<HTMLElement>) => {
// e.stopPropagation();
// if (!filterModel?.items?.length) {
// const init = [
// {
// field: columns[0]?.field ?? '',
// operator: defaultOperatorForField(columns[0]?.field),
// value: '',
// },
// ];
// setLocalItems(init);
// } else {
// setLocalItems(filterModel.items ?? []);
// }
// setAnchorEl(buttonRef.current);
// };
// const handleClose = () => {
// setAnchorEl(null);
// };
// // add/remove operate only on localItems now (no immediate parent update)
// const addFilter = React.useCallback(() => {
// setLocalItems((prev) => {
// const next = [
// ...(prev || []),
// {
// field: columns[0]?.field ?? '',
// operator: defaultOperatorForField(columns[0]?.field),
// value: '',
// },
// ];
// return next;
// });
// }, [columns]);
// const removeFilter = React.useCallback((index: number) => {
// setLocalItems((prev) => (prev || []).filter((_, i) => i !== index));
// }, []);
// const handleClearAll = React.useCallback(() => {
// clearDebounce();
// setLocalItems([]);
// setFilterModel({ items: [] });
// }, [clearDebounce, setFilterModel]);
// // updateFilter: when user changes field/operator/value inside popover
// const updateFilter = React.useCallback(
// (index: number, key: keyof GridFilterModel['items'][0], value: any) => {
// setLocalItems((prev) => {
// const next = Array.isArray(prev) ? [...prev] : [];
// const old = next[index] ?? {
// field: columns[0]?.field ?? '',
// operator: defaultOperatorForField(columns[0]?.field),
// value: '',
// };
// let updated = { ...old, [key]: value };
// // if field changed -> reset operator/value sensibly
// if (key === 'field') {
// const newField = value as string;
// const defOp = defaultOperatorForField(newField) as any;
// updated = { ...updated, field: newField, operator: defOp, value: '' };
// next[index] = updated;
// return next;
// }
// // if operator changed -> reset value if operator is an empty-kind or multi-kind
// if (key === 'operator') {
// const op = String(value).toLowerCase();
// if (op === 'is empty' || op === 'is not empty') {
// updated = { ...updated, operator: value, value: '' };
// next[index] = updated;
// // persist operator-only change? we'll debounce on value changes; operator change itself doesn't require immediate apply
// return next;
// }
// if (op === 'is any of') {
// // set value to array if not already
// if (!Array.isArray(updated.value)) updated.value = [];
// updated = { ...updated, operator: value };
// next[index] = updated;
// return next;
// }
// // other operators: keep value but ensure type (string)
// updated = { ...updated, operator: value, value: String(updated.value ?? '') };
// next[index] = updated;
// return next;
// }
// // if value changed -> decide immediate vs debounce depending on column type
// next[index] = updated;
// const items = next;
// const col = getCol(items[index].field as string);
// // detect singleSelect
// const isSingleSelect =
// !!col && (col.type === 'singleSelect' || Array.isArray((col as any).valueOptions));
// if (isSingleSelect) {
// applyImmediate(items);
// } else {
// applyDebounced(items);
// }
// return items;
// });
// },
// [applyDebounced, applyImmediate, columns, getCol]
// );
// // APPLY button: persist localItems to parent and close
// const handleApply = React.useCallback(() => {
// clearDebounce();
// setFilterModel({ items: localItems || [] });
// setAnchorEl(null);
// }, [localItems, clearDebounce, setFilterModel]);
// return (
// <>
// <Badge badgeContent={activeCount} color="primary" overlap="circular">
// <IconButton
// size="small"
// ref={buttonRef}
// onMouseDown={(e) => e.stopPropagation()}
// onClick={handleClick}
// sx={{ color: '#123375' }}
// aria-label="filter"
// >
// <FilterListIcon />
// </IconButton>
// </Badge>
// <Popover
// open={Boolean(anchorEl)}
// anchorEl={anchorEl}
// onClose={handleClose}
// keepMounted
// anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
// transformOrigin={{ vertical: 'top', horizontal: 'right' }}
// PaperProps={{ sx: { p: 2, minWidth: 540, maxWidth: '95vw' } }}
// >
// <Stack gap={2}>
// {(localItems || []).map((item, idx) => {
// const colDef = getCol(item.field as string);
// const operatorOptions = getOperatorOptionsForField(item.field);
// const op = String(item.operator ?? defaultOperatorForField(item.field));
// // value options for select fields
// const valueOptions: string[] =
// (colDef && (colDef as any).valueOptions) ||
// (colDef && colDef.type === 'singleSelect'
// ? ((colDef as any).valueOptions ?? [])
// : []);
// // if fgStatus, source options from statusOptions prop
// const statusVals = statusOptions.map((s) => s.value);
// const isNoValueOp = op === 'is empty' || op === 'is not empty';
// const isAnyOfOp = op === 'is any of';
// const isDateField = dateFields.has(item.field as string);
// const isNumericField = numericFields.has(item.field as string);
// const isStatusField = item.field === 'fgStatus';
// return (
// <Stack
// key={idx}
// direction="row"
// spacing={1}
// alignItems="center"
// sx={{ width: '100%' }}
// >
// {/* Column */}
// <TextField
// select
// size="small"
// label="Column"
// value={item.field ?? ''}
// onChange={(e) => updateFilter(idx, 'field', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ minWidth: 180 }}
// >
// {columns.map((col) => (
// <MenuItem key={String(col.field)} value={col.field}>
// {col.headerName ?? String(col.field)}
// </MenuItem>
// ))}
// </TextField>
// {/* Operator */}
// <TextField
// select
// size="small"
// label="Operator"
// value={op}
// onChange={(e) => updateFilter(idx, 'operator', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ minWidth: 160 }}
// >
// {operatorOptions.map((o) => (
// <MenuItem key={o.value} value={o.value}>
// {o.label}
// </MenuItem>
// ))}
// </TextField>
// {/* Value */}
// {isNoValueOp ? (
// // show disabled placeholder when operator does not require a value
// <TextField
// size="small"
// label="Value"
// placeholder="No value required"
// value=""
// disabled
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// />
// ) : isAnyOfOp ? (
// // multi-value input (free text or from options)
// <Autocomplete
// multiple
// freeSolo
// size="small"
// options={
// isStatusField ? statusVals : ((valueOptions as string[] | undefined) ?? [])
// }
// value={
// Array.isArray(item.value) ? item.value : item.value ? [item.value] : []
// }
// onChange={(_, values) => {
// updateFilter(idx, 'value', values);
// }}
// renderTags={(value: any[], getTagProps) =>
// value.map((option: any, index: number) => (
// <Chip
// variant="outlined"
// label={String(option)}
// {...getTagProps({ index })}
// />
// ))
// }
// renderInput={(params) => (
// <TextField
// {...params}
// label="Value(s)"
// placeholder="Select values..."
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// />
// )}
// sx={{ flex: 1, minWidth: 160 }}
// />
// ) : isDateField ? (
// // date input (yyyy-mm-dd)
// <TextField
// size="small"
// label="Value"
// type="date"
// value={item.value ? String(item.value).slice(0, 10) : ''}
// onChange={(e) => updateFilter(idx, 'value', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// />
// ) : colDef &&
// (colDef.type === 'singleSelect' ||
// Array.isArray((colDef as any).valueOptions)) ? (
// // single select (enum)
// <TextField
// select
// size="small"
// label="Value"
// value={item.value ?? ''}
// onChange={(e) => updateFilter(idx, 'value', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// >
// {((colDef as any).valueOptions ?? (isStatusField ? statusVals : [])).map(
// (opt: any) => (
// <MenuItem key={String(opt)} value={opt}>
// {isStatusField
// ? (statusOptions.find((s) => s.value === opt)?.label ?? String(opt))
// : String(opt)}
// </MenuItem>
// )
// )}
// </TextField>
// ) : (
// // normal text / numeric input
// <TextField
// size="small"
// label="Value"
// placeholder="Filter Value"
// value={item.value ?? ''}
// onChange={(e) => {
// const v = e.target.value;
// if (isNumericField) {
// // allow only digits
// const digits = v.replace(/[^0-9]/g, '');
// updateFilter(idx, 'value', digits);
// } else {
// updateFilter(idx, 'value', v);
// }
// }}
// InputLabelProps={{ shrink: true }}
// inputProps={isNumericField ? { inputMode: 'numeric', pattern: '[0-9]*' } : {}}
// sx={{ flex: 1, minWidth: 160 }}
// />
// )}
// <MuiIconButton
// size="small"
// onClick={(e) => {
// e.stopPropagation();
// removeFilter(idx);
// }}
// >
// <CloseIcon fontSize="small" />
// </MuiIconButton>
// </Stack>
// );
// })}
// <Divider />
// <Stack direction="row" justifyContent="space-between" alignItems="center">
// <Button
// size="small"
// variant="outlined"
// onClick={(e) => {
// e.stopPropagation();
// addFilter();
// }}
// >
// + Add filter
// </Button>
// <Stack direction="row" spacing={1}>
// <Button
// size="small"
// onClick={(e) => {
// e.stopPropagation();
// handleClearAll();
// }}
// >
// Clear
// </Button>
// <Button
// size="small"
// variant="contained"
// onClick={(e) => {
// e.stopPropagation();
// handleApply();
// }}
// >
// Apply
// </Button>
// </Stack>
// </Stack>
// </Stack>
// </Popover>
// </>
// );
// }
// );
// export default CustomFilterButton;
import
React
from
'
react
'
;
import
{
IconButton
,
...
...
@@ -519,7 +13,7 @@ import {
Chip
,
}
from
'
@mui/material
'
;
import
{
FilterList
as
FilterListIcon
,
Close
as
CloseIcon
}
from
'
@mui/icons-material
'
;
import
{
GridFilterModel
,
GridColDef
}
from
'
@mui/x-data-grid-premium
'
;
import
type
{
GridFilterModel
,
GridColDef
}
from
'
@mui/x-data-grid-premium
'
;
interface
StatusOption
{
value
:
string
;
...
...
src/sections/bupot-unifikasi/bupot-nr/components/CustomToolbar.tsx
View file @
d89056f3
import
*
as
React
from
'
react
'
;
import
{
GridToolbarContainer
,
GridToolbarProps
}
from
'
@mui/x-data-grid-premium
'
;
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
{
ActionItem
}
from
'
../types/types
'
;
import
type
{
ActionItem
}
from
'
../types/types
'
;
import
{
CustomFilterButton
}
from
'
./CustomFilterButton
'
;
import
CustomColumnsButton
from
'
./CustomColumnsButton
'
;
...
...
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalCancel
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalCancel
Nr
.tsx
View file @
d89056f3
// import React, { useMemo, useState } from 'react';
// import { Stack, Button, Typography } from '@mui/material';
// import { useQueryClient } from '@tanstack/react-query';
// import { enqueueSnackbar } from 'notistack';
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
// import dayjs, { Dayjs } from 'dayjs';
// import minMax from 'dayjs/plugin/minMax';
// import DialogUmum from 'src/shared/components/dialog/DialogUmum';
// import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
// import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
// import useCancelDn from '../../hooks/useCancelDn';
// import { GridRowSelectionModel } from '@mui/x-data-grid-premium';
// dayjs.extend(minMax);
// // Helper format tanggal ke format API (DDMMYYYY)
// const formatDateDDMMYYYY = (d: Date) => {
// const dd = String(d.getDate()).padStart(2, '0');
// const mm = String(d.getMonth() + 1).padStart(2, '0');
// const yyyy = d.getFullYear();
// return `${dd}${mm}${yyyy}`;
// };
// interface ModalCancelDnProps {
// dataSelected?: any[]; // ✅ array of full row data (dari dataSelectedRef.current)
// setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
// tableApiRef?: React.MutableRefObject<any>;
// isOpenDialogCancel: boolean;
// setIsOpenDialogCancel: (v: boolean) => void;
// successMessage?: string;
// }
// const ModalCancelDn: React.FC<ModalCancelDnProps> = ({
// dataSelected = [],
// setSelectionModel,
// tableApiRef,
// isOpenDialogCancel,
// setIsOpenDialogCancel,
// successMessage = 'Data berhasil dibatalkan',
// }) => {
// const queryClient = useQueryClient();
// const [tglPembatalan, setTglPembatalan] = useState<Dayjs | null>(null);
// const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// const {
// numberOfData,
// numberOfDataFail,
// numberOfDataProcessed,
// numberOfDataSuccess,
// processSuccess,
// processFail,
// resetToDefault,
// status,
// } = useDialogProgressBar();
// const { mutateAsync } = useCancelDn({
// onSuccess: () => processSuccess(),
// onError: () => processFail(),
// });
// // ✅ Ambil tanggal pemotongan paling awal (minDate untuk DatePicker)
// const minPembatalanDate = useMemo(() => {
// if (!dataSelected.length) return null;
// const dates = dataSelected
// .map((d) => {
// const tgl = d.tglPemotongan || d.tglpemotongan;
// return tgl ? dayjs(tgl, ['YYYY-MM-DD', 'DD/MM/YYYY']) : null;
// })
// .filter((d): d is Dayjs => !!d && d.isValid());
// return dates.length > 0 ? dayjs.min(dates) : null;
// }, [dataSelected]);
// const handleCloseModal = () => {
// setIsOpenDialogCancel(false);
// resetToDefault();
// };
// const clearSelection = () => {
// tableApiRef?.current?.setRowSelectionModel?.([]);
// setSelectionModel?.(undefined);
// };
// const handleSubmit = async () => {
// if (!tglPembatalan) {
// enqueueSnackbar('Tanggal pembatalan harus diisi', { variant: 'warning' });
// return;
// }
// const formattedDate = formatDateDDMMYYYY(tglPembatalan.toDate());
// const ids = dataSelected.map((item) => String(item.id ?? item.internal_id));
// try {
// setIsOpenDialogProgressBar(true);
// const results = await Promise.allSettled(
// ids.map((id) => mutateAsync({ id, tglPembatalan: formattedDate }))
// );
// const rejected = results.filter((r) => r.status === 'rejected');
// const success = results.filter((r) => r.status === 'fulfilled');
// if (rejected.length > 0) {
// const errorMessages = rejected
// .map((r) => (r.status === 'rejected' ? r.reason?.message : ''))
// .filter(Boolean)
// .join('\n');
// enqueueSnackbar(
// <span style={{ whiteSpace: 'pre-line' }}>
// {errorMessages || `${rejected.length} dari ${ids.length} data gagal dibatalkan.`}
// </span>,
// { variant: 'error' }
// );
// processFail();
// } else {
// enqueueSnackbar(successMessage, { variant: 'success' });
// processSuccess();
// }
// // ✅ Langkah penting:
// // tunggu sampai React Query benar-benar refetch data baru
// await queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
// // ✅ lalu clear selection di DataGrid
// tableApiRef?.current?.setRowSelectionModel?.([]);
// setSelectionModel?.(undefined);
// handleCloseModal();
// } catch (error: any) {
// enqueueSnackbar(error?.message || 'Gagal membatalkan data', { variant: 'error' });
// } finally {
// setIsOpenDialogProgressBar(false);
// }
// };
// return (
// <>
// {/* ✅ Dialog reusable */}
// <DialogUmum
// isOpen={isOpenDialogCancel}
// onClose={handleCloseModal}
// title="Batal Bukti Pemotongan/Pemungutan PPh Unifikasi"
// >
// <Stack spacing={2}>
// <Typography>
// Silakan isi tanggal pembatalan. Tanggal tidak boleh sebelum tanggal pemotongan.
// </Typography>
// <DatePicker
// label="Tanggal Pembatalan"
// format="DD/MM/YYYY"
// value={tglPembatalan}
// maxDate={dayjs()} // tanggal maksimal = hari ini
// minDate={minPembatalanDate || undefined} // 🔹 minDate sesuai tglPemotongan
// onChange={(newValue) => setTglPembatalan(newValue)}
// slotProps={{
// textField: {
// size: 'medium',
// fullWidth: true,
// helperText:
// minPembatalanDate && `Tanggal minimal: ${minPembatalanDate.format('DD/MM/YYYY')}`,
// InputLabelProps: { shrink: true },
// sx: {
// '& .MuiOutlinedInput-root': {
// borderRadius: 1.5,
// backgroundColor: '#fff',
// '&:hover fieldset': {
// borderColor: '#123375 !important',
// },
// '&.Mui-focused fieldset': {
// borderColor: '#123375 !important',
// borderWidth: '1px',
// },
// },
// },
// },
// }}
// />
// <Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
// <Button variant="outlined" onClick={handleCloseModal}>
// Batal
// </Button>
// <Button
// variant="contained"
// color="error"
// onClick={handleSubmit}
// disabled={!tglPembatalan}
// >
// Batalkan
// </Button>
// </Stack>
// </Stack>
// </DialogUmum>
// {/* ✅ Dialog progress bar */}
// <DialogProgressBar
// isOpen={isOpenDialogProgressBar}
// handleClose={() => {
// handleCloseModal();
// setIsOpenDialogProgressBar(false);
// }}
// numberOfData={numberOfData}
// numberOfDataProcessed={numberOfDataProcessed}
// numberOfDataFail={numberOfDataFail}
// numberOfDataSuccess={numberOfDataSuccess}
// status={status}
// />
// </>
// );
// };
// export default ModalCancelDn;
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
'
react
'
;
import
{
Stack
,
Button
,
Typography
}
from
'
@mui/material
'
;
import
{
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
{
DatePicker
}
from
'
@mui/x-date-pickers/DatePicker
'
;
import
dayjs
,
{
Dayjs
}
from
'
dayjs
'
;
import
type
{
Dayjs
}
from
'
dayjs
'
;
import
dayjs
from
'
dayjs
'
;
import
minMax
from
'
dayjs/plugin/minMax
'
;
import
DialogUmum
from
'
src/shared/components/dialog/DialogUmum
'
;
import
DialogProgressBar
from
'
src/shared/components/dialog/DialogProgressBar
'
;
import
useDialogProgressBar
from
'
src/shared/hooks/useDialogProgressBar
'
;
import
useCancelDn
from
'
../../hooks/useCancel
Dn
'
;
import
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
useCancelDn
from
'
../../hooks/useCancel
Nr
'
;
import
type
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
dayjs
.
extend
(
minMax
);
...
...
@@ -247,7 +31,7 @@ interface ModalCancelDnProps {
successMessage
?:
string
;
}
const
ModalCancel
Dn
:
React
.
FC
<
ModalCancelDnProps
>
=
({
const
ModalCancel
Nr
:
React
.
FC
<
ModalCancelDnProps
>
=
({
dataSelected
=
[],
setSelectionModel
,
tableApiRef
,
...
...
@@ -441,4 +225,4 @@ const ModalCancelDn: React.FC<ModalCancelDnProps> = ({
);
};
export
default
ModalCancel
Dn
;
export
default
ModalCancel
Nr
;
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalCetakPdf
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalCetakPdf
Nr
.tsx
View file @
d89056f3
...
...
@@ -4,7 +4,7 @@ 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
useCetakPdfDn
from
'
../../hooks/useCetakPdf
Dn
'
;
import
useCetakPdfDn
from
'
../../hooks/useCetakPdf
Nr
'
;
import
normalizePayloadCetakPdf
from
'
../../utils/normalizePayloadCetakPdf
'
;
interface
ModalCetakPdfDnProps
{
...
...
@@ -13,18 +13,7 @@ interface ModalCetakPdfDnProps {
onClose
:
()
=>
void
;
}
const
formatTanggalIndo
=
(
isoDate
:
string
|
undefined
|
null
):
string
=>
{
if
(
!
isoDate
)
return
''
;
const
date
=
new
Date
(
isoDate
);
const
formatter
=
new
Intl
.
DateTimeFormat
(
'
id-ID
'
,
{
day
:
'
2-digit
'
,
month
:
'
long
'
,
year
:
'
numeric
'
,
});
return
formatter
.
format
(
date
);
};
const
ModalCetakPdfDn
:
React
.
FC
<
ModalCetakPdfDnProps
>
=
({
payload
,
isOpen
,
onClose
})
=>
{
const
ModalCetakPdfNr
:
React
.
FC
<
ModalCetakPdfDnProps
>
=
({
payload
,
isOpen
,
onClose
})
=>
{
const
[
pdfUrl
,
setPdfUrl
]
=
useState
<
string
|
null
>
(
null
);
const
[
loading
,
setLoading
]
=
useState
<
boolean
>
(
false
);
...
...
@@ -104,4 +93,4 @@ const ModalCetakPdfDn: React.FC<ModalCetakPdfDnProps> = ({ payload, isOpen, onCl
);
};
export
default
ModalCetakPdf
Dn
;
export
default
ModalCetakPdf
Nr
;
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalDelete
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalDelete
Nr
.tsx
View file @
d89056f3
import
React
,
{
useEffect
,
useState
}
from
'
react
'
;
import
{
use
Mutation
,
use
QueryClient
}
from
'
@tanstack/react-query
'
;
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
dnApi
from
'
../../utils/api
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
import
DialogConfirm
from
'
src/shared/components/dialog/DialogConfirm
'
;
import
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
useDeleteDn
from
'
../../hooks/useDelete
Dn
'
;
import
type
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
useDeleteDn
from
'
../../hooks/useDelete
Nr
'
;
interface
ModalDeleteDnProps
{
dataSelected
?:
GridRowSelectionModel
;
...
...
@@ -46,7 +44,7 @@ const normalizeSelection = (sel?: any): (string | number)[] => {
return
[];
};
const
ModalDelete
Dn
:
React
.
FC
<
ModalDeleteDnProps
>
=
({
const
ModalDelete
Nr
:
React
.
FC
<
ModalDeleteDnProps
>
=
({
dataSelected
,
setSelectionModel
,
tableApiRef
,
...
...
@@ -145,4 +143,4 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({
);
};
export
default
ModalDelete
Dn
;
export
default
ModalDelete
Nr
;
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalUpload
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/components/dialog/ModalUpload
Nr
.tsx
View file @
d89056f3
...
...
@@ -3,7 +3,7 @@ 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
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
type
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
useUpload
from
'
../../hooks/useUpload
'
;
import
DialogUmum
from
'
src/shared/components/dialog/DialogUmum
'
;
import
Stack
from
'
@mui/material/Stack
'
;
...
...
@@ -11,12 +11,12 @@ import Grid from '@mui/material/Grid';
import
{
Field
}
from
'
src/components/hook-form
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
import
{
useSelector
}
from
'
react-redux
'
;
import
{
RootState
}
from
'
src/store
'
;
import
type
{
RootState
}
from
'
src/store
'
;
import
Agreement
from
'
src/shared/components/agreement/Agreement
'
;
import
{
FormProvider
,
useForm
}
from
'
react-hook-form
'
;
import
{
LoadingButton
}
from
'
@mui/lab
'
;
interface
ModalUpload
Dn
Props
{
interface
ModalUpload
Nr
Props
{
dataSelected
?:
GridRowSelectionModel
;
setSelectionModel
?:
React
.
Dispatch
<
React
.
SetStateAction
<
GridRowSelectionModel
|
undefined
>>
;
tableApiRef
?:
React
.
MutableRefObject
<
any
>
;
...
...
@@ -55,7 +55,7 @@ const normalizeSelection = (sel?: any): (string | number)[] => {
return
[];
};
const
ModalUpload
Dn
:
React
.
FC
<
ModalUploadDn
Props
>
=
({
const
ModalUpload
Nr
:
React
.
FC
<
ModalUploadNr
Props
>
=
({
dataSelected
,
setSelectionModel
,
tableApiRef
,
...
...
@@ -66,7 +66,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
})
=>
{
const
queryClient
=
useQueryClient
();
const
upload
Dn
=
useUpload
();
const
upload
Nr
=
useUpload
();
// custom hooks for progress state
const
{
numberOfData
,
...
...
@@ -124,7 +124,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
enqueueSnackbar
(
error
?.
message
||
'
Gagal upload data
'
,
{
variant
:
'
error
'
});
}
finally
{
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient
.
invalidateQueries
({
queryKey
:
[
'
unifikasi
'
,
'
dn
'
]
});
queryClient
.
invalidateQueries
({
queryKey
:
[
'
unifikasi
'
,
'
nr
'
]
});
}
};
...
...
@@ -158,17 +158,17 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
<
LoadingButton
type=
"button"
disabled=
{
!
isCheckedAgreement
}
// onClick={onSubmit}
onClick=
{
async
()
=>
{
if
(
onConfirmUpload
)
{
await
onConfirmUpload
();
setIsOpenDialogUpload
(
false
);
return
;
}
await
onSubmit
();
}
}
loading=
{
upload
Dn
.
isPending
}
loading=
{
upload
Nr
.
isPending
}
variant=
"contained"
sx=
{
{
background
:
'
#143B88
'
}
}
>
...
...
@@ -195,4 +195,4 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
);
};
export
default
ModalUpload
Dn
;
export
default
ModalUpload
Nr
;
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/DokumenReferensi.tsx
0 → 100644
View file @
d89056f3
import
Divider
from
'
@mui/material/Divider
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
import
{
useEffect
}
from
'
react
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
{
JENIS_DOKUMEN
}
from
'
../../constant
'
;
import
dayjs
from
'
dayjs
'
;
import
{
useSelector
}
from
'
react-redux
'
;
import
type
{
RootState
}
from
'
src/store
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
const
DokumenReferensi
=
()
=>
{
const
{
watch
,
setValue
}
=
useFormContext
<
Record
<
string
,
any
>>
();
const
nitku
=
useSelector
((
state
:
RootState
)
=>
state
.
user
.
data
.
nitku_trial
);
const
nitkuValue
=
watch
(
'
idTku
'
);
useEffect
(()
=>
{
if
(
!
nitkuValue
&&
nitku
)
{
setValue
(
'
idTku
'
,
nitku
);
}
},
[
nitku
,
nitkuValue
,
setValue
]);
return
(
<
Grid
sx=
{
{
mb
:
3
}
}
container
rowSpacing=
{
2
}
columnSpacing=
{
2
}
>
<
Grid
sx=
{
{
mt
:
3
}
}
size=
{
{
md
:
12
}
}
>
<
Divider
sx=
{
{
fontWeight
:
'
bold
'
}
}
textAlign=
"left"
>
Daftar Dokumen
</
Divider
>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Select
name=
"namaDok"
label=
"Nama Dokumen"
>
{
JENIS_DOKUMEN
.
map
((
item
,
index
)
=>
(
<
MenuItem
key=
{
index
}
value=
{
item
.
value
}
>
{
item
.
label
}
</
MenuItem
>
))
}
</
Field
.
Select
>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"nomorDok"
label=
"Nomor Dokumen"
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
DatePicker
name=
"tglDok"
label=
"Tanggal Dokumen"
maxDate=
{
dayjs
()
}
minDate=
{
dayjs
(
'
2025-01-01
'
)
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Select
name=
"idTku"
label=
"NITKU Pemotong"
>
<
MenuItem
value=
{
nitku
}
>
{
nitku
}
</
MenuItem
>
</
Field
.
Select
>
</
Grid
>
</
Grid
>
);
};
export
default
DokumenReferensi
;
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/Identitas.tsx
0 → 100644
View file @
d89056f3
import
Box
from
'
@mui/material/Box
'
;
import
Button
from
'
@mui/material/Button
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
dayjs
from
'
dayjs
'
;
import
{
useEffect
,
useState
}
from
'
react
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
type
{
TCountryResult
}
from
'
../../types/types
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
type
IdentitasProps
=
{
isPengganti
:
boolean
;
existingNr
?:
any
;
// Data penuh dari API (opsional, untuk edit/pengganti)
country
:
TCountryResult
;
};
const
Identitas
=
({
isPengganti
,
existingNr
,
country
}:
IdentitasProps
)
=>
{
const
{
setValue
,
watch
,
getValues
}
=
useFormContext
();
const
tanggalPemotongan
=
watch
(
'
tglPemotongan
'
);
const
maxKeterangan
=
5
;
const
[
jumlahKeterangan
,
setJumlahKeterangan
]
=
useState
<
number
>
(
0
);
// 🧩 Auto isi Tahun & Masa Pajak berdasarkan tanggalPemotongan
useEffect
(()
=>
{
if
(
tanggalPemotongan
)
{
const
date
=
dayjs
(
tanggalPemotongan
);
setValue
(
'
thnPajak
'
,
date
.
format
(
'
YYYY
'
));
setValue
(
'
masaPajak
'
,
date
.
format
(
'
MM
'
));
}
else
{
setValue
(
'
thnPajak
'
,
''
);
setValue
(
'
masaPajak
'
,
''
);
}
},
[
tanggalPemotongan
,
setValue
]);
// 🧠 Saat data API sudah masuk (edit/pengganti)
// Gunakan getValues() agar langsung membaca nilai dari form (bukan nunggu watch)
useEffect
(()
=>
{
if
(
existingNr
)
{
const
currentValues
=
getValues
();
const
arr
=
[
currentValues
.
keterangan1
,
currentValues
.
keterangan2
,
currentValues
.
keterangan3
,
currentValues
.
keterangan4
,
currentValues
.
keterangan5
,
];
const
count
=
arr
.
filter
((
k
)
=>
!!
k
&&
k
.
trim
()
!==
''
).
length
;
console
.
log
(
'
🧠 Detected existing keterangan:
'
,
arr
,
'
count:
'
,
count
);
if
(
count
>
0
)
{
setJumlahKeterangan
(
count
);
}
}
},
[
existingNr
,
getValues
]);
// 🧩 Pantau perubahan manual user (Tambah/Hapus)
useEffect
(()
=>
{
const
subscription
=
watch
((
values
)
=>
{
const
arr
=
[
values
.
keterangan1
,
values
.
keterangan2
,
values
.
keterangan3
,
values
.
keterangan4
,
values
.
keterangan5
,
];
const
count
=
arr
.
filter
((
k
)
=>
!!
k
&&
k
.
trim
()
!==
''
).
length
;
setJumlahKeterangan
(
count
);
});
return
()
=>
subscription
.
unsubscribe
();
},
[
watch
]);
// ➕ Tambah field baru
const
handleTambah
=
()
=>
{
if
(
jumlahKeterangan
<
maxKeterangan
)
{
setJumlahKeterangan
((
prev
)
=>
prev
+
1
);
}
};
// ➖ Hapus field terakhir
const
handleHapus
=
()
=>
{
if
(
jumlahKeterangan
>
0
)
{
setValue
(
`keterangan
${
jumlahKeterangan
}
`
,
''
);
setJumlahKeterangan
((
prev
)
=>
prev
-
1
);
}
};
return
(
<>
{
/* 📋 Identitas Dasar */
}
<
Grid
container
rowSpacing=
{
2
}
alignItems=
"center"
columnSpacing=
{
2
}
sx=
{
{
mb
:
4
}
}
>
{
/* 📅 Tanggal & Masa Pajak */
}
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
DatePicker
name=
"tglPemotongan"
label=
"Tanggal Pemotongan"
format=
"DD/MM/YYYY"
maxDate=
{
dayjs
()
}
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
3
}
}
>
<
Field
.
DatePicker
name=
"thnPajak"
label=
"Tahun Pajak"
view=
"year"
format=
"YYYY"
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
3
}
}
>
<
Field
.
DatePicker
name=
"masaPajak"
label=
"Masa Pajak"
view=
"month"
format=
"MM"
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"idDipotong"
label=
"Tax ID Number (TIN)"
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"namaDipotong"
label=
"Nama"
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
12
}
}
>
<
Field
.
Text
name=
"alamatDipotong"
label=
"Alamat"
multiline
minRows=
{
2
}
disabled=
{
isPengganti
}
sx=
{
{
'
& .MuiInputBase-inputMultiline
'
:
{
lineHeight
:
1.6
,
},
'
& .MuiOutlinedInput-root
'
:
{
borderRadius
:
'
8px
'
,
},
'
& .MuiOutlinedInput-notchedOutline
'
:
{
borderColor
:
'
#bdbdbd
'
,
},
'
&.Mui-focused .MuiOutlinedInput-notchedOutline
'
:
{
borderColor
:
'
#1976d2
'
,
borderWidth
:
'
2px
'
,
},
}
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
12
}
}
>
<
Field
.
Select
name=
"negaraDipotong"
label=
"Negara"
disabled=
{
isPengganti
}
>
{
country
.
map
((
item
)
=>
(
<
MenuItem
key=
{
item
.
kode
}
value=
{
item
.
kode
}
>
{
`${item.nama}`
}
</
MenuItem
>
))
}
</
Field
.
Select
>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"tmptLahirDipotong"
label=
"Tempat Lahir"
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
DatePicker
name=
"tglLahirDipotong"
label=
"Tanggal Lahir"
format=
"DD/MM/YYYY"
maxDate=
{
dayjs
()
}
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"nomorPaspor"
label=
"No. Paspor"
disabled=
{
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"nomorKitasKitap"
label=
"No.KITAS/KITAP"
disabled=
{
isPengganti
}
/>
</
Grid
>
</
Grid
>
{
/* ✏️ Tombol Tambah / Hapus Keterangan */
}
<
Box
sx=
{
{
display
:
'
flex
'
,
gap
:
2
,
mb
:
3
}
}
>
<
Box
sx=
{
{
borderRadius
:
'
18px
'
,
border
:
jumlahKeterangan
>=
maxKeterangan
?
'
1px solid #eee
'
:
'
1px solid #2e7d3280
'
,
color
:
jumlahKeterangan
>=
maxKeterangan
?
'
#eee
'
:
'
#2e7d3280
'
,
p
:
'
0px 10px
'
,
}
}
>
<
Button
disabled=
{
jumlahKeterangan
>=
maxKeterangan
}
onClick=
{
handleTambah
}
>
Tambah Keterangan
</
Button
>
</
Box
>
<
Box
sx=
{
{
borderRadius
:
'
18px
'
,
border
:
jumlahKeterangan
===
0
?
'
1px solid #eee
'
:
'
1px solid #f44336
'
,
color
:
jumlahKeterangan
===
0
?
'
#eee
'
:
'
#f44336
'
,
p
:
'
0px 10px
'
,
}
}
>
<
Button
disabled=
{
jumlahKeterangan
===
0
}
onClick=
{
handleHapus
}
>
Hapus Keterangan
</
Button
>
</
Box
>
</
Box
>
{
/* 🗒️ Input Keterangan Tambahan */
}
<
Box
sx=
{
{
mb
:
3
}
}
>
{
Array
.
from
({
length
:
jumlahKeterangan
}).
map
((
_
,
i
)
=>
(
<
Grid
size=
{
{
md
:
12
}
}
key=
{
`keterangan${i + 1}`
}
>
<
Field
.
Text
sx=
{
{
mb
:
2
}
}
name=
{
`keterangan${i + 1}`
}
label=
{
`Keterangan Tambahan ${i + 1}`
}
/>
</
Grid
>
))
}
</
Box
>
</>
);
};
export
default
Identitas
;
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/PanduanDnRekam.tsx
0 → 100644
View file @
d89056f3
import
type
{
FC
}
from
'
react
'
;
import
{
Fragment
,
memo
}
from
'
react
'
;
import
{
Box
,
Button
,
Card
,
CardContent
,
CardHeader
,
IconButton
,
Typography
}
from
'
@mui/material
'
;
import
{
ChevronRightRounded
,
CloseRounded
}
from
'
@mui/icons-material
'
;
import
{
m
}
from
'
framer-motion
'
;
import
{
PANDUAN_REKAM_DN
}
from
'
../../constant
'
;
interface
PanduanDnRekamProps
{
handleOpen
:
()
=>
void
;
isOpen
:
boolean
;
}
const
PanduanDnRekam
:
FC
<
PanduanDnRekamProps
>
=
({
handleOpen
,
isOpen
})
=>
(
<
Box
position=
"sticky"
>
{
/* Tombol toggle */
}
{
!
isOpen
&&
(
<
Box
height=
"100%"
display=
"flex"
justifyContent=
"center"
alignItems=
"center"
>
<
Button
variant=
"contained"
sx=
{
{
height
:
'
fit-content
'
,
right
:
0
,
borderRadius
:
0
,
minWidth
:
35
,
pt
:
3
,
pb
:
3
,
fontWeight
:
'
bold
'
,
fontSize
:
16
,
backgroundColor
:
'
#143B88
'
,
}
}
size=
"small"
onClick=
{
handleOpen
}
>
<
span
style=
{
{
writingMode
:
'
vertical-rl
'
,
transform
:
'
rotate(180deg)
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
}
}
>
Panduan Penggunaan
<
ChevronRightRounded
sx=
{
{
fontSize
:
30
}
}
/>
</
span
>
</
Button
>
</
Box
>
)
}
{
/* Konten panduan */
}
{
isOpen
&&
(
<
m
.
div
initial=
{
{
x
:
20
,
opacity
:
0
}
}
animate=
{
{
x
:
0
,
opacity
:
1
,
transition
:
{
delay
:
0.2
}
}
}
>
<
Card
>
<
CardHeader
avatar=
{
<
img
src=
"/assets/icon_panduan_penggunaan_1.svg"
alt=
"Panduan"
loading=
"lazy"
/>
}
sx=
{
{
backgroundColor
:
'
#123375
'
,
color
:
'
#FFFFFF
'
,
p
:
2
,
'
& .MuiCardHeader-title
'
:
{
fontSize
:
18
},
}
}
action=
{
<
IconButton
aria
-
label=
"close"
onClick=
{
handleOpen
}
sx=
{
{
color
:
'
white
'
}
}
>
<
CloseRounded
/>
</
IconButton
>
}
title=
"Panduan Penggunaan"
/>
<
CardContent
sx=
{
{
maxHeight
:
300
,
overflow
:
'
auto
'
,
'
&::-webkit-scrollbar
'
:
{
width
:
6
},
'
&::-webkit-scrollbar-track
'
:
{
backgroundColor
:
'
#f0f0f0
'
,
borderRadius
:
8
,
},
'
&::-webkit-scrollbar-thumb
'
:
{
backgroundColor
:
'
#123375
'
,
borderRadius
:
8
,
},
'
&::-webkit-scrollbar-thumb:hover
'
:
{
backgroundColor
:
'
#0d2858
'
,
},
scrollbarWidth
:
'
thin
'
,
scrollbarColor
:
'
#123375 #f0f0f0
'
,
}
}
>
{
/* Deskripsi Form */
}
<
Typography
variant=
"body2"
sx=
{
{
mb
:
2
,
whiteSpace
:
'
pre-line
'
}
}
>
<
strong
>
Deskripsi Form:
</
strong
>
<
br
/>
{
PANDUAN_REKAM_DN
.
description
.
intro
}
</
Typography
>
<
Typography
variant=
"body2"
>
{
PANDUAN_REKAM_DN
.
description
.
textList
}
</
Typography
>
<
Box
component=
"ol"
sx=
{
{
pl
:
3
,
mb
:
2
}
}
>
{
PANDUAN_REKAM_DN
.
description
.
list
.
map
((
item
,
idx
)
=>
(
<
Typography
key=
{
`desc-${idx}`
}
variant=
"body2"
component=
"li"
>
{
item
}
</
Typography
>
))
}
</
Box
>
<
Typography
variant=
"body2"
sx=
{
{
mb
:
2
}
}
>
{
PANDUAN_REKAM_DN
.
description
.
closing
}
</
Typography
>
{
/* Bagian-bagian */
}
{
PANDUAN_REKAM_DN
.
sections
.
map
((
section
,
i
)
=>
(
<
Box
key=
{
`section-${i}`
}
sx=
{
{
mb
:
2
}
}
>
<
Typography
variant=
"body2"
sx=
{
{
fontWeight
:
'
bold
'
,
fontSize
:
'
0.95rem
'
,
mb
:
0.5
}
}
>
{
section
.
title
}
</
Typography
>
<
Box
component=
"ul"
sx=
{
{
pl
:
2
,
listStyle
:
'
disc
'
}
}
>
{
section
.
items
.
map
((
item
,
idx
)
=>
(
<
Fragment
key=
{
`item-${i}-${idx}`
}
>
<
Box
component=
"li"
sx=
{
{
mb
:
0.5
}
}
>
<
Typography
variant=
"body2"
component=
"span"
>
{
item
.
text
}
</
Typography
>
{
item
.
subItems
?.
length
>
0
&&
(
<
Box
component=
"ol"
sx=
{
{
pl
:
3
,
listStyle
:
'
decimal
'
}
}
>
{
item
.
subItems
.
map
((
sub
,
subIdx
)
=>
(
<
Typography
key=
{
`sub-${i}-${idx}-${subIdx}`
}
variant=
"body2"
component=
"li"
>
{
sub
}
</
Typography
>
))
}
</
Box
>
)
}
</
Box
>
</
Fragment
>
))
}
</
Box
>
</
Box
>
))
}
</
CardContent
>
</
Card
>
</
m
.
div
>
)
}
</
Box
>
);
export
default
memo
(
PanduanDnRekam
);
src/sections/bupot-unifikasi/bupot-nr/components/rekamNr/PphDipotong.tsx
0 → 100644
View file @
d89056f3
// import Divider from '@mui/material/Divider';
// import Grid from '@mui/material/Grid';
// import MenuItem from '@mui/material/MenuItem';
// import { useEffect, useMemo, useRef, useState } from 'react';
// import { Field } from 'src/components/hook-form';
// import type { TGetListDataKOPNr } from '../../types/types';
// import { useFormContext } from 'react-hook-form';
// import {
// FG_FASILITAS_DN,
// FG_FASILITAS_MASTER_KEY,
// FG_FASILITAS_TEXT,
// TARIF_0,
// } from '../../constant';
// import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
// type PPHDipotongProps = {
// kodeObjectPajak: TGetListDataKOPNr[];
// isFormPrefilled?: boolean;
// };
// const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongProps) => {
// const { watch, setValue, getValues } = useFormContext<Record<string, any>>();
// const selectedKode = watch('kodeObjekPajak');
// const fgFasilitas = watch('fgFasilitas');
// const penghasilanBruto = watch('penghasilanBruto');
// const tarifWatched = watch('tarif');
// const normaPenghasilanNetoWatched = watch('normaPenghasilanNeto');
// const pphdipotongW = watch('pphDipotong');
// const kodeLoaded = Array.isArray(kodeObjectPajak) && kodeObjectPajak.length > 0;
// // ---- state & refs
// const [isPrefillDone, setIsPrefillDone] = useState(!isFormPrefilled);
// const initialCapturedRef = useRef<null | { kd?: any; fg?: any; tarif?: any; noDok?: any }>(null);
// const hasUserInteractedRef = useRef(false);
// const prevKdRef = useRef<string | undefined>(undefined);
// const prevFgRef = useRef<string | undefined>(undefined);
// // --- kode objek pajak terpilih
// const kodeObjekPajakSelected = useMemo(
// () => kodeObjectPajak.find((item) => item.kode === selectedKode),
// [kodeObjectPajak, selectedKode]
// );
// // --- helper: preserve decimal vs integer
// const setValuePreserve = (field: string, val: any) => {
// const safeVal = val != null && val !== '' ? String(val) : '';
// setValue(field, safeVal, { shouldValidate: true, shouldDirty: true });
// };
// const setValueInteger = (field: string, val: any) => {
// const safeVal = val != null && val !== '' ? String(Math.round(Number(val))) : '';
// setValue(field, safeVal, { shouldValidate: true, shouldDirty: true });
// };
// // --- Prefill detection
// useEffect(() => {
// if (!isFormPrefilled) {
// setIsPrefillDone(true);
// return;
// }
// if (isFormPrefilled && kodeLoaded && !initialCapturedRef.current) {
// initialCapturedRef.current = {
// kd: getValues('kodeObjekPajak'),
// fg: getValues('fgFasilitas'),
// tarif: getValues('tarif'),
// noDok: getValues('noDokLainnya'),
// };
// setIsPrefillDone(true);
// }
// }, [isFormPrefilled, kodeLoaded, getValues]);
// // --- detect user interactions
// useEffect(() => {
// if (prevKdRef.current !== undefined && prevKdRef.current !== selectedKode) {
// hasUserInteractedRef.current = true;
// }
// prevKdRef.current = selectedKode;
// }, [selectedKode]);
// useEffect(() => {
// if (prevFgRef.current !== undefined && prevFgRef.current !== fgFasilitas) {
// hasUserInteractedRef.current = true;
// }
// prevFgRef.current = fgFasilitas;
// }, [fgFasilitas]);
// // --- Perhitungan otomatis pph dipotong (27-100-06 pakai norma)
// useEffect(() => {
// if (!isPrefillDone) return;
// // hindari overwrite data prefilled sebelum interaksi
// if (isFormPrefilled && !hasUserInteractedRef.current) return;
// const fg = getValues('fgFasilitas');
// const bruto = Number(getValues('penghasilanBruto') || 0);
// const tarif = Number(getValues('tarif') || 0);
// const kode = getValues('kodeObjekPajak');
// const normaPenghasilanNeto = Number(getValues('normaPenghasilanNeto') || 0);
// const normaNetto = Number(getValues('normaPenghasilanNeto') || 0);
// let pph = 0;
// if (fg && !TARIF_0.includes(fg)) {
// if (kode === '27-100-06') {
// const norma = normaNetto || normaPenghasilanNeto || 0;
// pph = (bruto * (norma / 100) * tarif) / 100;
// } else {
// pph = (bruto * tarif) / 100;
// }
// }
// const currentPph = Number(getValues('pphDipotong') || 0);
// if (currentPph !== pph) {
// setValueInteger('pphDipotong', pph);
// }
// }, [
// penghasilanBruto,
// tarifWatched,
// fgFasilitas,
// normaPenghasilanNetoWatched,
// watch('normaPenghasilanNeto'),
// getValues,
// setValue,
// isPrefillDone,
// isFormPrefilled,
// ]);
// // --- Hitung awal setelah prefill selesai
// useEffect(() => {
// if (!isPrefillDone) return;
// const fg = getValues('fgFasilitas');
// const bruto = Number(getValues('penghasilanBruto') || 0);
// const tarif = Number(getValues('tarif') || 0);
// const initialPph = !fg ? 0 : TARIF_0.includes(fg) ? 0 : (bruto * tarif) / 100;
// setValueInteger('pphDipotong', initialPph);
// }, [isPrefillDone, getValues]);
// // --- Update tarif saat kode objek pajak berubah
// useEffect(() => {
// if (!isPrefillDone) return;
// if (!selectedKode || !kodeObjekPajakSelected) return;
// const kodeTarif = Number(kodeObjekPajakSelected.tarif) || 0;
// const currentTarif = Number(getValues('tarif') || 0);
// const prevKd = prevKdRef.current;
// const kdChangedByUser = prevKd !== undefined && prevKd !== selectedKode;
// if (isFormPrefilled && !hasUserInteractedRef.current && !kdChangedByUser) return;
// if (currentTarif !== kodeTarif) {
// setValuePreserve('tarif', kodeTarif); // ✅ keep decimals
// }
// }, [selectedKode, kodeObjekPajakSelected, isPrefillDone, isFormPrefilled, getValues]);
// // --- Reaksi terhadap perubahan fasilitas
// useEffect(() => {
// if (!isPrefillDone) return;
// const currentTarif = getValues('tarif');
// const currentNoDok = getValues('noDokLainnya');
// const kodeTarif = Number(kodeObjekPajakSelected?.tarif) || 0;
// const prevFg = prevFgRef.current;
// const fgChangedByUser = prevFg !== undefined && prevFg !== fgFasilitas;
// if (isFormPrefilled && !hasUserInteractedRef.current && !fgChangedByUser) return;
// if (fgFasilitas === FG_FASILITAS_DN.FASILITAS_LAINNYA) {
// setValuePreserve('tarif', 0);
// setValue('noDokLainnya', '', { shouldValidate: true });
// return;
// }
// if (fgFasilitas === FG_FASILITAS_DN.SKD_WPLN) {
// return; // biarkan user isi sendiri
// }
// if (String(currentTarif) !== String(kodeTarif)) {
// setValuePreserve('tarif', kodeTarif);
// }
// if (currentNoDok !== '') {
// setValue('noDokLainnya', '', { shouldValidate: true });
// }
// }, [fgFasilitas, kodeObjekPajakSelected, isPrefillDone, isFormPrefilled, getValues]);
// // --- Filter opsi fasilitas
// const fasilitasOptions = useMemo(() => {
// if (!kodeObjekPajakSelected) return [];
// return Object.values(FG_FASILITAS_DN)
// .map((v) => ({ value: v, label: FG_FASILITAS_TEXT[v] }))
// .filter(
// (opt) =>
// kodeObjekPajakSelected[FG_FASILITAS_MASTER_KEY[opt.value] as keyof TGetListDataKOPNr] ===
// 1
// );
// }, [kodeObjekPajakSelected]);
// // --- Update normaPenghasilanNeto dari norma netto
// useEffect(() => {
// if (!kodeObjekPajakSelected) return;
// if (kodeObjekPajakSelected.kode === '27-100-06') {
// if (!getValues('normaPenghasilanNeto')) {
// setValuePreserve('normaPenghasilanNeto', '5');
// }
// return;
// }
// const normaNetto = kodeObjekPajakSelected?.normanetto ?? '';
// const currentnormaPenghasilanNeto = getValues('normaPenghasilanNeto');
// if (String(currentnormaPenghasilanNeto) !== String(normaNetto)) {
// setValuePreserve('normaPenghasilanNeto', normaNetto);
// }
// }, [kodeObjekPajakSelected, getValues, setValue]);
// return (
// <Grid container rowSpacing={2} columnSpacing={2}>
// <Grid sx={{ mt: 3 }} size={{ md: 6 }}>
// <Field.Select name="kodeObjekPajak" label="Kode Objek Pajak">
// {kodeObjectPajak.map((item) => (
// <MenuItem key={item.kode} value={item.kode}>
// {`(${item.kode}) ${item.nama}`}
// </MenuItem>
// ))}
// </Field.Select>
// </Grid>
// <Grid size={{ md: 12 }}>
// <Divider sx={{ fontWeight: 'bold' }} textAlign="left">
// Fasilitas Pajak Penghasilan
// </Divider>
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Select name="fgFasilitas" label="Fasilitas">
// {fasilitasOptions.length === 0 ? (
// <MenuItem disabled value="">
// No options
// </MenuItem>
// ) : (
// fasilitasOptions.map((opt) => (
// <MenuItem key={opt.value} value={opt.value}>
// {opt.label}
// </MenuItem>
// ))
// )}
// </Field.Select>
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Text
// name="noDokLainnya"
// label="Nomor Dokumen Lainnya"
// disabled={['9', ''].includes(fgFasilitas)}
// sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
// />
// </Grid>
// <Grid size={{ md: 6 }}>
// <RHFNumeric
// name="penghasilanBruto"
// label="Jumlah Penghasilan Bruto (Rp)"
// allowNegativeValue={false}
// allowDecimalValue={false}
// />
// </Grid>
// <Grid size={{ md: 6 }}>
// {selectedKode === '27-100-06' ? (
// <Field.Select name="normaPenghasilanNeto" label="Perkiraan Penghasilan Netto (%)">
// <MenuItem value="5">5%</MenuItem>
// <MenuItem value="10">10%</MenuItem>
// <MenuItem value="25">25%</MenuItem>
// </Field.Select>
// ) : (
// <RHFNumeric
// name="normaPenghasilanNeto"
// label="Perkiraan Penghasilan Netto (%)"
// allowNegativeValue={false}
// allowDecimalValue={false}
// readOnly
// />
// )}
// </Grid>
// <Grid size={{ md: 6 }}>
// <RHFNumeric
// name="tarif"
// label="Tarif (%)"
// allowDecimalValue
// maxValue={100}
// readOnly={
// ![FG_FASILITAS_DN.FASILITAS_LAINNYA, FG_FASILITAS_DN.SKD_WPLN].includes(fgFasilitas)
// }
// disabled={
// ![FG_FASILITAS_DN.FASILITAS_LAINNYA, FG_FASILITAS_DN.SKD_WPLN].includes(fgFasilitas)
// }
// />
// </Grid>
// <Grid size={{ md: 6 }}>
// <RHFNumeric
// name="pphDipotong"
// label="PPh Yang Dipotong/Dipungut"
// allowNegativeValue={false}
// allowDecimalValue={false}
// readOnly
// />
// </Grid>
// </Grid>
// );
// };
// export default PphDipotong;
import
Divider
from
'
@mui/material/Divider
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
import
{
useEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
type
{
TGetListDataKOPNr
}
from
'
../../types/types
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
import
{
FG_FASILITAS_DN
,
FG_FASILITAS_MASTER_KEY
,
FG_FASILITAS_TEXT
,
TARIF_0
,
}
from
'
../../constant
'
;
import
{
RHFNumeric
}
from
'
src/components/hook-form/rhf-numeric
'
;
type
PPHDipotongProps
=
{
kodeObjectPajak
:
TGetListDataKOPNr
[];
isFormPrefilled
?:
boolean
;
};
const
PphDipotong
=
({
kodeObjectPajak
,
isFormPrefilled
=
false
}:
PPHDipotongProps
)
=>
{
const
{
watch
,
setValue
,
getValues
}
=
useFormContext
<
Record
<
string
,
any
>>
();
const
selectedKode
=
watch
(
'
kodeObjekPajak
'
);
const
fgFasilitas
=
watch
(
'
fgFasilitas
'
);
const
penghasilanBruto
=
watch
(
'
penghasilanBruto
'
);
const
tarifWatched
=
watch
(
'
tarif
'
);
const
normaPenghasilanNetoWatched
=
watch
(
'
normaPenghasilanNeto
'
);
const
kodeLoaded
=
Array
.
isArray
(
kodeObjectPajak
)
&&
kodeObjectPajak
.
length
>
0
;
// ---- state & refs
const
[
isPrefillDone
,
setIsPrefillDone
]
=
useState
(
!
isFormPrefilled
);
// initialCapturedRef sekarang menyimpan lebih banyak field termasuk bruto/norma
const
initialCapturedRef
=
useRef
<
null
|
{
kd
?:
any
;
fg
?:
any
;
tarif
?:
any
;
noDok
?:
any
;
bruto
?:
any
;
norma
?:
any
;
pph
?:
any
;
}
>
(
null
);
const
hasUserInteractedRef
=
useRef
(
false
);
const
prevKdRef
=
useRef
<
string
|
undefined
>
(
undefined
);
const
prevFgRef
=
useRef
<
string
|
undefined
>
(
undefined
);
// --- kode objek pajak terpilih
const
kodeObjekPajakSelected
=
useMemo
(
()
=>
kodeObjectPajak
.
find
((
item
)
=>
item
.
kode
===
selectedKode
),
[
kodeObjectPajak
,
selectedKode
]
);
// --- helper: preserve decimal vs integer
const
setValuePreserve
=
(
field
:
string
,
val
:
any
)
=>
{
const
safeVal
=
val
!=
null
&&
val
!==
''
?
String
(
val
)
:
''
;
setValue
(
field
,
safeVal
,
{
shouldValidate
:
true
,
shouldDirty
:
true
});
};
const
setValueInteger
=
(
field
:
string
,
val
:
any
)
=>
{
const
safeVal
=
val
!=
null
&&
val
!==
''
?
String
(
Math
.
round
(
Number
(
val
)))
:
''
;
setValue
(
field
,
safeVal
,
{
shouldValidate
:
true
,
shouldDirty
:
true
});
};
// --- Prefill detection
useEffect
(()
=>
{
if
(
!
isFormPrefilled
)
{
setIsPrefillDone
(
true
);
return
;
}
if
(
isFormPrefilled
&&
kodeLoaded
&&
!
initialCapturedRef
.
current
)
{
initialCapturedRef
.
current
=
{
kd
:
getValues
(
'
kodeObjekPajak
'
),
fg
:
getValues
(
'
fgFasilitas
'
),
tarif
:
getValues
(
'
tarif
'
),
noDok
:
getValues
(
'
noDokLainnya
'
),
bruto
:
getValues
(
'
penghasilanBruto
'
),
norma
:
getValues
(
'
normaPenghasilanNeto
'
),
pph
:
getValues
(
'
pphDipotong
'
),
};
setIsPrefillDone
(
true
);
}
},
[
isFormPrefilled
,
kodeLoaded
,
getValues
]);
// --- detect user interactions for kode & fasilitas (existing logic)
useEffect
(()
=>
{
if
(
prevKdRef
.
current
!==
undefined
&&
prevKdRef
.
current
!==
selectedKode
)
{
hasUserInteractedRef
.
current
=
true
;
}
prevKdRef
.
current
=
selectedKode
;
},
[
selectedKode
]);
useEffect
(()
=>
{
if
(
prevFgRef
.
current
!==
undefined
&&
prevFgRef
.
current
!==
fgFasilitas
)
{
hasUserInteractedRef
.
current
=
true
;
}
prevFgRef
.
current
=
fgFasilitas
;
},
[
fgFasilitas
]);
// --- NEW: detect if user changed bruto / tarif / norma / noDok compared to initial prefill
useEffect
(()
=>
{
if
(
!
initialCapturedRef
.
current
)
return
;
const
init
=
initialCapturedRef
.
current
;
const
curBruto
=
getValues
(
'
penghasilanBruto
'
);
const
curTarif
=
getValues
(
'
tarif
'
);
const
curNorma
=
getValues
(
'
normaPenghasilanNeto
'
);
const
curNoDok
=
getValues
(
'
noDokLainnya
'
);
const
changed
=
String
(
init
.
bruto
??
''
)
!==
String
(
curBruto
??
''
)
||
String
(
init
.
tarif
??
''
)
!==
String
(
curTarif
??
''
)
||
String
(
init
.
norma
??
''
)
!==
String
(
curNorma
??
''
)
||
String
(
init
.
noDok
??
''
)
!==
String
(
curNoDok
??
''
);
if
(
changed
)
{
hasUserInteractedRef
.
current
=
true
;
}
// only depend on watched values to trigger
},
[
penghasilanBruto
,
tarifWatched
,
normaPenghasilanNetoWatched
,
getValues
]);
// --- Perhitungan otomatis pph dipotong (27-100-06 pakai norma)
useEffect
(()
=>
{
if
(
!
isPrefillDone
)
return
;
// hindari overwrite data prefilled sebelum user benar2 ubah (tetapi allow jika user sudah berinteraksi)
if
(
isFormPrefilled
&&
!
hasUserInteractedRef
.
current
)
return
;
const
fg
=
getValues
(
'
fgFasilitas
'
);
const
bruto
=
Number
(
getValues
(
'
penghasilanBruto
'
)
||
0
);
const
tarif
=
Number
(
getValues
(
'
tarif
'
)
||
0
);
const
kode
=
getValues
(
'
kodeObjekPajak
'
);
const
normaPenghasilanNeto
=
Number
(
getValues
(
'
normaPenghasilanNeto
'
)
||
0
);
let
pph
=
0
;
if
(
fg
&&
!
TARIF_0
.
includes
(
fg
))
{
if
(
kode
===
'
27-100-06
'
)
{
const
norma
=
normaPenghasilanNeto
||
0
;
pph
=
(
bruto
*
(
norma
/
100
)
*
tarif
)
/
100
;
}
else
{
pph
=
(
bruto
*
tarif
)
/
100
;
}
}
const
currentPph
=
Number
(
getValues
(
'
pphDipotong
'
)
||
0
);
if
(
currentPph
!==
pph
)
{
setValueInteger
(
'
pphDipotong
'
,
pph
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
penghasilanBruto
,
tarifWatched
,
fgFasilitas
,
normaPenghasilanNetoWatched
,
getValues
,
setValue
,
isPrefillDone
,
isFormPrefilled
,
]);
// --- Hitung awal setelah prefill selesai
useEffect
(()
=>
{
if
(
!
isPrefillDone
)
return
;
const
fg
=
getValues
(
'
fgFasilitas
'
);
const
bruto
=
Number
(
getValues
(
'
penghasilanBruto
'
)
||
0
);
const
tarif
=
Number
(
getValues
(
'
tarif
'
)
||
0
);
const
initialPph
=
!
fg
?
0
:
TARIF_0
.
includes
(
fg
)
?
0
:
(
bruto
*
tarif
)
/
100
;
setValueInteger
(
'
pphDipotong
'
,
initialPph
);
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
isPrefillDone
,
getValues
]);
// --- Update tarif saat kode objek pajak berubah
useEffect
(()
=>
{
if
(
!
isPrefillDone
)
return
;
if
(
!
selectedKode
||
!
kodeObjekPajakSelected
)
return
;
const
kodeTarif
=
Number
(
kodeObjekPajakSelected
.
tarif
)
||
0
;
const
currentTarif
=
Number
(
getValues
(
'
tarif
'
)
||
0
);
const
prevKd
=
prevKdRef
.
current
;
const
kdChangedByUser
=
prevKd
!==
undefined
&&
prevKd
!==
selectedKode
;
if
(
isFormPrefilled
&&
!
hasUserInteractedRef
.
current
&&
!
kdChangedByUser
)
return
;
if
(
currentTarif
!==
kodeTarif
)
{
setValuePreserve
(
'
tarif
'
,
kodeTarif
);
// ✅ keep decimals
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
selectedKode
,
kodeObjekPajakSelected
,
isPrefillDone
,
isFormPrefilled
,
getValues
]);
// --- Reaksi terhadap perubahan fasilitas
useEffect
(()
=>
{
if
(
!
isPrefillDone
)
return
;
const
currentTarif
=
getValues
(
'
tarif
'
);
const
currentNoDok
=
getValues
(
'
noDokLainnya
'
);
const
kodeTarif
=
Number
(
kodeObjekPajakSelected
?.
tarif
)
||
0
;
const
prevFg
=
prevFgRef
.
current
;
const
fgChangedByUser
=
prevFg
!==
undefined
&&
prevFg
!==
fgFasilitas
;
if
(
isFormPrefilled
&&
!
hasUserInteractedRef
.
current
&&
!
fgChangedByUser
)
return
;
if
(
fgFasilitas
===
FG_FASILITAS_DN
.
FASILITAS_LAINNYA
)
{
setValuePreserve
(
'
tarif
'
,
0
);
setValue
(
'
noDokLainnya
'
,
''
,
{
shouldValidate
:
true
});
return
;
}
if
(
fgFasilitas
===
FG_FASILITAS_DN
.
SKD_WPLN
)
{
return
;
// biarkan user isi sendiri
}
if
(
String
(
currentTarif
)
!==
String
(
kodeTarif
))
{
setValuePreserve
(
'
tarif
'
,
kodeTarif
);
}
if
(
currentNoDok
!==
''
)
{
setValue
(
'
noDokLainnya
'
,
''
,
{
shouldValidate
:
true
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
fgFasilitas
,
kodeObjekPajakSelected
,
isPrefillDone
,
isFormPrefilled
,
getValues
]);
// --- Filter opsi fasilitas
const
fasilitasOptions
=
useMemo
(()
=>
{
if
(
!
kodeObjekPajakSelected
)
return
[];
return
Object
.
values
(
FG_FASILITAS_DN
)
.
map
((
v
)
=>
({
value
:
v
,
label
:
FG_FASILITAS_TEXT
[
v
]
}))
.
filter
(
(
opt
)
=>
kodeObjekPajakSelected
[
FG_FASILITAS_MASTER_KEY
[
opt
.
value
]
as
keyof
TGetListDataKOPNr
]
===
1
);
},
[
kodeObjekPajakSelected
]);
// --- Update normaPenghasilanNeto dari norma netto
useEffect
(()
=>
{
if
(
!
kodeObjekPajakSelected
)
return
;
if
(
kodeObjekPajakSelected
.
kode
===
'
27-100-06
'
)
{
if
(
!
getValues
(
'
normaPenghasilanNeto
'
))
{
setValuePreserve
(
'
normaPenghasilanNeto
'
,
'
5
'
);
}
return
;
}
const
normaNetto
=
kodeObjekPajakSelected
?.
normanetto
??
''
;
const
currentnormaPenghasilanNeto
=
getValues
(
'
normaPenghasilanNeto
'
);
if
(
String
(
currentnormaPenghasilanNeto
)
!==
String
(
normaNetto
))
{
setValuePreserve
(
'
normaPenghasilanNeto
'
,
normaNetto
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
kodeObjekPajakSelected
,
getValues
,
setValue
]);
return
(
<
Grid
container
rowSpacing=
{
2
}
columnSpacing=
{
2
}
>
<
Grid
sx=
{
{
mt
:
3
}
}
size=
{
{
md
:
6
}
}
>
<
Field
.
Select
name=
"kodeObjekPajak"
label=
"Kode Objek Pajak"
>
{
kodeObjectPajak
.
map
((
item
)
=>
(
<
MenuItem
key=
{
item
.
kode
}
value=
{
item
.
kode
}
>
{
`(${item.kode}) ${item.nama}`
}
</
MenuItem
>
))
}
</
Field
.
Select
>
</
Grid
>
<
Grid
size=
{
{
md
:
12
}
}
>
<
Divider
sx=
{
{
fontWeight
:
'
bold
'
}
}
textAlign=
"left"
>
Fasilitas Pajak Penghasilan
</
Divider
>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Select
name=
"fgFasilitas"
label=
"Fasilitas"
>
{
fasilitasOptions
.
length
===
0
?
(
<
MenuItem
disabled
value=
""
>
No options
</
MenuItem
>
)
:
(
fasilitasOptions
.
map
((
opt
)
=>
(
<
MenuItem
key=
{
opt
.
value
}
value=
{
opt
.
value
}
>
{
opt
.
label
}
</
MenuItem
>
))
)
}
</
Field
.
Select
>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"noDokLainnya"
label=
"Nomor Dokumen Lainnya"
disabled=
{
[
'
9
'
,
''
].
includes
(
fgFasilitas
)
}
sx=
{
{
'
& .MuiInputBase-root.Mui-disabled
'
:
{
backgroundColor
:
'
#f6f6f6
'
}
}
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
RHFNumeric
name=
"penghasilanBruto"
label=
"Jumlah Penghasilan Bruto (Rp)"
allowNegativeValue=
{
false
}
allowDecimalValue=
{
false
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
{
selectedKode
===
'
27-100-06
'
?
(
<
Field
.
Select
name=
"normaPenghasilanNeto"
label=
"Perkiraan Penghasilan Netto (%)"
>
<
MenuItem
value=
"5"
>
5%
</
MenuItem
>
<
MenuItem
value=
"10"
>
10%
</
MenuItem
>
<
MenuItem
value=
"25"
>
25%
</
MenuItem
>
</
Field
.
Select
>
)
:
(
<
RHFNumeric
name=
"normaPenghasilanNeto"
label=
"Perkiraan Penghasilan Netto (%)"
allowNegativeValue=
{
false
}
allowDecimalValue=
{
false
}
readOnly
/>
)
}
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
RHFNumeric
name=
"tarif"
label=
"Tarif (%)"
allowDecimalValue
maxValue=
{
100
}
readOnly=
{
!
[
FG_FASILITAS_DN
.
FASILITAS_LAINNYA
,
FG_FASILITAS_DN
.
SKD_WPLN
].
includes
(
fgFasilitas
)
}
disabled=
{
!
[
FG_FASILITAS_DN
.
FASILITAS_LAINNYA
,
FG_FASILITAS_DN
.
SKD_WPLN
].
includes
(
fgFasilitas
)
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
RHFNumeric
name=
"pphDipotong"
label=
"PPh Yang Dipotong/Dipungut"
allowNegativeValue=
{
false
}
allowDecimalValue=
{
false
}
readOnly
/>
</
Grid
>
</
Grid
>
);
};
export
default
PphDipotong
;
src/sections/bupot-unifikasi/bupot-nr/constant/queryKey.tsx
View file @
d89056f3
...
...
@@ -2,14 +2,14 @@ const appRootKey = 'unifikasi';
const
queryKey
=
{
getKodeObjekPajak
:
(
params
:
any
)
=>
[
appRootKey
,
'
kode-objek-pajak
'
,
params
],
dn
:
{
all
:
(
params
:
any
)
=>
[
appRootKey
,
'
dn
'
,
params
],
detail
:
(
params
:
any
)
=>
[
appRootKey
,
'
dn
'
,
'
detail
'
,
params
],
draft
:
[
appRootKey
,
'
dn
'
,
'
draft
'
],
delete
:
[
appRootKey
,
'
dn
'
,
'
delete
'
],
upload
:
[
appRootKey
,
'
dn
'
,
'
upload
'
],
cancel
:
[
appRootKey
,
'
dn
'
,
'
cancel
'
],
cetakPdf
:
(
params
:
any
)
=>
[
appRootKey
,
'
dn
-cetak-pdf
'
,
params
],
nr
:
{
all
:
(
params
:
any
)
=>
[
appRootKey
,
'
nr
'
,
params
],
detail
:
(
params
:
any
)
=>
[
appRootKey
,
'
nr
'
,
'
detail
'
,
params
],
draft
:
[
appRootKey
,
'
nr
'
,
'
draft
'
],
delete
:
[
appRootKey
,
'
nr
'
,
'
delete
'
],
upload
:
[
appRootKey
,
'
nr
'
,
'
upload
'
],
cancel
:
[
appRootKey
,
'
nr
'
,
'
cancel
'
],
cetakPdf
:
(
params
:
any
)
=>
[
appRootKey
,
'
nr
-cetak-pdf
'
,
params
],
},
};
...
...
src/sections/bupot-unifikasi/bupot-nr/hooks/useAdvancedFilter
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/hooks/useAdvancedFilter
Nr
.tsx
View file @
d89056f3
// type FilterItem = {
// field: string;
// operator: string;
// value?: string | number | Array<string | number> | null;
// join?: 'AND' | 'OR'; // optional: join connector BEFORE this item (first item usually undefined)
// };
// type BaseParams = Record<string, any>;
// export function useAdvancedFilter() {
// const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
// const dateFields = new Set(['created_at', 'updated_at']);
// const fieldMap: Record<string, string> = {
// noBupot: 'nomorBupot',
// };
// const dbField = (field: string) => fieldMap[field] ?? field;
// const escape = (v: string) => String(v).replace(/'/g, "''");
// const toDbDate = (value: string | Date) => {
// if (value instanceof Date) {
// const y = value.getFullYear();
// const m = String(value.getMonth() + 1).padStart(2, '0');
// const d = String(value.getDate()).padStart(2, '0');
// return `${y}${m}${d}`;
// }
// const digits = String(value).replace(/[^0-9]/g, '');
// if (digits.length >= 8) return digits.slice(0, 8);
// return digits;
// };
// const normalizeOp = (op: string) => op?.toString().trim();
// function buildAdvancedFilter(filters?: FilterItem[] | null) {
// if (!filters || filters.length === 0) return '';
// const exprs: string[] = []; // each item's expression
// const joins: ('AND' | 'OR')[] = []; // join before each expr (for item 0, push nothing/AND by default)
// for (let i = 0; i < filters.length; i++) {
// const f = filters[i];
// if (!f || !f.field) continue;
// const op = normalizeOp(f.operator ?? '');
// const fieldName = dbField(f.field);
// // build expression for this item
// let expr: string | null = null;
// // DATE handling
// if (dateFields.has(fieldName)) {
// const rawVal = f.value;
// if (!rawVal && !/is empty|is not empty/i.test(op)) {
// continue;
// }
// if (/^is$/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd} 00:00:00' AND \"${fieldName}\" <= '${ymd} 23:59:59'`;
// } else if (/is on or after/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd}'`;
// } else if (/is on or before/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" <= '${ymd}'`;
// }
// }
// // EMPTY checks (user requested LOWER("col") IS NULL semantics)
// if (/is empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NULL`;
// } else if (/is not empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NOT NULL`;
// }
// // IS ANY OF handling
// if (!expr && /is any of/i.test(op)) {
// // collect values array
// let values: Array<string | number> = [];
// if (Array.isArray(f.value)) values = f.value as any;
// else if (typeof f.value === 'string')
// values = (f.value as string)
// .split(',')
// .map((s) => s.trim())
// .filter(Boolean);
// else if (f.value != null) values = [f.value as any];
// if ((values || []).length === 0) {
// expr = null;
// } else {
// // special-case fgStatus: need LIKE %val% OR LIKE %val2%
// if (fieldName === 'fgStatus' || fieldName === 'fg_status') {
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") LIKE LOWER('%${s}%')`;
// });
// expr = `(${ors.join(' OR ')})`;
// } else {
// // default: OR of equality (case-insensitive)
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") = '${s}'`;
// });
// expr = `(${ors.join(' OR ')})`;
// }
// }
// }
// // FGSTATUS special single-value is / is not / contains semantics
// if (!expr && (fieldName === 'fgStatus' || fieldName === 'fg_status')) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '' && !/is any of|is empty|is not empty/i.test(op)) {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// if (/^is$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/is not/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") NOT LIKE LOWER('%${valEscaped}%')`;
// }
// }
// }
// // GENERIC text/numeric handling when expr still not set
// if (!expr) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '') {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// // numeric fields: operators (=, >=, <=)
// if (numericFields.has(fieldName) && /^(=|>=|<=)$/.test(op)) {
// expr = `\"${fieldName}\" ${op} '${valEscaped}'`;
// } else if (/^contains$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/^equals$/i.test(op)) {
// // equals should produce IN (wrap single value as IN)
// // attempt to parse CSV if provided
// let values: string[] = [];
// if (Array.isArray(f.value))
// values = (f.value as any[]).map((v) => escape(String(v).toLowerCase()));
// else values = [escape(String(f.value).toLowerCase())];
// expr = `LOWER(\"${fieldName}\") IN (${values.map((v) => `'${v}'`).join(',')})`;
// } else if (/^(>=|<=|=)$/.test(op) && !numericFields.has(fieldName)) {
// expr = `LOWER(\"${fieldName}\") ${op} '${valEscaped}'`;
// } else if (/^(is)$/i.test(op)) {
// // fallback: treat as equals
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// } else {
// // fallback equality
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// }
// }
// }
// if (expr) {
// exprs.push(expr);
// // record join for this item (use provided join or default AND except for first item)
// const joinBefore = (f.join as 'AND' | 'OR') ?? (exprs.length > 1 ? 'AND' : 'AND');
// joins.push(joinBefore);
// }
// }
// // now combine exprs with joins; joins[i] is join BEFORE exprs[i]
// if (exprs.length === 0) return '';
// let out = exprs[0];
// for (let i = 1; i < exprs.length; i++) {
// const j = joins[i] ?? 'AND';
// out = `(${out}) ${j} (${exprs[i]})`;
// }
// return out;
// }
// function buildRequestParams(base: BaseParams = {}, advanced: string) {
// const out: BaseParams = { ...(base ?? {}) };
// if ('noBupot' in out) {
// out.nomorBupot = out.noBupot;
// delete out.noBupot;
// }
// out.advanced = advanced || '';
// return out;
// }
// return { buildAdvancedFilter, buildRequestParams } as const;
// }
// export default useAdvancedFilter;
type
FilterItem
=
{
field
:
string
;
operator
:
string
;
...
...
src/sections/bupot-unifikasi/bupot-nr/hooks/useCancelDn.tsx
deleted
100644 → 0
View file @
9ffe9ff5
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
{
TCancelDnRequest
,
TCancelDnResponse
}
from
'
../types/types
'
;
import
dnApi
from
'
../utils/api
'
;
const
useCancelDn
=
(
props
?:
any
)
=>
useMutation
<
TCancelDnResponse
,
Error
,
TCancelDnRequest
>
({
mutationKey
:
[
'
cancel-dn
'
],
mutationFn
:
(
payload
)
=>
dnApi
.
cancel
(
payload
),
...
props
,
});
export
default
useCancelDn
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useCancelNr.tsx
0 → 100644
View file @
d89056f3
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
type
{
TCancelNrRequest
,
TCancelNrResponse
}
from
'
../types/types
'
;
import
nrApi
from
'
../utils/api
'
;
const
useCancelNr
=
(
props
?:
any
)
=>
useMutation
<
TCancelNrResponse
,
Error
,
TCancelNrRequest
>
({
mutationKey
:
[
'
cancel-nr
'
],
mutationFn
:
(
payload
)
=>
nrApi
.
cancel
(
payload
),
...
props
,
});
export
default
useCancelNr
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useCetakPdf
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/hooks/useCetakPdf
Nr
.tsx
View file @
d89056f3
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
dn
Api
from
'
../utils/api
'
;
import
nr
Api
from
'
../utils/api
'
;
const
useCetakPdf
Dn
=
(
options
?:
any
)
=>
const
useCetakPdf
Nr
=
(
options
?:
any
)
=>
useMutation
({
mutationKey
:
[
'
unifikasi
'
,
'
dn
'
,
'
cetak-pdf
'
],
mutationFn
:
async
(
params
:
any
)
=>
dn
Api
.
cetakPdfDetail
(
params
),
mutationKey
:
[
'
unifikasi
'
,
'
nr
'
,
'
cetak-pdf
'
],
mutationFn
:
async
(
params
:
any
)
=>
nr
Api
.
cetakPdfDetail
(
params
),
...
options
,
});
export
default
useCetakPdf
Dn
;
export
default
useCetakPdf
Nr
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useDelete
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/hooks/useDelete
Nr
.tsx
View file @
d89056f3
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
{
TDeleteDnRequest
,
TBaseResponseAPI
}
from
'
../types/types
'
;
import
dn
Api
from
'
../utils/api
'
;
import
type
{
TBaseResponseAPI
,
TDeleteNrRequest
}
from
'
../types/types
'
;
import
nr
Api
from
'
../utils/api
'
;
const
useDeleteDn
=
(
props
?:
any
)
=>
useMutation
<
TBaseResponseAPI
<
null
>
,
Error
,
TDelete
Dn
Request
>
({
mutationKey
:
[
'
delete-
dn
'
],
mutationFn
:
(
payload
)
=>
dnApi
.
deleteDn
(
payload
),
useMutation
<
TBaseResponseAPI
<
null
>
,
Error
,
TDelete
Nr
Request
>
({
mutationKey
:
[
'
delete-
nr
'
],
mutationFn
:
(
payload
)
=>
nrApi
.
deleteNr
(
payload
),
...
props
,
});
...
...
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetKodeObjekPajak.tsx
→
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetKodeObjekPajak
Nr
.tsx
View file @
d89056f3
import
{
useQuery
}
from
'
@tanstack/react-query
'
;
import
{
TBaseResponseAPI
,
TGetListDataKOPDn
Result
}
from
'
../types/types
'
;
import
type
{
TBaseResponseAPI
,
TGetListDataKOPNr
Result
}
from
'
../types/types
'
;
import
queryKey
from
'
../constant/queryKey
'
;
import
dn
Api
from
'
../utils/api
'
;
import
nr
Api
from
'
../utils/api
'
;
const
useGetKodeObjekPajak
=
(
params
?:
Record
<
string
,
any
>
)
=>
useQuery
<
TBaseResponseAPI
<
TGetListDataKOP
Dn
Result
>>
({
const
useGetKodeObjekPajak
Nr
=
(
params
?:
Record
<
string
,
any
>
)
=>
useQuery
<
TBaseResponseAPI
<
TGetListDataKOP
Nr
Result
>>
({
queryKey
:
queryKey
.
getKodeObjekPajak
(
params
),
queryFn
:
()
=>
dnApi
.
getKodeObjekPajakDn
(
params
),
queryFn
:
()
=>
nrApi
.
getKodeObjekPajakNr
(
params
),
});
export
default
useGetKodeObjekPajak
;
export
default
useGetKodeObjekPajak
Nr
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useGetNegara.tsx
0 → 100644
View file @
d89056f3
import
{
useQuery
}
from
'
@tanstack/react-query
'
;
import
type
{
TCountryResult
}
from
'
../types/types
'
;
import
nrApi
from
'
../utils/api
'
;
export
const
useGetNegara
=
(
params
?:
Record
<
string
,
any
>
)
=>
useQuery
<
TCountryResult
>
({
queryKey
:
[
'
negara-nr
'
],
queryFn
:
async
()
=>
{
const
res
=
await
nrApi
.
getCountryNr
(
params
);
return
res
.
data
;
// ✅ langsung array negara
},
});
export
default
useGetNegara
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useGet
Dn
.tsx
→
src/sections/bupot-unifikasi/bupot-nr/hooks/useGet
Nr
.tsx
View file @
d89056f3
import
{
isEmpty
}
from
'
lodash
'
;
import
{
useQuery
}
from
'
@tanstack/react-query
'
;
import
dnApi
from
'
../utils/api
'
;
import
{
TGetListDataTableDn
,
TGetListDataTableDnResult
}
from
'
../types/types
'
;
import
type
{
// TGetListDataTableDnResult,
TGetListDataTableNr
,
TGetListDataTableNrResult
,
}
from
'
../types/types
'
;
import
{
FG_PDF_STATUS
,
FG_SIGN_STATUS
}
from
'
../constant
'
;
import
queryKey
from
'
../constant/queryKey
'
;
import
nrApi
from
'
../utils/api
'
;
import
dayjs
from
'
dayjs
'
;
export
type
TGetDnApiWrapped
=
{
data
:
TGetListDataTable
Dn
Result
[];
data
:
TGetListDataTable
Nr
Result
[];
total
:
number
;
pageSize
:
number
;
page
:
number
;
// 1-based
...
...
@@ -61,7 +66,7 @@ export const formatDateToDDMMYYYY = (dateString: string | null | undefined) => {
return
`
${
day
}
/
${
month
}
/
${
year
}
`
;
};
const
normalisePropsGet
Dn
=
(
params
:
TGetListDataTableDn
)
=>
({
const
normalisePropsGet
Nr
=
(
params
:
TGetListDataTableNr
)
=>
({
...
params
,
nomorSP2D
:
params
.
dokumen_referensi
?.[
0
]?.
nomorSP2D
||
''
,
metodePembayaranBendahara
:
params
.
dokumen_referensi
?.[
0
]?.
metodePembayaranBendahara
||
''
,
...
...
@@ -74,7 +79,7 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
fgStatus
:
params
.
fgStatus
,
fgSignStatus
:
transformFgStatusToFgSignStatus
(
params
.
fgStatus
),
fgPdf
:
getFgStatusPdf
(
params
.
link
,
transformFgStatusToFgSignStatus
(
params
.
fgStatus
)),
fgLapor
:
params
.
fgLapor
,
//
fgLapor: params.fgLapor,
revNo
:
params
.
revNo
,
thnPajak
:
params
.
tahunPajak
,
msPajak
:
params
.
masaPajak
,
...
...
@@ -92,23 +97,55 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
updated_at
:
formatDateToDDMMYYYY
(
params
.
updated_at
),
});
// ---------- normalizer for params request ----------
const
normalisPropsParmasGetDn
=
(
params
:
any
)
=>
{
const
sorting
=
!
isEmpty
(
params
.
sortModel
)
?
transformSortModelToSortApiPayload
(
params
.
sortModel
)
:
{};
export
const
normalizeExistingNr
=
(
res
:
any
)
=>
({
// 🧾 Data Pajak Utama
tglPemotongan
:
res
.
tglpemotongan
??
''
,
thnPajak
:
res
.
tahunPajak
??
''
,
msPajak
:
res
.
masaPajak
??
''
,
return
{
...
params
,
page
:
(
typeof
params
.
page
===
'
number
'
?
params
.
page
:
0
)
+
1
,
limit
:
params
.
pageSize
,
masaPajak
:
params
.
msPajak
||
null
,
tahunPajak
:
params
.
thnPajak
||
null
,
npwp
:
params
.
idDipotong
||
null
,
advanced
:
isEmpty
(
params
.
advanced
)
?
undefined
:
params
.
advanced
,
...
sorting
,
};
};
// 👤 Identitas Dipotong
idDipotong
:
res
.
npwpPemotong
??
''
,
namaDipotong
:
res
.
namaDipotong
??
''
,
alamatDipotong
:
res
.
alamatDipotong
??
''
,
negaraDipotong
:
res
.
negaraDipotong
??
''
,
tmptLahirDipotong
:
res
.
tmptLahirDipotong
??
''
,
tglLahirDipotong
:
res
.
tglLahirDipotong
&&
res
.
tglLahirDipotong
.
length
===
8
?
dayjs
(
res
.
tglLahirDipotong
,
'
DDMMYYYY
'
).
format
(
'
YYYY-MM-DD
'
)
:
''
,
nomorPaspor
:
res
.
nomorPaspor
??
''
,
nomorKitasKitap
:
res
.
nomorKitasKitap
??
''
,
// 🧠 Informasi Tambahan
email
:
res
.
email
??
''
,
keterangan1
:
res
.
keterangan1
??
''
,
keterangan2
:
res
.
keterangan2
??
''
,
keterangan3
:
res
.
keterangan3
??
''
,
keterangan4
:
res
.
keterangan4
??
''
,
keterangan5
:
res
.
keterangan5
??
''
,
// 💰 Pajak dan Penghasilan
kodeObjekPajak
:
res
.
kodeObjekPajak
??
''
,
fgFasilitas
:
res
.
sertifikatInsentifDipotong
??
''
,
noDokLainnya
:
res
.
nomorSertifikatInsentif
??
''
,
penghasilanBruto
:
res
.
penghasilanBruto
??
''
,
normaPenghasilanNeto
:
res
.
normaPenghasilanNeto
??
''
,
tarif
:
String
(
res
.
tarif
??
''
),
pphDipotong
:
String
(
res
.
pphDipotong
??
''
),
// 📄 Dokumen Referensi
namaDok
:
res
.
dokumen_referensi
?.[
0
]?.
dokReferensi
??
''
,
nomorDok
:
res
.
dokumen_referensi
?.[
0
]?.
nomorDokumen
??
''
,
tglDok
:
res
.
dokumen_referensi
?.[
0
]?.
tanggal_Dokumen
??
''
,
// 🏢 Cabang / Unit
idTku
:
res
.
idTku
??
''
,
// 🆔 Metadata tambahan
idBupot
:
res
.
idBupot
??
''
,
noBupot
:
res
.
noBupot
??
''
,
revNo
:
res
.
revNo
??
0
,
});
const
normalizeParams
=
(
params
:
any
)
=>
{
const
{
...
...
@@ -155,7 +192,7 @@ const normalizeParams = (params: any) => {
};
};
export
const
useGet
Dn
=
({
params
}:
{
params
:
any
})
=>
{
export
const
useGet
Nr
=
({
params
}:
{
params
:
any
})
=>
{
const
{
page
,
limit
,
advanced
,
sortingMode
,
sortingMethod
}
=
params
;
const
normalized
=
normalizeParams
(
params
);
...
...
@@ -163,19 +200,19 @@ export const useGetDn = ({ params }: { params: any }) => {
queryKey
:
[
'
dn
'
,
page
,
limit
,
advanced
,
sortingMode
,
sortingMethod
],
queryFn
:
async
()
=>
{
const
res
:
any
=
await
dnApi
.
getDn
({
params
:
normalized
});
const
res
:
any
=
await
nrApi
.
getNr
({
params
:
normalized
});
const
rawData
:
any
[]
=
Array
.
isArray
(
res
?.
data
)
?
res
.
data
:
res
?.
data
?
[
res
.
data
]
:
[];
const
total
=
Number
(
res
?.
total
??
res
?.
totalRow
??
0
);
let
dataArray
:
TGetListDataTable
Dn
Result
[]
=
[];
let
dataArray
:
TGetListDataTable
Nr
Result
[]
=
[];
const
normalizeWithWorker
=
()
=>
new
Promise
<
TGetListDataTable
Dn
Result
[]
>
((
resolve
,
reject
)
=>
{
new
Promise
<
TGetListDataTable
Nr
Result
[]
>
((
resolve
,
reject
)
=>
{
try
{
const
worker
=
new
Worker
(
new
URL
(
'
../workers/normalize
Dn
.worker.js
'
,
import
.
meta
.
url
),
new
URL
(
'
../workers/normalize
Nr
.worker.js
'
,
import
.
meta
.
url
),
{
type
:
'
module
'
}
);
...
...
@@ -186,7 +223,7 @@ export const useGetDn = ({ params }: { params: any }) => {
reject
(
new
Error
(
error
));
}
else
{
worker
.
terminate
();
resolve
(
data
as
TGetListDataTable
Dn
Result
[]);
resolve
(
data
as
TGetListDataTable
Nr
Result
[]);
}
};
...
...
@@ -206,11 +243,11 @@ export const useGetDn = ({ params }: { params: any }) => {
dataArray
=
await
normalizeWithWorker
();
}
else
{
console
.
warn
(
'
⚠️ Worker not supported, using sync normalization
'
);
dataArray
=
rawData
.
map
(
normalisePropsGet
Dn
)
as
unknown
as
TGetListDataTableDn
Result
[];
dataArray
=
rawData
.
map
(
normalisePropsGet
Nr
)
as
unknown
as
TGetListDataTableNr
Result
[];
}
}
catch
(
err
)
{
console
.
error
(
'
❌ Worker failed, fallback to sync normalize:
'
,
err
);
dataArray
=
rawData
.
map
(
normalisePropsGet
Dn
)
as
unknown
as
TGetListDataTableDn
Result
[];
dataArray
=
rawData
.
map
(
normalisePropsGet
Nr
)
as
unknown
as
TGetListDataTableNr
Result
[];
}
return
{
...
...
@@ -230,43 +267,14 @@ export const useGetDn = ({ params }: { params: any }) => {
});
};
export
const
useGet
Dn
ById
=
(
id
:
string
,
options
=
{})
=>
export
const
useGet
Nr
ById
=
(
id
:
string
,
options
=
{})
=>
useQuery
({
queryKey
:
queryKey
.
dn
.
detail
(
id
),
queryKey
:
queryKey
.
nr
.
detail
(
id
),
queryFn
:
async
()
=>
{
cons
ole
.
log
(
'
🔍 Fetching getDnById with ID:
'
,
id
);
cons
t
res
=
await
dnApi
.
getDnById
(
id
);
cons
t
res
=
await
nrApi
.
getNrById
(
id
);
cons
ole
.
log
(
res
);
if
(
!
res
)
throw
new
Error
(
'
Data tidak ditemukan
'
);
const
normalized
=
{
id
:
res
.
id
??
''
,
tglPemotongan
:
res
.
tglpemotongan
??
''
,
thnPajak
:
res
.
tahunPajak
??
''
,
msPajak
:
res
.
masaPajak
??
''
,
idDipotong
:
res
.
npwpPemotong
??
''
,
nitku
:
res
.
idTku
??
''
,
namaDipotong
:
res
.
nama
??
''
,
email
:
res
.
email
??
''
,
keterangan1
:
res
.
keterangan1
??
''
,
keterangan2
:
res
.
keterangan2
??
''
,
keterangan3
:
res
.
keterangan3
??
''
,
keterangan4
:
res
.
keterangan4
??
''
,
keterangan5
:
res
.
keterangan5
??
''
,
kdObjPjk
:
res
.
kodeObjekPajak
??
''
,
fgFasilitas
:
res
.
sertifikatInsentifDipotong
??
''
,
noDokLainnya
:
res
.
nomorSertifikatInsentif
??
''
,
jmlBruto
:
res
.
dpp
??
''
,
tarif
:
String
(
res
.
tarif
??
''
),
pphDipotong
:
String
(
res
.
pphDipotong
??
''
),
namaDok
:
res
.
dokumen_referensi
?.[
0
]?.
dokReferensi
??
''
,
nomorDok
:
res
.
dokumen_referensi
?.[
0
]?.
nomorDokumen
??
''
,
tglDok
:
res
.
dokumen_referensi
?.[
0
]?.
tanggal_Dokumen
??
''
,
idTku
:
res
.
idTku
??
''
,
revNo
:
res
.
revNo
??
0
,
noBupot
:
res
.
noBupot
??
''
,
idBupot
:
res
.
idBupot
??
''
,
};
const
normalized
=
normalizeExistingNr
(
res
);
console
.
log
(
'
✅ Normalized data:
'
,
normalized
);
return
normalized
;
},
...
...
@@ -275,4 +283,4 @@ export const useGetDnById = (id: string, options = {}) =>
...
options
,
});
export
default
useGet
Dn
;
export
default
useGet
Nr
;
src/sections/bupot-unifikasi/bupot-nr/hooks/usePphDipotong.tsx
View file @
d89056f3
/* eslint-disable @typescript-eslint/no-shadow */
import
{
useEffect
}
from
'
react
'
;
import
{
useFormContext
,
useWatch
}
from
'
react-hook-form
'
;
import
{
TGetListDataKOPDn
}
from
'
../types/types
'
;
import
type
{
TGetListDataKOPNr
}
from
'
../types/types
'
;
const
usePphDipotong
=
(
kodeObjekPajakSelected
?:
TGetListDataKOP
Dn
)
=>
{
const
usePphDipotong
=
(
kodeObjekPajakSelected
?:
TGetListDataKOP
Nr
)
=>
{
const
{
watch
,
setValue
,
control
}
=
useFormContext
();
// ambil value dari form
...
...
@@ -35,7 +36,6 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
name
:
[
'
thnPajak
'
,
'
fgFasilitas
'
,
'
fgIdDipotong
'
,
'
jmlBruto
'
,
'
tarif
'
],
});
// eslint-disable-next-line @typescript-eslint/no-shadow
const
calculateAndSetPphDipotong
=
(
thnPajak
:
number
,
fgFasilitas
:
string
,
...
...
@@ -67,6 +67,7 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
Number
(
handlerSetPphDipotong
[
4
])
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
handlerSetPphDipotong
]);
return
{
...
...
src/sections/bupot-unifikasi/bupot-nr/hooks/useSaveDn.tsx
deleted
100644 → 0
View file @
9ffe9ff5
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
dayjs
from
'
dayjs
'
;
import
dnApi
from
'
../utils/api
'
;
import
{
TPostDnRequest
}
from
'
../types/types
'
;
const
transformParams
=
({
isPengganti
=
false
,
...
dnData
}:
any
):
TPostDnRequest
=>
{
const
{
id
,
idBupot
,
noBupot
,
msPajak
,
thnPajak
,
idDipotong
,
nitku
,
namaDipotong
,
fgFasilitas
,
noDokLainnya
,
kdObjPjk
,
kdJnsPjk
,
statusPph
,
jmlBruto
,
tarif
,
pphDipotong
,
kap
,
kjs
,
revNo
:
initialRevNo
,
tglPemotongan
,
namaDok
,
nomorDok
,
tglDok
,
metodePembayaranBendahara
,
nomorSP2D
,
idTku
,
email
,
glAccount
,
keterangan1
,
keterangan2
,
keterangan3
,
keterangan4
,
keterangan5
,
}
=
dnData
;
const
dokReferensi
=
[
{
dokReferensi
:
namaDok
||
''
,
nomorDokumen
:
nomorDok
||
''
,
tanggal_Dokumen
:
tglDok
?
dayjs
(
tglDok
).
format
(
'
DDMMYYYY
'
)
:
''
,
metodePembayaranBendahara
:
metodePembayaranBendahara
||
''
,
nomorSP2D
:
nomorSP2D
||
''
,
},
];
const
revNo
=
isPengganti
?
parseInt
(
initialRevNo
||
0
,
10
)
+
1
:
parseInt
(
initialRevNo
||
0
,
10
);
const
npwpLog
=
localStorage
.
getItem
(
'
npwp_log
'
)
??
''
;
return
{
id
:
!
isPengganti
?
(
id
??
null
)
:
null
,
idBupot
:
idBupot
??
null
,
noBupot
:
noBupot
??
null
,
npwpPemotong
:
npwpLog
,
idTku
:
idTku
??
''
,
masaPajak
:
msPajak
?
dayjs
(
msPajak
).
format
(
'
MM
'
)
:
''
,
tahunPajak
:
thnPajak
?
Number
(
dayjs
(
thnPajak
).
format
(
'
YYYY
'
))
:
0
,
npwp
:
idDipotong
??
''
,
nik
:
nitku
??
(
idDipotong
?
`
${
idDipotong
}
000000`
:
''
),
nama
:
namaDipotong
??
''
,
revNo
,
fgNpwpNik
:
'
true
'
,
// static
fgJnsBupot
:
'
BPU
'
,
// static
dataDetilBpu
:
{
sertifikatInsentifDipotong
:
fgFasilitas
??
'
9
'
,
nomorSertifikatInsentif
:
noDokLainnya
??
''
,
kodeObjekPajak
:
kdObjPjk
??
''
,
pasalPPh
:
kdJnsPjk
??
''
,
statusPPh
:
statusPph
??
''
,
dpp
:
jmlBruto
??
''
,
tarif
:
tarif
??
''
,
pphDipotong
:
pphDipotong
??
''
,
kap
:
kap
??
''
,
kjs
:
kjs
??
''
,
dokReferensi
,
},
tglPemotongan
:
tglPemotongan
?
dayjs
(
tglPemotongan
).
format
(
'
DDMMYYYY
'
)
:
''
,
email
:
email
??
''
,
glAccount
:
glAccount
??
''
,
keterangan1
:
keterangan1
??
''
,
keterangan2
:
keterangan2
??
''
,
keterangan3
:
keterangan3
??
''
,
keterangan4
:
keterangan4
??
''
,
keterangan5
:
keterangan5
??
''
,
};
};
const
useSaveDn
=
(
props
?:
any
)
=>
useMutation
({
mutationKey
:
[
'
Save-Dn
'
],
mutationFn
:
(
params
:
any
)
=>
dnApi
.
saveDn
(
transformParams
(
params
)),
...
props
,
});
export
default
useSaveDn
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useSaveNr.tsx
0 → 100644
View file @
d89056f3
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
dayjs
from
'
dayjs
'
;
import
type
{
TPostNrRequest
}
from
'
../types/types
'
;
import
nrApi
from
'
../utils/api
'
;
const
transformParams
=
({
isPengganti
=
false
,
...
nrData
}:
any
):
TPostNrRequest
=>
{
const
{
id
,
idBupot
,
noBupot
,
npwpPemotong
,
idTku
,
masaPajak
,
tahunPajak
,
tinDipotong
,
namaDipotong
,
alamatDipotong
,
negaraDipotong
,
tglLahirDipotong
,
tmptLahirDipotong
,
nomorPaspor
,
nomorKitasKitap
,
sertifikatInsentifDipotong
,
nomorSertifikatInsentif
,
kodeObjekPajak
,
pasalPph
,
statusPph
,
penghasilanBruto
,
normaPenghasilanNeto
,
tarif
,
pphDipotong
,
kap
,
kjs
,
metodePembayaranBendahara
,
nomorSP2D
,
tglPemotongan
,
userId
,
kanal
,
revNo
:
initialRevNo
,
glAccount
,
keterangan1
,
keterangan2
,
keterangan3
,
keterangan4
,
keterangan5
,
}
=
nrData
;
// Increment revNo kalau pengganti
const
revNo
=
isPengganti
?
parseInt
(
initialRevNo
?.
toString
()
||
'
0
'
,
10
)
+
1
:
parseInt
(
initialRevNo
?.
toString
()
||
'
0
'
,
10
);
// Ambil NPWP dari localStorage kalau mau fallback
const
npwpLog
=
localStorage
.
getItem
(
'
npwp_log
'
)
??
''
;
return
{
id
:
!
isPengganti
?
(
id
??
null
)
:
null
,
idBupot
:
idBupot
??
null
,
noBupot
:
noBupot
??
null
,
// Header-level Identitas
npwpPemotong
:
npwpPemotong
??
npwpLog
,
idTku
:
idTku
??
''
,
masaPajak
:
masaPajak
?
dayjs
(
masaPajak
).
format
(
'
MM
'
)
:
''
,
tahunPajak
:
tahunPajak
?
Number
(
dayjs
(
tahunPajak
).
format
(
'
YYYY
'
))
:
new
Date
().
getFullYear
(),
// Data Wajib Pajak Dipotong
tinDipotong
:
tinDipotong
??
''
,
namaDipotong
:
namaDipotong
??
''
,
alamatDipotong
:
alamatDipotong
??
''
,
negaraDipotong
:
negaraDipotong
??
''
,
tglLahirDipotong
:
tglLahirDipotong
?
dayjs
(
tglLahirDipotong
).
format
(
'
DDMMYYYY
'
)
:
''
,
tmptLahirDipotong
:
tmptLahirDipotong
??
''
,
nomorPaspor
:
nomorPaspor
??
''
,
nomorKitasKitap
:
nomorKitasKitap
??
''
,
keterangan1
:
keterangan1
??
''
,
keterangan2
:
keterangan2
??
''
,
keterangan3
:
keterangan3
??
''
,
keterangan4
:
keterangan4
??
''
,
keterangan5
:
keterangan5
??
''
,
// Fasilitas
sertifikatInsentifDipotong
:
sertifikatInsentifDipotong
??
'
9
'
,
nomorSertifikatInsentif
:
nomorSertifikatInsentif
??
''
,
// Objek Pajak
kodeObjekPajak
:
kodeObjekPajak
??
''
,
pasalPph
:
pasalPph
??
''
,
statusPph
:
statusPph
??
''
,
penghasilanBruto
:
Number
(
penghasilanBruto
??
0
),
normaPenghasilanNeto
:
Number
(
normaPenghasilanNeto
??
0
),
tarif
:
Number
(
tarif
??
0
),
pphDipotong
:
Number
(
pphDipotong
??
0
),
kap
:
Number
(
kap
??
0
),
kjs
:
Number
(
kjs
??
0
),
dokReferensi
:
(()
=>
{
const
{
namaDok
,
nomorDok
,
tglDok
}
=
nrData
;
// pastikan tidak undefined dan tanggal valid
if
(
!
namaDok
||
!
nomorDok
||
!
tglDok
)
return
[];
const
parsedDate
=
dayjs
(
tglDok
);
const
tanggalFormatted
=
parsedDate
.
isValid
()
?
parsedDate
.
format
(
'
DDMMYYYY
'
)
:
''
;
if
(
!
tanggalFormatted
)
return
[];
return
[
{
dokReferensi
:
namaDok
,
nomorDokumen
:
nomorDok
,
tanggal_Dokumen
:
tanggalFormatted
,
},
];
})(),
metodePembayaranBendahara
:
metodePembayaranBendahara
??
''
,
nomorSP2D
:
nomorSP2D
??
''
,
tglPemotongan
:
tglPemotongan
?
dayjs
(
tglPemotongan
).
format
(
'
DDMMYYYY
'
)
:
''
,
userId
:
userId
??
''
,
kanal
:
kanal
??
''
,
revNo
,
glAccount
:
glAccount
??
''
,
};
};
const
useSaveNr
=
(
props
?:
any
)
=>
useMutation
({
mutationKey
:
[
'
Save-Nr
'
],
mutationFn
:
(
params
:
any
)
=>
nrApi
.
saveNr
(
transformParams
(
params
)),
...
props
,
});
export
default
useSaveNr
;
src/sections/bupot-unifikasi/bupot-nr/hooks/useUpload.tsx
View file @
d89056f3
// hooks/useUpload.ts
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
dn
Api
from
'
../utils/api
'
;
import
nr
Api
from
'
../utils/api
'
;
const
useUpload
=
(
props
?:
any
)
=>
useMutation
({
mutationKey
:
[
'
upload-
dn
'
],
mutationFn
:
(
payload
:
{
id
:
string
|
number
})
=>
dn
Api
.
upload
(
payload
),
mutationKey
:
[
'
upload-
nr
'
],
mutationFn
:
(
payload
:
{
id
:
string
|
number
})
=>
nr
Api
.
upload
(
payload
),
...
props
,
});
...
...
src/sections/bupot-unifikasi/bupot-nr/types/types.ts
0 → 100644
View file @
d89056f3
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
TGetListDataTableNr
=
{
id
:
number
;
npwpPemotong
:
string
;
idTku
:
string
;
masaPajak
:
string
;
tahunPajak
:
string
;
fgNpwpNik
:
string
;
npwp
:
string
;
nik
:
string
;
nama
:
string
;
sertifikatInsentifDipotong
:
string
;
nomorSertifikatInsentif
:
string
;
kodeObjekPajak
:
string
;
pasalPPh
:
string
;
statusPPh
:
string
;
dpp
:
string
;
tarif
:
string
;
pphDipotong
:
string
;
kap
:
string
;
kjs
:
string
;
tglpemotongan
:
string
;
userId
:
string
;
created_at
:
string
;
updated_at
:
string
;
created_by
:
string
;
updated_by
:
string
;
fgStatus
:
string
;
internal_id
:
string
;
dokumen_referensi
:
{
dokReferensi
:
string
;
nomorDokumen
:
string
;
tanggal_Dokumen
:
string
;
metodePembayaranBendahara
:
string
;
nomorSP2D
:
string
;
}[];
revNo
:
number
;
noBupot
:
string
;
idBupot
:
string
;
npwpNikPenandatangan
:
string
;
namaPenandatangan
:
string
;
link
:
string
|
null
;
errorMsg
:
string
|
null
;
email
:
string
|
null
;
glAccount
:
string
;
fgkirimemail
:
string
;
glName
:
string
|
null
;
keterangan1
:
string
|
null
;
keterangan2
:
string
|
null
;
keterangan3
:
string
|
null
;
keterangan4
:
string
|
null
;
keterangan5
:
string
|
null
;
};
export
type
TGetListDataTableNrResult
=
TGetListDataTableNr
[];
export
type
TGetListDataKOPNr
=
{
kode
:
string
;
nama
:
string
;
pasal
:
string
;
statuspph
:
string
;
normanetto
:
string
;
tarif
:
string
;
kap
:
string
;
kjs
:
string
;
noCertificate
:
number
;
certofDomicile
:
number
;
otherCert
:
number
;
};
export
type
TGetListDataKOPNrResult
=
TGetListDataKOPNr
[];
export
type
ActionItem
=
{
title
:
string
;
icon
:
React
.
ReactNode
;
func
?:
()
=>
void
;
disabled
?:
boolean
;
};
export
type
TDokReferensi
=
{
dokReferensi
:
string
;
nomorDokumen
:
string
;
tanggal_Dokumen
:
string
;
// format: DDMMYYYY
};
export
type
TPostNrRequest
=
{
id
:
string
|
null
;
idBupot
:
string
;
noBupot
:
string
;
npwpPemotong
:
string
;
idTku
:
string
;
masaPajak
:
string
;
tahunPajak
:
number
;
tinDipotong
:
string
;
namaDipotong
:
string
;
alamatDipotong
:
string
;
negaraDipotong
:
string
;
tglLahirDipotong
:
string
;
tmptLahirDipotong
:
string
;
nomorPaspor
:
string
;
nomorKitasKitap
:
string
;
sertifikatInsentifDipotong
:
string
;
nomorSertifikatInsentif
:
string
;
kodeObjekPajak
:
string
;
pasalPph
:
string
;
statusPph
:
string
;
penghasilanBruto
:
number
;
normaPenghasilanNeto
:
number
;
tarif
:
number
;
pphDipotong
:
number
;
kap
:
number
;
kjs
:
number
;
dokReferensi
:
TDokReferensi
[];
metodePembayaranBendahara
:
string
;
nomorSP2D
:
string
;
tglPemotongan
:
string
;
userId
:
string
;
kanal
:
string
;
revNo
:
number
;
glAccount
:
string
;
keterangan1
:
string
|
null
;
keterangan2
:
string
|
null
;
keterangan3
:
string
|
null
;
keterangan4
:
string
|
null
;
keterangan5
:
string
|
null
;
};
export
type
TCountry
=
{
kode
:
string
;
nama
:
string
;
};
export
type
TCountryResult
=
TCountry
[];
export
type
TPostUpload
=
{
id
:
string
;
};
export
type
TDeleteNrRequest
=
{
id
:
string
;
};
export
type
TCancelNrRequest
=
{
id
:
string
|
number
;
tglPembatalan
:
string
;
// format: DDMMYYYY
};
export
type
TCancelNrResponse
=
TBaseResponseAPI
<
{
id
:
string
|
number
;
statusBatal
?:
string
;
message
?:
string
;
}
>
;
src/sections/bupot-unifikasi/bupot-nr/utils/api.tsx
View file @
d89056f3
import
axios
from
'
axios
'
;
import
{
import
type
{
TBaseResponseAPI
,
TCancel
Dn
Request
,
TCancel
Dn
Response
,
T
DeleteDnReques
t
,
T
GetListDataKOPDnResul
t
,
TGetListData
TableDn
Result
,
T
PostDnReques
t
,
TPost
Upload
,
TCancel
Nr
Request
,
TCancel
Nr
Response
,
T
CountryResul
t
,
T
DeleteNrReques
t
,
TGetListData
KOPNr
Result
,
T
GetListDataTableNrResul
t
,
TPost
NrRequest
,
}
from
'
../types/types
'
;
import
unifikasiClient
from
'
./unifikasiClient
'
;
const
dn
Api
=
()
=>
{};
const
nr
Api
=
()
=>
{};
const
axiosCetakPdf
=
axios
.
create
({
baseURL
:
import
.
meta
.
env
.
VITE_APP_BASE_API_URL_CETAK
,
...
...
@@ -22,11 +23,11 @@ const axiosCetakPdf = axios.create({
});
// API untuk get list table
dnApi
.
getDn
=
async
(
config
:
any
)
=>
{
nrApi
.
getNr
=
async
(
config
:
any
)
=>
{
const
{
data
:
{
status
,
message
,
metaPage
,
data
},
data
:
{
message
,
metaPage
,
data
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
get
<
TBaseResponseAPI
<
TGetListDataTable
DnResult
>
>
('IF_TXR_028/bpu
',
{
}
=
await
unifikasiClient
.
get
<
TBaseResponseAPI
<
TGetListDataTable
NrResult
>
>
('IF_TXR_029/
',
{
...
config
,
}
);
...
...
@@ -37,9 +38,24 @@ dnApi.getDn = async (config: any) => {
return
{
total
:
metaPage
?
Number
(
metaPage
.
totalRow
)
:
0
,
data
}
;
};
dnApi.getKodeObjekPajakDn = async (params?: Record
<
string
,
any
>
) =
>
{
const
response
=
await
unifikasiClient
.
get
<
TBaseResponseAPI
<
TGetListDataKOPDnResult
>
>
(
'/sandbox/mst_kop_bpu',
nrApi.getKodeObjekPajakNr = async (params?: Record
<
string
,
any
>
) =
>
{
const
response
=
await
unifikasiClient
.
get
<
TBaseResponseAPI
<
TGetListDataKOPNrResult
>
>
(
'/sandbox/mst_kop_bpnr',
{
params
}
);
const body = response.data;
if (response.status !== 200 || body.status !== 'success')
{
throw
new
Error
(
body
.
message
);
}
return body;
}
;
nrApi.getCountryNr = async (params?: Record
<
string
,
any
>
) =
>
{
const
response
=
await
unifikasiClient
.
get
<
TBaseResponseAPI
<
TCountryResult
>
>
(
'/sandbox/mst_negara',
{
params
}
);
...
...
@@ -52,11 +68,10 @@ dnApi.getKodeObjekPajakDn = async (params?: Record<string, any>) => {
return body;
}
;
dnApi.saveDn = async (config: TPostDn
Request) =
>
{
nrApi.saveNr = async (config: TPostNr
Request) =
>
{
const
{
data
:
{
status
,
message
,
data
,
code
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
post
<
TBaseResponseAPI
<
TPostDnRequest
>>
(
'
/IF_TXR_028/bpu
'
,
{
data
:
{
message
,
data
,
code
},
}
=
await
unifikasiClient
.
post
<
TBaseResponseAPI
<
TPostNrRequest
>>
(
'
/IF_TXR_029/
'
,
{
...
config
,
});
if
(
code
===
0
)
{
...
...
@@ -66,8 +81,8 @@ dnApi.saveDn = async (config: TPostDnRequest) => {
return
data
;
}
;
dnApi.getDn
ById = async (id: string) =
>
{
const
res
=
await
unifikasiClient
.
get
(
'
/IF_TXR_02
8/bpu
'
,
{
params
:
{
id
}
});
nrApi.getNr
ById = async (id: string) =
>
{
const
res
=
await
unifikasiClient
.
get
(
'
/IF_TXR_02
9/
'
,
{
params
:
{
id
}
});
const
{
data
:
{
status
,
message
,
data
},
...
...
@@ -75,8 +90,8 @@ dnApi.getDnById = async (id: string) => {
}
=
res
;
if
(
statusCode
!==
200
||
status
?.
toLowerCase
()
!==
'
success
'
)
{
console
.
error
(
'
get
DnBy
Id failed:
'
,
{
statusCode
,
status
,
message
});
throw
new
Error
(
message
||
'
Gagal mengambil data
DN
'
);
console
.
error
(
'
get
Nr
Id failed:
'
,
{
statusCode
,
status
,
message
});
throw
new
Error
(
message
||
'
Gagal mengambil data
NR
'
);
}
const
dnData
=
Array
.
isArray
(
data
)
?
data
[
0
]
:
data
;
...
...
@@ -84,40 +99,39 @@ dnApi.getDnById = async (id: string) => {
return
dnData
;
}
;
dn
Api.upload = async (
{
id
}
:
{
id
:
string
|
number
}
) =
>
{
nr
Api.upload = async (
{
id
}
:
{
id
:
string
|
number
}
) =
>
{
const
{
data
:
{
status
,
message
,
data
,
code
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
post
(
'
/IF_TXR_02
8/bpu
/upload
'
,
{
id
});
}
=
await
unifikasiClient
.
post
(
'
/IF_TXR_02
9
/upload
'
,
{
id
});
return
{
status
,
message
,
data
,
code
,
statusCode
};
}
;
dnApi.deleteDn = async (payload: TDeleteDn
Request, config?: Record
<
string
,
any
>
): Promise
<
any
>
=
>
{
nrApi.deleteNr = async (payload: TDeleteNr
Request, config?: Record
<
string
,
any
>
): Promise
<
any
>
=
>
{
const
{
data
:
{
status
,
message
,
data
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
post
<
TBaseResponseAPI
<
any
>>
(
'
/IF_TXR_02
8/bpu
/delete
'
,
payload
,
{
}
=
await
unifikasiClient
.
post
<
TBaseResponseAPI
<
any
>>
(
'
/IF_TXR_02
9
/delete
'
,
payload
,
{
...
config
,
});
if
(
statusCode
!==
200
||
status
?.
toLowerCase
()
===
'
error
'
)
{
throw
new
Error
(
message
||
'
Gagal menghapus data
DN
'
);
throw
new
Error
(
message
||
'
Gagal menghapus data
NR
'
);
}
return
data
;
}
;
dnApi.cancel = async (
{
id
,
tglPembatalan
}
: TCancelDnRequest): Promise
<
TCancelDn
Response
>
=
>
{
nrApi.cancel = async (
{
id
,
tglPembatalan
}
: TCancelNrRequest): Promise
<
TCancelNr
Response
>
=
>
{
const
{
data
:
{
status
,
message
,
data
,
code
,
time
,
metaPage
,
total
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
post
(
'
/IF_TXR_028/bpu/batal
'
,
{
}
=
await
unifikasiClient
.
post
(
'
/IF_TXR_029/batal
'
,
{
id
,
tglPembatalan
,
});
console
.
log
(
'
Cancel
DN
response:
'
,
{
code
,
message
,
status
});
console
.
log
(
'
Cancel
NR
response:
'
,
{
code
,
message
,
status
});
if
(
code
===
0
)
{
throw
new
Error
(
message
||
'
Gagal membatalkan data
'
);
}
...
...
@@ -133,8 +147,8 @@ dnApi.cancel = async ({ id, tglPembatalan }: TCancelDnRequest): Promise<TCancelD
};
}
;
dn
Api.cetakPdfDetail = async (payload: Record
<
string
,
any
>
) =
>
{
const
response
=
await
axiosCetakPdf
.
post
(
'
/report/ctas/bp
u
'
,
payload
);
nr
Api.cetakPdfDetail = async (payload: Record
<
string
,
any
>
) =
>
{
const
response
=
await
axiosCetakPdf
.
post
(
'
/report/ctas/bp
nr
'
,
payload
);
const
body
=
response
.
data
;
...
...
@@ -153,4 +167,4 @@ dnApi.cetakPdfDetail = async (payload: Record<string, any>) => {
return
body
;
}
;
export default
dn
Api;
export default
nr
Api;
src/sections/bupot-unifikasi/bupot-nr/utils/normalizePayloadCetakPdf.ts
View file @
d89056f3
...
...
@@ -43,11 +43,11 @@ export const normalizePayloadCetakPdf = (payload: Record<string, any>) => {
adjusted
.
metodePembayaranBendahara
=
adjusted
.
metodePembayaranBendahara
||
'
-
'
;
adjusted
.
nomorSP2D
=
adjusted
.
nomorSP2D
||
'
-
'
;
adjusted
.
npwpDipotong
=
adjusted
.
npwp
||
''
;
adjusted
.
namaDipotong
=
adjusted
.
nama
||
''
;
adjusted
.
namaDipotong
=
adjusted
.
nama
Dipotong
||
''
;
adjusted
.
nitkuDipotong
=
adjusted
.
nik
||
''
;
adjusted
.
namaPemotong
=
adjusted
.
nama
||
''
;
adjusted
.
nitkuPemotong
=
adjusted
.
nik
||
''
;
adjusted
.
penghasilanBruto
=
adjusted
.
dpp
||
''
;
adjusted
.
namaPemotong
=
adjusted
.
nama
Dipotong
||
''
;
adjusted
.
nitkuPemotong
=
adjusted
.
idTku
||
''
;
adjusted
.
penghasilanBruto
=
adjusted
.
penghasilanBruto
||
''
;
adjusted
.
tanggal_Dokumen
=
adjusted
.
dokumen_referensi
[
0
].
tanggal_Dokumen
;
adjusted
.
status
=
'
Proforma
'
;
adjusted
.
msPajak
=
adjusted
.
masaPajak
;
...
...
src/sections/bupot-unifikasi/bupot-nr/view/index.ts
x
→
src/sections/bupot-unifikasi/bupot-nr/view/index.ts
View file @
d89056f3
File moved
src/sections/bupot-unifikasi/bupot-nr/view/nr-list-view.tsx
View file @
d89056f3
...
...
@@ -29,18 +29,20 @@ import {
// import { CustomToolbar } from '../components/CustomToolbar';
import
{
formatRupiah
}
from
'
src/shared/FormatRupiah/FormatRupiah
'
;
import
{
FG_STATUS_DN
}
from
'
../constant
'
;
import
ModalDelete
Dn
from
'
../components/dialog/ModalDeleteDn
'
;
import
ModalUpload
Dn
from
'
../components/dialog/ModalUploadDn
'
;
import
ModalCancel
Dn
from
'
../components/dialog/ModalCancelDn
'
;
import
ModalCetakPdf
Dn
from
'
../components/dialog/ModalCetakPdfDn
'
;
import
useGetDn
from
'
../hooks/useGetDn
'
;
import
ModalDelete
Nr
from
'
../components/dialog/ModalDeleteNr
'
;
import
ModalUpload
Nr
from
'
../components/dialog/ModalUploadNr
'
;
import
ModalCancel
Nr
from
'
../components/dialog/ModalCancelNr
'
;
import
ModalCetakPdf
Nr
from
'
../components/dialog/ModalCetakPdfNr
'
;
import
{
useGetNr
}
from
'
../hooks/useGetNr
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
{
usePaginationStore
}
from
'
../store/paginationStore
'
;
import
StatusChip
from
'
../components/StatusChip
'
;
import
{
useDebounce
,
useThrottle
}
from
'
src/shared/hooks/useDebounceThrottle
'
;
import
useGetKodeObjekPajak
from
'
../hooks/useGetKodeObjekPajak
'
;
import
useAdvancedFilter
from
'
../hooks/useAdvancedFilter
Dn
'
;
import
useGetKodeObjekPajak
from
'
../hooks/useGetKodeObjekPajak
Nr
'
;
import
useAdvancedFilter
from
'
../hooks/useAdvancedFilter
Nr
'
;
import
{
CustomToolbar
}
from
'
../components/CustomToolbar
'
;
import
{
useSelector
}
from
'
react-redux
'
;
import
type
{
RootState
}
from
'
src/store
'
;
export
type
IColumnGrid
=
GridColDef
&
{
field
:
...
...
@@ -48,13 +50,14 @@ export type IColumnGrid = GridColDef & {
|
'
noBupot
'
|
'
masaPajak
'
|
'
tahunPajak
'
|
'
k
dObjPj
k
'
|
'
k
odeObjekPaja
k
'
|
'
pasalPPh
'
|
'
npwp
'
|
'
nama
'
|
'
dpp
'
|
'
npwp
Pemotong
'
|
'
nama
Dipotong
'
|
'
penghasilanBruto
'
|
'
pphDipotong
'
|
'
idTku
'
|
'
negaraDipotong
'
|
'
dokReferensi
'
|
'
nomorDokumen
'
|
'
created_by
'
...
...
@@ -75,13 +78,14 @@ type TKodeObjekPajak = {
nama
:
string
;
pasal
:
string
;
statuspph
:
string
;
normanetto
:
string
;
};
export
function
NrListView
()
{
const
apiRef
=
useGridApiRef
();
const
navigate
=
useNavigate
();
const
tableKey
=
'
dn
'
;
const
tableKey
=
'
nr
'
;
const
page
=
usePaginationStore
((
s
)
=>
s
.
tables
[
tableKey
]?.
page
??
0
);
const
pageSize
=
usePaginationStore
((
s
)
=>
s
.
tables
[
tableKey
]?.
pageSize
??
10
);
...
...
@@ -104,7 +108,8 @@ export function NrListView() {
const
[
selectionVersion
,
setSelectionVersion
]
=
useState
(
0
);
const
[
kodeObjekPajaks
,
setKodeObjekPajaks
]
=
useState
<
TKodeObjekPajak
[]
>
([]);
const
{
data
:
kodeObjekPajak
,
isLoading
:
isLoadingKop
}
=
useGetKodeObjekPajak
();
const
{
data
:
kodeObjekPajak
}
=
useGetKodeObjekPajak
();
const
signer
=
useSelector
((
state
:
RootState
)
=>
state
.
user
.
data
.
signer
);
const
{
buildAdvancedFilter
,
buildRequestParams
}
=
useAdvancedFilter
();
...
...
@@ -127,7 +132,7 @@ export function NrListView() {
return
buildRequestParams
(
baseParams
,
advanced
);
},
[
page
,
pageSize
,
sortModel
,
filterModel
.
items
,
buildAdvancedFilter
,
buildRequestParams
]);
const
{
data
,
isFetching
,
refetch
}
=
useGet
Dn
({
const
{
data
,
isFetching
,
refetch
}
=
useGet
Nr
({
params
,
});
const
idStatusMapRef
=
useRef
<
Map
<
string
|
number
,
string
>>
(
new
Map
());
...
...
@@ -174,6 +179,7 @@ export function NrListView() {
// ---------- status options and columns (kept identical to your original) ----------
type
Status
=
'
draft
'
|
'
normal
'
|
'
cancelled
'
|
'
amended
'
;
type
StatusOption
=
{
value
:
Status
;
label
:
string
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const
statusOptions
:
StatusOption
[]
=
[
{
value
:
'
draft
'
,
label
:
'
Draft
'
},
{
value
:
'
normal
'
,
label
:
'
Normal
'
},
...
...
@@ -198,14 +204,24 @@ export function NrListView() {
{
field
:
'
noBupot
'
,
headerName
:
'
Nomor Bukti Pemotongan
'
,
width
:
200
},
{
field
:
'
masaPajak
'
,
headerName
:
'
Masa Pajak
'
,
width
:
150
},
{
field
:
'
tahunPajak
'
,
headerName
:
'
Tahun Pajak
'
,
width
:
150
},
{
field
:
'
k
dObjPj
k
'
,
headerName
:
'
Kode Objek Pajak
'
,
width
:
150
},
{
field
:
'
npwp
'
,
headerName
:
'
Identitas
'
,
width
:
150
},
{
field
:
'
nama
'
,
headerName
:
'
Nama
'
,
width
:
150
},
{
field
:
'
k
odeObjekPaja
k
'
,
headerName
:
'
Kode Objek Pajak
'
,
width
:
150
},
{
field
:
'
npwp
Pemotong
'
,
headerName
:
'
Identitas
'
,
width
:
150
},
{
field
:
'
nama
Dipotong
'
,
headerName
:
'
Nama
'
,
width
:
150
},
{
field
:
'
dpp
'
,
field
:
'
negaraDipotong
'
,
headerName
:
'
Negara
'
,
width
:
180
,
renderCell
:
({
row
})
=>
{
const
kode
=
row
.
negaraDipotong
||
'
-
'
;
const
nama
=
row
.
namaNegara
||
''
;
return
`
${
kode
}${
nama
?
'
-
'
+
nama
:
''
}
`
;
},
},
{
field
:
'
penghasilanBruto
'
,
headerName
:
'
Jumlah Penghasilan Bruto (Rp)
'
,
width
:
150
,
renderCell
:
({
row
})
=>
formatRupiah
(
row
.
dpp
),
renderCell
:
({
row
})
=>
formatRupiah
(
row
.
penghasilanBruto
),
},
{
field
:
'
pphDipotong
'
,
...
...
@@ -217,7 +233,7 @@ export function NrListView() {
{
field
:
'
dokReferensi
'
,
headerName
:
'
Nama dokumen
'
,
width
:
150
},
{
field
:
'
nomorDokumen
'
,
headerName
:
'
Nomor dokumen
'
,
width
:
150
},
{
field
:
'
created_by
'
,
headerName
:
'
Created
'
,
width
:
150
},
{
field
:
'
created_at
'
,
headerName
:
'
Created At
'
,
width
:
20
0
},
{
field
:
'
created_at
'
,
headerName
:
'
Created At
'
,
width
:
15
0
},
{
field
:
'
updated_by
'
,
headerName
:
'
Updated
'
,
width
:
150
},
{
field
:
'
updated_at
'
,
headerName
:
'
Update At
'
,
width
:
150
},
{
field
:
'
internal_id
'
,
headerName
:
'
Referensi
'
,
width
:
150
},
...
...
@@ -243,7 +259,7 @@ export function NrListView() {
(
type
=
'
ubah
'
)
=>
{
const
selectedRow
=
dataSelectedRef
.
current
[
0
];
if
(
!
selectedRow
)
return
;
navigate
(
`/unifikasi/
dn
/
${
selectedRow
.
id
}
/
${
type
}
`
);
navigate
(
`/unifikasi/
nr
/
${
selectedRow
.
id
}
/
${
type
}
`
);
},
[
navigate
]
);
...
...
@@ -305,6 +321,7 @@ export function NrListView() {
canReplacement
:
count
===
1
&&
dataSelected
[
0
].
fgStatus
===
FG_STATUS_DN
.
NORMAL_DONE
,
canCancel
:
hasSelection
&&
allNormal
,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
selectionVersion
]);
useEffect
(()
=>
{
...
...
@@ -321,9 +338,13 @@ export function NrListView() {
return
;
}
console
.
log
(
'
🧾 selectedRow:
'
,
selectedRow
);
console
.
log
(
'
🧩 Keys:
'
,
Object
.
keys
(
selectedRow
));
const
kode
=
selectedRow
.
kodeObjekPajak
||
selectedRow
.
kdObjPjk
;
const
detailKop
=
kodeObjekPajaks
.
find
((
item
)
=>
item
.
kode
===
kode
);
console
.
log
(
detailKop
);
const
mergedRow
=
{
...
selectedRow
,
...(
detailKop
...
...
@@ -331,8 +352,11 @@ export function NrListView() {
namaObjekPajak
:
detailKop
.
nama
,
pasalPPh
:
detailKop
.
pasal
,
statusPPh
:
detailKop
.
statuspph
,
normaPenghasilanNeto
:
detailKop
.
normanetto
,
}
:
{}),
tinDipotong
:
selectedRow
.
npwpPemotong
??
''
,
namaPenandatangan
:
signer
,
};
setPreviewPayload
(
mergedRow
);
...
...
@@ -391,6 +415,7 @@ export function NrListView() {
},
],
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[
validatedActions
,
refetch
,
handleEditData
]
);
...
...
@@ -416,20 +441,25 @@ export function NrListView() {
const
api
=
apiRef
.
current
;
if
(
!
api
)
return
;
const
id
=
window
.
setTimeout
(()
=>
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const
selected
=
getSelectedRowByKey
(
'
all
'
);
},
100
);
// eslint-disable-next-line consistent-return
return
()
=>
clearTimeout
(
id
);
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
apiRef
,
selectionVersion
]);
return
(
<>
<
DashboardContent
>
<
CustomBreadcrumbs
heading=
"Bupot Unifikasi"
links=
{
[{
name
:
'
Dashboard
'
,
href
:
paths
.
dashboard
.
root
},
{
name
:
'
e-Bupot Unifikasi
'
}]
}
heading=
"Bupot Unifikasi Non Residen"
links=
{
[
{
name
:
'
Dashboard
'
,
href
:
paths
.
dashboard
.
root
},
{
name
:
'
e-Bupot Unifikasi Non Residen
'
},
]
}
action=
{
<
Button
component=
{
RouterLink
}
href=
{
paths
.
unifikasi
.
dn
New
}
variant=
"contained"
>
<
Button
component=
{
RouterLink
}
href=
{
paths
.
unifikasi
.
nr
New
}
variant=
"contained"
>
Rekam Data
</
Button
>
}
...
...
@@ -487,7 +517,7 @@ export function NrListView() {
</
DashboardContent
>
{
isDeleteModalOpen
&&
(
<
ModalDelete
Dn
<
ModalDelete
Nr
dataSelected=
{
rowSelectionModel
}
setSelectionModel=
{
setRowSelectionModel
}
tableApiRef=
{
apiRef
}
...
...
@@ -498,7 +528,7 @@ export function NrListView() {
)
}
{
isUploadModalOpen
&&
(
<
ModalUpload
Dn
<
ModalUpload
Nr
dataSelected=
{
rowSelectionModel
}
setSelectionModel=
{
setRowSelectionModel
}
tableApiRef=
{
apiRef
}
...
...
@@ -509,7 +539,7 @@ export function NrListView() {
)
}
{
isCancelModalOpen
&&
(
<
ModalCancel
Dn
<
ModalCancel
Nr
dataSelected=
{
dataSelectedRef
.
current
}
setSelectionModel=
{
setRowSelectionModel
}
tableApiRef=
{
apiRef
}
...
...
@@ -520,7 +550,7 @@ export function NrListView() {
)
}
{
isPreviewOpen
&&
(
<
ModalCetakPdf
Dn
<
ModalCetakPdf
Nr
payload=
{
previewPayload
}
isOpen=
{
isPreviewOpen
}
onClose=
{
()
=>
{
...
...
src/sections/bupot-unifikasi/bupot-nr/view/nrRekamView.tsx
View file @
d89056f3
import
{
zodResolver
}
from
'
@hookform/resolvers/zod
'
;
import
{
LoadingButton
}
from
'
@mui/lab
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
{
useState
,
useEffect
}
from
'
react
'
;
import
{
FormProvider
,
useForm
}
from
'
react-hook-form
'
;
import
{
CustomBreadcrumbs
}
from
'
src/components/custom-breadcrumbs
'
;
import
{
DashboardContent
}
from
'
src/layouts/dashboard
'
;
import
{
paths
}
from
'
src/routes/paths
'
;
import
HeadingRekam
from
'
src/shared/components/HeadingRekam
'
;
import
z
from
'
zod
'
;
import
Identitas
from
'
../components/rekamNr/Identitas
'
;
import
Divider
from
'
@mui/material/Divider
'
;
import
FormSkeleton
from
'
src/shared/skeletons/FormSkeleton
'
;
import
PphDipotong
from
'
../components/rekamNr/PphDipotong
'
;
import
useGetKodeObjekPajak
from
'
../hooks/useGetKodeObjekPajakNr
'
;
import
DokumenReferensi
from
'
../components/rekamNr/DokumenReferensi
'
;
import
Agreement
from
'
src/shared/components/agreement/Agreement
'
;
import
Stack
from
'
@mui/material/Stack
'
;
import
PanduanDnRekam
from
'
../components/rekamNr/PanduanDnRekam
'
;
import
useSaveNr
from
'
../hooks/useSaveNr
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
{
useNavigate
,
useParams
}
from
'
react-router
'
;
import
useUpload
from
'
../hooks/useUpload
'
;
import
ModalUploadNr
from
'
../components/dialog/ModalUploadNr
'
;
import
{
useGetNrById
}
from
'
../hooks/useGetNr
'
;
import
useGetNegara
from
'
../hooks/useGetNegara
'
;
const
bpuNrSchema
=
z
.
object
({
tglPemotongan
:
z
.
string
().
nonempty
(
'
Tanggal Pemotongan harus diisi
'
),
thnPajak
:
z
.
string
().
nonempty
(
'
Tahun Pajak harus diisi
'
),
masaPajak
:
z
.
string
().
nonempty
(
'
Masa Pajak harus diisi
'
),
idDipotong
:
z
.
string
().
nonempty
(
'
TIN harus diisi
'
),
alamatDipotong
:
z
.
string
().
nonempty
(
'
Alamat harus diisi
'
),
namaDipotong
:
z
.
string
().
nonempty
(
'
Nama harus diisi
'
),
negaraDipotong
:
z
.
string
().
nonempty
(
'
Negara harus diisi
'
),
tmptLahirDipotong
:
z
.
string
().
nonempty
(
'
Tempat Lahir harus diisi
'
),
tglLahirDipotong
:
z
.
string
().
nonempty
(
'
Tanggal Lahir harus diisi
'
),
nomorPaspor
:
z
.
string
().
nonempty
(
'
No Paspor harus diisi
'
),
nomorKitasKitap
:
z
.
string
().
nonempty
(
'
No.KITAS/KITAP harus diisi
'
),
keterangan1
:
z
.
string
().
optional
(),
keterangan2
:
z
.
string
().
optional
(),
keterangan3
:
z
.
string
().
optional
(),
keterangan4
:
z
.
string
().
optional
(),
keterangan5
:
z
.
string
().
optional
(),
kodeObjekPajak
:
z
.
string
().
nonempty
(
'
Kode Objek Pajak harus diisi
'
),
fgFasilitas
:
z
.
string
().
nonempty
(
'
Fasilitas harus diisi
'
),
noDokLainnya
:
z
.
string
().
optional
(),
penghasilanBruto
:
z
.
string
().
nonempty
(
'
Jumlah Penghasilan Bruto harus diisi
'
),
normaPenghasilanNeto
:
z
.
string
().
nonempty
(
'
Perkiraan Penghasilan Netto (%) harus diisi
'
),
tarif
:
z
.
union
([
z
.
string
().
nonempty
(
'
Tarif harus diisi
'
),
z
.
number
()]),
pphDipotong
:
z
.
string
().
nonempty
(
'
PPh Yang Dipotong/Dipungut harus diisi
'
),
namaDok
:
z
.
string
().
nonempty
(
'
Nama Dokumen harus diisi
'
),
nomorDok
:
z
.
string
().
nonempty
(
'
Nomor Dokumen harus diisi
'
),
tglDok
:
z
.
string
().
nonempty
(
'
Tanggal Dokumen harus diisi
'
),
idTku
:
z
.
string
().
nonempty
(
'
Cabang harus diisi
'
),
})
.
superRefine
((
data
,
ctx
)
=>
{
// Field dianggap DISABLED kalau fgFasilitas kosong ('') atau '9'
const
isDisabled
=
[
''
,
'
9
'
].
includes
(
data
.
fgFasilitas
);
// Jika tidak disabled, berarti aktif → wajib isi
if
(
!
isDisabled
&&
(
!
data
.
noDokLainnya
||
data
.
noDokLainnya
.
trim
()
===
''
))
{
ctx
.
addIssue
({
path
:
[
'
noDokLainnya
'
],
code
:
'
custom
'
,
message
:
'
No Dokumen Lainnya harus diisi
'
,
});
}
});
const
NrRekamView
=
()
=>
{
const
{
id
,
type
}
=
useParams
<
{
id
?:
string
;
type
?:
'
ubah
'
|
'
pengganti
'
|
'
new
'
}
>
();
const
[
isOpenPanduan
,
setIsOpenPanduan
]
=
useState
<
boolean
>
(
false
);
const
[
isCheckedAgreement
,
setIsCheckedAgreement
]
=
useState
<
boolean
>
(
false
);
const
[
isFormPrefilled
,
setIsFormPrefilled
]
=
useState
<
boolean
>
(
false
);
const
[
formInitialized
,
setFormInitialized
]
=
useState
<
boolean
>
(
false
);
const
[
isUploadModalOpen
,
setIsUploadModalOpen
]
=
useState
<
boolean
>
(
false
);
const
isEdit
=
type
===
'
ubah
'
;
const
isPengganti
=
type
===
'
pengganti
'
;
const
{
data
:
existingNr
,
isLoading
:
isLoadingDn
}
=
useGetNrById
(
id
!
,
{
enabled
:
!!
id
&&
(
isEdit
||
isPengganti
),
});
const
{
data
:
country
=
[]
}
=
useGetNegara
();
const
{
data
:
kodeObjekPajak
,
isLoading
:
isLoadingKop
}
=
useGetKodeObjekPajak
();
const
navigate
=
useNavigate
();
type
BpuNrFormData
=
z
.
infer
<
typeof
bpuNrSchema
>
;
const
saveNr
=
useSaveNr
({
onSuccess
:
()
=>
enqueueSnackbar
(
'
Data berhasil disimpan
'
,
{
variant
:
'
success
'
}),
});
const
uploadNr
=
useUpload
();
const
handleOpenPanduan
=
()
=>
setIsOpenPanduan
(
!
isOpenPanduan
);
const
defaultValues
:
BpuNrFormData
=
{
tglPemotongan
:
''
,
thnPajak
:
''
,
masaPajak
:
''
,
idDipotong
:
''
,
namaDipotong
:
''
,
alamatDipotong
:
''
,
// ✅ disamakan
negaraDipotong
:
''
,
// ✅ disamakan
tmptLahirDipotong
:
''
,
// ✅ disamakan
tglLahirDipotong
:
''
,
nomorPaspor
:
''
,
// ✅ disamakan
nomorKitasKitap
:
''
,
// ✅ disamakan
keterangan1
:
''
,
keterangan2
:
''
,
keterangan3
:
''
,
keterangan4
:
''
,
keterangan5
:
''
,
kodeObjekPajak
:
''
,
// ✅ disamakan
fgFasilitas
:
''
,
noDokLainnya
:
''
,
penghasilanBruto
:
''
,
normaPenghasilanNeto
:
''
,
// ✅ disamakan
tarif
:
''
,
pphDipotong
:
''
,
namaDok
:
''
,
nomorDok
:
''
,
tglDok
:
''
,
idTku
:
''
,
};
const
methods
=
useForm
<
BpuNrFormData
>
({
mode
:
'
all
'
,
resolver
:
zodResolver
(
bpuNrSchema
),
defaultValues
,
});
const
{
reset
,
handleSubmit
}
=
methods
;
useEffect
(()
=>
{
if
(
isEdit
||
isPengganti
)
{
if
(
existingNr
&&
!
isLoadingKop
)
{
// 🧩 Normalisasi nilai numeric ke string sebelum reset
const
normalized
=
{
...
existingNr
,
jmlBruto
:
existingNr
.
penghasilanBruto
!==
null
&&
existingNr
.
penghasilanBruto
!==
undefined
?
String
(
existingNr
.
penghasilanBruto
)
:
''
,
tarif
:
existingNr
.
tarif
!==
null
&&
existingNr
.
tarif
!==
undefined
?
String
(
existingNr
.
tarif
)
:
''
,
pphDipotong
:
existingNr
.
pphDipotong
!==
null
&&
existingNr
.
pphDipotong
!==
undefined
?
String
(
existingNr
.
pphDipotong
)
:
''
,
masaPajak
:
existingNr
.
msPajak
!==
null
&&
existingNr
.
msPajak
!==
undefined
?
String
(
existingNr
.
msPajak
)
:
''
,
namaDok
:
existingNr
.
namaDok
??
''
,
nomorDok
:
existingNr
.
nomorDok
??
''
,
tglDok
:
existingNr
.
tglDok
??
''
,
};
reset
(
normalized
);
setIsFormPrefilled
(
true
);
setFormInitialized
(
true
);
}
}
else
{
setIsFormPrefilled
(
false
);
setFormInitialized
(
true
);
}
},
[
existingNr
,
isLoadingKop
,
isEdit
,
isPengganti
,
reset
]);
const
buildPayload
=
(
values
:
BpuNrFormData
)
=>
{
if
(
isEdit
)
{
return
{
...
values
,
id
,
isPengganti
:
false
,
};
}
if
(
isPengganti
)
{
return
{
...
values
,
id
,
isPengganti
:
true
,
idBupot
:
existingNr
?.
idBupot
,
noBupot
:
existingNr
?.
noBupot
,
revNo
:
existingNr
?.
revNo
??
0
,
};
}
return
{
...
values
,
isPengganti
:
false
,
};
};
const
onSubmit
=
async
(
values
:
BpuNrFormData
)
=>
{
console
.
log
(
'
ini ke klik
'
);
try
{
const
payload
=
buildPayload
(
values
);
await
saveNr
.
mutateAsync
(
payload
);
enqueueSnackbar
(
isEdit
?
'
Data berhasil diperbarui
'
:
isPengganti
?
'
Data pengganti berhasil disimpan
'
:
'
Data berhasil disimpan
'
,
{
variant
:
'
success
'
}
);
navigate
(
'
/unifikasi/nr
'
);
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
.
message
||
'
Gagal menyimpan data
'
,
{
variant
:
'
error
'
});
console
.
error
(
'
❌ saveNr error:
'
,
error
);
}
};
const
handleSaveAndUpload
=
async
(
values
:
BpuNrFormData
)
=>
{
try
{
const
payload
=
buildPayload
(
values
);
const
res
:
any
=
await
saveNr
.
mutateAsync
(
payload
);
const
savedId
=
res
?.[
0
]?.
id
;
if
(
!
savedId
)
throw
new
Error
(
'
ID dokumen tidak ditemukan dari hasil save
'
);
await
uploadNr
.
mutateAsync
({
id
:
savedId
});
enqueueSnackbar
(
'
Data berhasil disimpan dan diupload
'
,
{
variant
:
'
success
'
});
navigate
(
'
/unifikasi/nr
'
);
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
.
message
||
'
Gagal save & upload data
'
,
{
variant
:
'
error
'
});
console
.
error
(
'
❌ Upload error:
'
,
error
);
}
};
if
(
isLoadingDn
||
isLoadingKop
||
!
formInitialized
)
{
return
(
<
DashboardContent
>
<
CustomBreadcrumbs
heading=
{
isEdit
?
'
Ubah Bupot Unifikasi
'
:
isPengganti
?
'
Bupot Pengganti
'
:
'
Tambah Bupot Unifikasi
'
}
links=
{
[
{
name
:
'
Dashboard
'
,
href
:
paths
.
dashboard
.
root
},
{
name
:
'
e-Bupot Non Residen
'
,
href
:
paths
.
unifikasi
.
nr
},
{
name
:
isEdit
?
'
Ubah
'
:
isPengganti
?
'
Pengganti
'
:
'
Tambah
'
},
]
}
/>
<
HeadingRekam
label=
{
isEdit
?
'
Ubah Data Bukti Pemotongan/Pemungutan PPh Non Residen
'
:
isPengganti
?
'
Rekam Bukti Pengganti PPh Non Residen
'
:
'
Rekam Data Bukti Potong PPh Non Residen
'
}
/>
<
FormSkeleton
numberOfRows=
{
8
}
/>
</
DashboardContent
>
);
}
return
(
<
DashboardContent
>
<
CustomBreadcrumbs
heading=
{
isEdit
?
'
Ubah Bupot Non Residen
'
:
isPengganti
?
'
Bupot Non Residen Pengganti
'
:
'
Tambah Bupot Non Residen
'
}
links=
{
[
{
name
:
'
Dashboard
'
,
href
:
paths
.
dashboard
.
root
},
{
name
:
'
e-Bupot Non Residen
'
,
href
:
paths
.
unifikasi
.
nr
},
{
name
:
isEdit
?
'
Ubah
'
:
isPengganti
?
'
Pengganti
'
:
'
Tambah
'
},
]
}
/>
<
HeadingRekam
label=
{
isEdit
?
'
Ubah Data Bukti Pemotongan/Pemungutan PPh Non Residen
'
:
isPengganti
?
'
Rekam Bukti Pengganti PPh Non Residen
'
:
'
Rekam Data Bukti Potong PPh Non Residen
'
}
/>
<
Grid
container
columnSpacing=
{
2
}
>
<
Grid
size=
{
{
xs
:
isOpenPanduan
?
8
:
11
}
}
>
<
FormProvider
{
...
methods
}
>
<
form
onSubmit=
{
handleSubmit
(
onSubmit
)
}
>
<
Identitas
isPengganti=
{
isPengganti
}
existingNr=
{
existingNr
}
country=
{
country
}
/>
<
Divider
/>
<
PphDipotong
kodeObjectPajak=
{
kodeObjekPajak
?.
data
??
[]
}
isFormPrefilled=
{
isFormPrefilled
}
/>
<
DokumenReferensi
/>
<
Divider
/>
<
Grid
size=
{
12
}
>
<
Agreement
isCheckedAgreement=
{
isCheckedAgreement
}
setIsCheckedAgreement=
{
setIsCheckedAgreement
}
text=
"Dengan ini saya menyatakan bahwa Bukti Pemotongan/Pemungutan Unifikasi telah saya isi dengan benar secara elektronik sesuai dengan"
/>
</
Grid
>
<
Stack
direction=
"row"
gap=
{
2
}
justifyContent=
"end"
marginTop=
{
2
}
>
{
/* <LoadingButton
type="submit"
loading={saveNr.isPending}
disabled={!isCheckedAgreement}
variant="outlined"
sx={{ color: '#143B88' }}
>
Save as Draft
</LoadingButton> */
}
<
LoadingButton
type=
"button"
loading=
{
saveNr
.
isPending
}
disabled=
{
!
isCheckedAgreement
}
variant=
"outlined"
sx=
{
{
color
:
'
#143B88
'
}
}
onClick=
{
()
=>
{
console
.
log
(
'
🟢 Save as Draft clicked
'
);
methods
.
handleSubmit
(
async
(
values
)
=>
{
console
.
log
(
'
✅ Form valid, calling onSubmit...
'
);
await
onSubmit
(
values
);
},
(
errors
)
=>
{
console
.
error
(
'
❌ Validation errors:
'
,
errors
);
}
)();
}
}
>
Save as Draft
</
LoadingButton
>
<
LoadingButton
type=
"button"
disabled=
{
!
isCheckedAgreement
}
// onClick={() => methods.handleSubmit(handleSaveAndUpload)()}
onClick=
{
()
=>
setIsUploadModalOpen
(
true
)
}
loading=
{
uploadNr
.
isPending
}
variant=
"contained"
sx=
{
{
background
:
'
#143B88
'
}
}
>
Save and Upload
</
LoadingButton
>
</
Stack
>
</
form
>
</
FormProvider
>
</
Grid
>
<
Grid
size=
{
{
xs
:
isOpenPanduan
?
4
:
1
}
}
>
<
PanduanDnRekam
handleOpen=
{
handleOpenPanduan
}
isOpen=
{
isOpenPanduan
}
/>
</
Grid
>
</
Grid
>
{
isUploadModalOpen
&&
(
<
ModalUploadNr
isOpenDialogUpload=
{
isUploadModalOpen
}
setIsOpenDialogUpload=
{
setIsUploadModalOpen
}
onConfirmUpload=
{
()
=>
handleSaveAndUpload
(
methods
.
getValues
())
}
/>
)
}
</
DashboardContent
>
);
};
export
default
NrRekamView
;
src/sections/bupot-unifikasi/bupot-nr/workers/normalizeNr.worker.js
0 → 100644
View file @
d89056f3
// src/workers/normalizeDn.worker.js
// NOTE: keep this file plain JS - no TS imports - copy needed transform functions here.
function
formatDateToDDMMYYYY
(
dateString
)
{
if
(
!
dateString
)
return
''
;
const
d
=
new
Date
(
dateString
);
const
day
=
String
(
d
.
getDate
()).
padStart
(
2
,
'
0
'
);
const
month
=
String
(
d
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
);
const
year
=
d
.
getFullYear
();
return
`
${
day
}
/
${
month
}
/
${
year
}
`
;
}
// minimal transform helpers used in normalize
function
transformFgStatusToFgSignStatus
(
fgStatus
)
{
const
splitted
=
(
fgStatus
||
''
).
split
(
'
-
'
)
||
[];
if
(
splitted
.
includes
(
'
SIGN
'
)
>
0
)
return
'
FAILED
'
;
if
(
splitted
.
includes
(
'
SIGNING IN PROGRESS
'
))
return
'
IN_PROGRESS
'
;
switch
(
splitted
[
1
])
{
case
'
document signed successfully
'
:
case
'
Done
'
:
return
'
SIGNED
'
;
default
:
return
null
;
}
}
function
getFgStatusPdf
(
link
,
fgSignStatus
)
{
if
(
!
link
||
fgSignStatus
===
'
IN_PROGRESS
'
)
return
'
TIDAK_TERSEDIA
'
;
if
(
!
link
.
includes
(
'
https://coretaxdjp.pajak.go.id/
'
))
return
'
BELUM_TERBENTUK
'
;
return
'
TERBENTUK
'
;
}
function
normalisePropsGetDn
(
params
)
{
if
(
!
params
)
return
params
;
return
{
...
params
,
nomorSP2D
:
params
.
dokumen_referensi
?.[
0
]?.
nomorSP2D
||
''
,
metodePembayaranBendahara
:
params
.
dokumen_referensi
?.[
0
]?.
metodePembayaranBendahara
||
''
,
dokReferensi
:
params
.
dokumen_referensi
?.[
0
]?.
dokReferensi
||
''
,
nomorDokumen
:
params
.
dokumen_referensi
?.[
0
]?.
nomorDokumen
||
''
,
id
:
params
.
id
,
internal_id
:
params
.
internal_id
,
fgStatus
:
params
.
fgStatus
,
fgSignStatus
:
transformFgStatusToFgSignStatus
(
params
.
fgStatus
),
fgPdf
:
getFgStatusPdf
(
params
.
link
,
transformFgStatusToFgSignStatus
(
params
.
fgStatus
)),
created_at
:
formatDateToDDMMYYYY
(
params
.
created_at
),
updated_at
:
formatDateToDDMMYYYY
(
params
.
updated_at
),
};
}
// eslint-disable-next-line func-names
onmessage
=
function
(
e
)
{
const
{
data
}
=
e
;
// data should be array of items
if
(
!
Array
.
isArray
(
data
))
{
postMessage
({
error
:
'
expected array
'
});
return
;
}
try
{
const
out
=
data
.
map
(
normalisePropsGetDn
);
postMessage
({
data
:
out
});
}
catch
(
err
)
{
postMessage
({
error
:
(
err
&&
err
.
message
)
||
String
(
err
)
});
}
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment