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
9549ebd4
Commit
9549ebd4
authored
Nov 19, 2025
by
Rais Aryaguna
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix tahunan A1
add Bupot Pasal 26
parent
5dae0e7c
Changes
42
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
42 changed files
with
5165 additions
and
63 deletions
+5165
-63
src/pages/pph21/bupotPasal26.tsx
src/pages/pph21/bupotPasal26.tsx
+2
-1
src/pages/pph21/bupotPasal26Rekam.tsx
src/pages/pph21/bupotPasal26Rekam.tsx
+14
-0
src/routes/paths.ts
src/routes/paths.ts
+3
-1
src/routes/sections/dashboard.tsx
src/routes/sections/dashboard.tsx
+3
-0
src/sections/bupot-21-26/bupot-26/components/CustomColumnsButton.tsx
...s/bupot-21-26/bupot-26/components/CustomColumnsButton.tsx
+33
-0
src/sections/bupot-21-26/bupot-26/components/CustomFilterButton.tsx
...ns/bupot-21-26/bupot-26/components/CustomFilterButton.tsx
+509
-0
src/sections/bupot-21-26/bupot-26/components/CustomToolbar.tsx
...ections/bupot-21-26/bupot-26/components/CustomToolbar.tsx
+69
-0
src/sections/bupot-21-26/bupot-26/components/StatusChip.tsx
src/sections/bupot-21-26/bupot-26/components/StatusChip.tsx
+127
-0
src/sections/bupot-21-26/bupot-26/components/dialog/CancelConfirmationDialog.tsx
...6/bupot-26/components/dialog/CancelConfirmationDialog.tsx
+40
-0
src/sections/bupot-21-26/bupot-26/components/dialog/ModalCancel.tsx
...ns/bupot-21-26/bupot-26/components/dialog/ModalCancel.tsx
+232
-0
src/sections/bupot-21-26/bupot-26/components/dialog/ModalCetakPdf.tsx
.../bupot-21-26/bupot-26/components/dialog/ModalCetakPdf.tsx
+96
-0
src/sections/bupot-21-26/bupot-26/components/dialog/ModalDelete.tsx
...ns/bupot-21-26/bupot-26/components/dialog/ModalDelete.tsx
+152
-0
src/sections/bupot-21-26/bupot-26/components/dialog/ModalUpload.tsx
...ns/bupot-21-26/bupot-26/components/dialog/ModalUpload.tsx
+192
-0
src/sections/bupot-21-26/bupot-26/components/rekamNr/DokumenReferensi.tsx
...ot-21-26/bupot-26/components/rekamNr/DokumenReferensi.tsx
+94
-0
src/sections/bupot-21-26/bupot-26/components/rekamNr/Identitas.tsx
...ons/bupot-21-26/bupot-26/components/rekamNr/Identitas.tsx
+132
-0
src/sections/bupot-21-26/bupot-26/components/rekamNr/PanduanNrRekam.tsx
...upot-21-26/bupot-26/components/rekamNr/PanduanNrRekam.tsx
+159
-0
src/sections/bupot-21-26/bupot-26/components/rekamNr/PphDipotong.tsx
...s/bupot-21-26/bupot-26/components/rekamNr/PphDipotong.tsx
+141
-0
src/sections/bupot-21-26/bupot-26/components/toolbarCancel.tsx
...ections/bupot-21-26/bupot-26/components/toolbarCancel.tsx
+40
-0
src/sections/bupot-21-26/bupot-26/constant/index.tsx
src/sections/bupot-21-26/bupot-26/constant/index.tsx
+766
-0
src/sections/bupot-21-26/bupot-26/hooks/useAdvancedFilter.tsx
...sections/bupot-21-26/bupot-26/hooks/useAdvancedFilter.tsx
+170
-0
src/sections/bupot-21-26/bupot-26/hooks/useCancel.tsx
src/sections/bupot-21-26/bupot-26/hooks/useCancel.tsx
+13
-0
src/sections/bupot-21-26/bupot-26/hooks/useCetakPdf.tsx
src/sections/bupot-21-26/bupot-26/hooks/useCetakPdf.tsx
+12
-0
src/sections/bupot-21-26/bupot-26/hooks/useDelete.tsx
src/sections/bupot-21-26/bupot-26/hooks/useDelete.tsx
+13
-0
src/sections/bupot-21-26/bupot-26/hooks/useGetBupot26.tsx
src/sections/bupot-21-26/bupot-26/hooks/useGetBupot26.tsx
+286
-0
src/sections/bupot-21-26/bupot-26/hooks/usePphDipotong.tsx
src/sections/bupot-21-26/bupot-26/hooks/usePphDipotong.tsx
+77
-0
src/sections/bupot-21-26/bupot-26/hooks/useSave.tsx
src/sections/bupot-21-26/bupot-26/hooks/useSave.tsx
+123
-0
src/sections/bupot-21-26/bupot-26/hooks/useUpload.tsx
src/sections/bupot-21-26/bupot-26/hooks/useUpload.tsx
+13
-0
src/sections/bupot-21-26/bupot-26/store/paginationStore.ts
src/sections/bupot-21-26/bupot-26/store/paginationStore.ts
+64
-0
src/sections/bupot-21-26/bupot-26/types/types.ts
src/sections/bupot-21-26/bupot-26/types/types.ts
+150
-0
src/sections/bupot-21-26/bupot-26/utils/api.tsx
src/sections/bupot-21-26/bupot-26/utils/api.tsx
+139
-0
src/sections/bupot-21-26/bupot-26/utils/normalizePayloadCetakPdf.ts
...ns/bupot-21-26/bupot-26/utils/normalizePayloadCetakPdf.ts
+61
-0
src/sections/bupot-21-26/bupot-26/utils/unifikasiClient.tsx
src/sections/bupot-21-26/bupot-26/utils/unifikasiClient.tsx
+28
-0
src/sections/bupot-21-26/bupot-26/utils/utils.tsx
src/sections/bupot-21-26/bupot-26/utils/utils.tsx
+19
-0
src/sections/bupot-21-26/bupot-26/view/bupot-26-list-view.tsx
...sections/bupot-21-26/bupot-26/view/bupot-26-list-view.tsx
+607
-0
src/sections/bupot-21-26/bupot-26/view/bupot-26-rekam-view.tsx
...ections/bupot-21-26/bupot-26/view/bupot-26-rekam-view.tsx
+450
-0
src/sections/bupot-21-26/bupot-26/view/index.ts
src/sections/bupot-21-26/bupot-26/view/index.ts
+2
-0
src/sections/bupot-21-26/bupot-26/workers/normalizeNr.worker.js
...ctions/bupot-21-26/bupot-26/workers/normalizeNr.worker.js
+65
-0
src/sections/bupot-21-26/bupot-a1/components/rekam/Identitas.tsx
...tions/bupot-21-26/bupot-a1/components/rekam/Identitas.tsx
+24
-17
src/sections/bupot-21-26/bupot-a1/components/rekam/PerhitunganA1.tsx
...s/bupot-21-26/bupot-a1/components/rekam/PerhitunganA1.tsx
+22
-35
src/sections/bupot-21-26/bupot-a1/components/rekam/RincianPenghasilan.tsx
...ot-21-26/bupot-a1/components/rekam/RincianPenghasilan.tsx
+6
-6
src/sections/bupot-21-26/bupot-a1/view/tahunan-a1-rekam-view.tsx
...tions/bupot-21-26/bupot-a1/view/tahunan-a1-rekam-view.tsx
+7
-3
src/sections/bupot-21-26/constant/queryKey.tsx
src/sections/bupot-21-26/constant/queryKey.tsx
+10
-0
No files found.
src/pages/pph21/bupotPasal26.tsx
View file @
9549ebd4
import
{
CONFIG
}
from
'
src/global-config
'
;
import
{
Bupot26ListView
}
from
'
src/sections/bupot-21-26/bupot-26/view
'
const
metadata
=
{
title
:
`E-Bupot 21/26-
${
CONFIG
.
appName
}
`
};
...
...
@@ -7,7 +8,7 @@ export default function Page() {
<>
<
title
>
{
metadata
.
title
}
</
title
>
<
p
>
Bupot Pasal 26
</
p
>
<
Bupot26ListView
/
>
</>
);
}
src/pages/pph21/bupotPasal26Rekam.tsx
0 → 100644
View file @
9549ebd4
import
{
CONFIG
}
from
'
src/global-config
'
;
import
{
Bupot26RekamView
}
from
'
src/sections/bupot-21-26/bupot-26/view
'
const
metadata
=
{
title
:
`E-Bupot 21/26-
${
CONFIG
.
appName
}
`
};
export
default
function
Page
()
{
return
(
<>
<
title
>
{
metadata
.
title
}
</
title
>
<
Bupot26RekamView
/>
</>
);
}
src/routes/paths.ts
View file @
9549ebd4
...
...
@@ -122,7 +122,9 @@ export const paths = {
tahunanA2
:
`
${
ROOTS
.
PPH21
}
/tahunan-a2`
,
detailstahunanA2
:
(
id
:
string
)
=>
`
${
ROOTS
.
PPH21
}
/tahunan-a2/
${
id
}
`
,
bupot26
:
`
${
ROOTS
.
PPH21
}
/bupot-26`
,
detailsbupot26
:
(
id
:
string
)
=>
`
${
ROOTS
.
PPH21
}
/bupot-26/
${
id
}
`
,
bupot26Rekam
:
`
${
ROOTS
.
PPH21
}
/bupot-26/rekam`
,
bupot26Edit
:
(
id
:
string
,
path
:
string
)
=>
`
${
ROOTS
.
PPH21
}
/bupot-26/
${
id
}
/
${
path
}
`
,
},
// DASHBOARD
dashboard
:
{
...
...
src/routes/sections/dashboard.tsx
View file @
9549ebd4
...
...
@@ -65,6 +65,7 @@ const OverviewBupotFinalTdkFinalRekamPage = lazy(
const
OverviewBupotA1Page
=
lazy
(()
=>
import
(
'
src/pages/pph21/bupotTahunanA1
'
));
const
OverviewBupotA1RekamPage
=
lazy
(()
=>
import
(
'
src/pages/pph21/bupotTahunanA1Rekam
'
));
const
OverviewBupotPasal26Page
=
lazy
(()
=>
import
(
'
src/pages/pph21/bupotPasal26
'
));
const
OverviewBupotPasal26RekamPage
=
lazy
(()
=>
import
(
'
src/pages/pph21/bupotPasal26Rekam
'
));
// ----------------------------------------------------------------------
...
...
@@ -155,6 +156,8 @@ export const dashboardRoutes: RouteObject[] = [
{
path
:
'
tahunan/rekam
'
,
element
:<
OverviewBupotA1RekamPage
/>},
{
path
:
'
tahunan/:id/:type
'
,
element
:<
OverviewBupotA1RekamPage
/>},
{
path
:
'
bupot-26
'
,
element
:
<
OverviewBupotPasal26Page
/>
},
{
path
:
'
bupot-26/rekam
'
,
element
:
<
OverviewBupotPasal26RekamPage
/>
},
{
path
:
'
bupot-26/:id/:type
'
,
element
:
<
OverviewBupotPasal26RekamPage
/>
},
],
},
];
src/sections/bupot-21-26/bupot-26/components/CustomColumnsButton.tsx
0 → 100644
View file @
9549ebd4
import
React
from
'
react
'
;
import
type
{
GridPreferencePanelsValue
}
from
'
@mui/x-data-grid-premium
'
;
import
{
useGridApiContext
}
from
'
@mui/x-data-grid-premium
'
;
import
{
IconButton
,
Tooltip
}
from
'
@mui/material
'
;
import
ViewColumnIcon
from
'
@mui/icons-material/ViewColumn
'
;
// ✅ React.memo: cegah render ulang tanpa alasan
const
CustomColumnsButton
:
React
.
FC
=
React
.
memo
(()
=>
{
const
apiRef
=
useGridApiContext
();
// ✅ useCallback biar referensi handleClick stabil di setiap render
const
handleClick
=
React
.
useCallback
(()
=>
{
if
(
!
apiRef
.
current
)
return
;
apiRef
.
current
.
showPreferences
(
'
columns
'
as
GridPreferencePanelsValue
);
},
[
apiRef
]);
return
(
<
Tooltip
title=
"Kolom"
>
<
IconButton
size=
"small"
onClick=
{
handleClick
}
sx=
{
{
color
:
'
#123375
'
,
'
&:hover
'
:
{
backgroundColor
:
'
rgba(18, 51, 117, 0.08)
'
},
}
}
>
<
ViewColumnIcon
fontSize=
"small"
/>
</
IconButton
>
</
Tooltip
>
);
});
export
default
CustomColumnsButton
;
src/sections/bupot-21-26/bupot-26/components/CustomFilterButton.tsx
0 → 100644
View file @
9549ebd4
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-26/components/CustomToolbar.tsx
0 → 100644
View file @
9549ebd4
import
*
as
React
from
'
react
'
;
import
type
{
GridToolbarProps
}
from
'
@mui/x-data-grid-premium
'
;
import
{
GridToolbarContainer
}
from
'
@mui/x-data-grid-premium
'
;
import
{
Stack
,
Divider
,
IconButton
,
Tooltip
}
from
'
@mui/material
'
;
import
type
{
ActionItem
}
from
'
../types/types
'
;
import
{
CustomFilterButton
}
from
'
./CustomFilterButton
'
;
import
CustomColumnsButton
from
'
./CustomColumnsButton
'
;
interface
CustomToolbarProps
extends
GridToolbarProps
{
actions
?:
ActionItem
[][];
columns
:
any
[];
// GridColDef[]
filterModel
:
any
;
setFilterModel
:
(
m
:
any
)
=>
void
;
statusOptions
?:
{
value
:
string
;
label
:
string
}[];
}
// ✅ React.memo mencegah render ulang kalau props sama
export
const
CustomToolbar
=
React
.
memo
(
function
CustomToolbar
({
actions
=
[],
columns
,
filterModel
,
setFilterModel
,
statusOptions
=
[],
...
gridToolbarProps
}:
CustomToolbarProps
)
{
return
(
<
GridToolbarContainer
sx=
{
{
display
:
'
flex
'
,
justifyContent
:
'
space-between
'
,
alignItems
:
'
center
'
,
p
:
1.5
,
}
}
{
...
gridToolbarProps
}
>
<
Stack
direction=
"row"
alignItems=
"center"
gap=
{
1
}
>
{
actions
.
map
((
group
,
groupIdx
)
=>
(
<
Stack
key=
{
groupIdx
}
direction=
"row"
gap=
{
0.5
}
alignItems=
"center"
>
{
group
.
map
((
action
,
idx
)
=>
(
<
Tooltip
key=
{
idx
}
title=
{
action
.
title
}
>
<
span
>
<
IconButton
sx=
{
{
color
:
action
.
disabled
?
'
action.disabled
'
:
'
#123375
'
}
}
size=
"small"
onClick=
{
action
.
func
}
disabled=
{
action
.
disabled
}
>
{
action
.
icon
}
</
IconButton
>
</
span
>
</
Tooltip
>
))
}
{
groupIdx
<
actions
.
length
-
1
&&
<
Divider
orientation=
"vertical"
flexItem
/>
}
</
Stack
>
))
}
</
Stack
>
<
Stack
direction=
"row"
alignItems=
"center"
gap=
{
0.5
}
>
<
CustomColumnsButton
/>
<
CustomFilterButton
columns=
{
columns
}
filterModel=
{
filterModel
}
setFilterModel=
{
setFilterModel
}
statusOptions=
{
statusOptions
}
/>
</
Stack
>
</
GridToolbarContainer
>
);
});
src/sections/bupot-21-26/bupot-26/components/StatusChip.tsx
0 → 100644
View file @
9549ebd4
import
React
from
'
react
'
;
import
Chip
from
'
@mui/material/Chip
'
;
import
Box
from
'
@mui/material/Box
'
;
type
Props
=
{
value
?:
string
;
revNo
?:
number
};
const
StatusChip
:
React
.
FC
<
Props
>
=
({
value
,
revNo
})
=>
{
if
(
!
value
)
return
<
Chip
label=
""
size=
"small"
/>;
if
(
value
===
'
NORMAL-Done
'
&&
revNo
!==
0
)
{
return
(
<
Box
sx=
{
{
position
:
'
relative
'
,
display
:
'
inline-flex
'
,
alignItems
:
'
center
'
,
}
}
>
<
Chip
label=
"Normal Pengganti"
size=
"small"
variant=
"outlined"
sx=
{
{
borderColor
:
'
#1976d2
'
,
color
:
'
#1976d2
'
,
borderRadius
:
'
8px
'
,
fontWeight
:
500
,
paddingRight
:
'
5px
'
,
}
}
/>
<
Chip
label=
{
revNo
}
size=
"small"
variant=
"filled"
sx=
{
{
position
:
'
absolute
'
,
top
:
-
6
,
right
:
-
6
,
backgroundColor
:
'
#1976d2
'
,
color
:
'
#fff
'
,
borderRadius
:
'
50%
'
,
fontWeight
:
500
,
width
:
18
,
height
:
18
,
minWidth
:
0
,
border
:
'
2px solid #fff
'
,
boxShadow
:
'
0 1px 3px rgba(0, 0, 0, 0.25)
'
,
'
& .MuiChip-label
'
:
{
padding
:
0
,
fontSize
:
'
0.65rem
'
,
lineHeight
:
1
,
},
}
}
/>
</
Box
>
);
}
if
(
value
===
'
NORMAL-Done
'
&&
revNo
===
0
)
{
return
(
<
Chip
label=
"Normal"
size=
"small"
variant=
"outlined"
sx=
{
{
borderColor
:
'
#1976d2
'
,
color
:
'
#1976d2
'
,
borderRadius
:
'
8px
'
,
fontWeight
:
'
500
'
,
}
}
/>
);
}
if
(
value
===
'
AMENDED
'
)
{
return
(
<
Chip
label=
"Diganti"
size=
"small"
variant=
"outlined"
sx=
{
{
color
:
'
#fff
'
,
backgroundColor
:
'
#f38c28
'
,
borderRadius
:
'
8px
'
,
fontWeight
:
500
,
border
:
'
none
'
,
boxShadow
:
'
0 1px 2px rgba(0, 0, 0, 0.15)
'
,
}
}
/>
);
}
if
(
value
===
'
CANCELLED
'
)
{
return
(
<
Chip
label=
"Dibatalkan"
size=
"small"
variant=
"outlined"
sx=
{
{
borderColor
:
'
#d32f2f
'
,
color
:
'
#d32f2f
'
,
borderRadius
:
'
8px
'
,
fontWeight
:
'
500
'
,
}
}
/>
);
}
if
(
value
===
'
DRAFT
'
)
{
return
(
<
Chip
label=
"Draft"
size=
"small"
variant=
"outlined"
sx=
{
{
borderColor
:
'
#9e9e9e
'
,
color
:
'
#616161
'
,
borderRadius
:
'
8px
'
,
}
}
/>
);
}
return
<
Chip
label=
{
value
}
size=
"small"
/>;
};
export
default
React
.
memo
(
StatusChip
);
src/sections/bupot-21-26/bupot-26/components/dialog/CancelConfirmationDialog.tsx
0 → 100644
View file @
9549ebd4
import
React
from
'
react
'
;
import
{
Dialog
,
DialogTitle
,
DialogContent
,
DialogActions
,
Button
,
Typography
,
}
from
'
@mui/material
'
;
interface
CancelConfirmationDialogProps
{
open
:
boolean
;
onClose
:
()
=>
void
;
onConfirm
:
()
=>
void
;
selectedCount
:
number
;
}
const
CancelConfirmationDialog
:
React
.
FC
<
CancelConfirmationDialogProps
>
=
({
open
,
onClose
,
onConfirm
,
selectedCount
,
})
=>
(
<
Dialog
open=
{
open
}
onClose=
{
onClose
}
>
<
DialogTitle
>
Konfirmasi Pembatalan
</
DialogTitle
>
<
DialogContent
>
<
Typography
>
Apakah Anda yakin ingin membatalkan
{
selectedCount
}
data yang dipilih?
</
Typography
>
</
DialogContent
>
<
DialogActions
>
<
Button
onClick=
{
onClose
}
>
Batal
</
Button
>
<
Button
onClick=
{
onConfirm
}
color=
"error"
variant=
"contained"
>
Ya, Batalkan
</
Button
>
</
DialogActions
>
</
Dialog
>
);
export
default
CancelConfirmationDialog
;
src/sections/bupot-21-26/bupot-26/components/dialog/ModalCancel.tsx
0 → 100644
View file @
9549ebd4
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
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
'
;
import
type
{
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
ModalCancelNrProps
{
dataSelected
?:
any
[];
setSelectionModel
?:
React
.
Dispatch
<
React
.
SetStateAction
<
GridRowSelectionModel
|
undefined
>>
;
tableApiRef
?:
React
.
MutableRefObject
<
any
>
;
isOpenDialogCancel
:
boolean
;
setIsOpenDialogCancel
:
(
v
:
boolean
)
=>
void
;
successMessage
?:
string
;
onConfirmCancel
?:
()
=>
Promise
<
void
>
|
void
;
}
const
ModalCancelNr
:
React
.
FC
<
ModalCancelNrProps
>
=
({
dataSelected
=
[],
setSelectionModel
,
tableApiRef
,
isOpenDialogCancel
,
setIsOpenDialogCancel
,
successMessage
=
'
Data berhasil dibatalkan
'
,
onConfirmCancel
,
})
=>
{
const
queryClient
=
useQueryClient
();
const
[
tglPembatalan
,
setTglPembatalan
]
=
useState
<
Dayjs
|
null
>
(
null
);
const
[
isOpenDialogProgressBar
,
setIsOpenDialogProgressBar
]
=
useState
(
false
);
const
{
numberOfData
,
setNumberOfData
,
numberOfDataFail
,
numberOfDataProcessed
,
numberOfDataSuccess
,
processSuccess
,
processFail
,
resetToDefault
,
status
,
}
=
useDialogProgressBar
();
const
{
mutateAsync
}
=
useCancelDn
({
onSuccess
:
()
=>
processSuccess
(),
onError
:
()
=>
processFail
(),
});
// ✅ update jumlah data di progress bar
useEffect
(()
=>
{
setNumberOfData
(
dataSelected
?.
length
??
0
);
},
[
dataSelected
,
setNumberOfData
]);
// ✅ Ambil tanggal pemotongan paling awal (untuk minDate)
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
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
,
feature
:
'
26
'
}))
);
const
rejected
=
results
.
filter
((
r
)
=>
r
.
status
===
'
rejected
'
);
const
success
=
results
.
filter
((
r
)
=>
r
.
status
===
'
fulfilled
'
);
// ✅ tampilkan pesan error detail
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
();
}
if
(
success
.
length
>
0
)
{
enqueueSnackbar
(
successMessage
,
{
variant
:
'
success
'
});
processSuccess
();
await
onConfirmCancel
?.();
}
// ✅ update cache data lokal agar status langsung berubah
queryClient
.
setQueryData
([
'
unifikasi
'
,
'
dn
'
],
(
old
:
any
)
=>
{
if
(
!
old
?.
data
)
return
old
;
return
{
...
old
,
data
:
old
.
data
.
map
((
row
:
any
)
=>
ids
.
includes
(
String
(
row
.
id
))
?
{
...
row
,
fgStatus
:
'
CANCELLED
'
}
:
row
),
};
});
// ✅ refetch data agar sinkron
await
queryClient
.
invalidateQueries
({
queryKey
:
[
'
unifikasi
'
,
'
dn
'
]
});
// ⚠️ Tidak perlu clearSelection di sini — DnListView akan sync otomatis lewat rowsSet
handleCloseModal
();
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
?.
message
||
'
Gagal membatalkan data
'
,
{
variant
:
'
error
'
});
processFail
();
}
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
()
}
minDate=
{
minPembatalanDate
||
undefined
}
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
ModalCancelNr
;
src/sections/bupot-21-26/bupot-26/components/dialog/ModalCetakPdf.tsx
0 → 100644
View file @
9549ebd4
import
React
,
{
useEffect
,
useState
}
from
'
react
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
DialogUmum
from
'
src/shared/components/dialog/DialogUmum
'
;
import
DialogContent
from
'
@mui/material/DialogContent
'
;
import
CircularProgress
from
'
@mui/material/CircularProgress
'
;
import
Box
from
'
@mui/material/Box
'
;
import
useCetakPdfDn
from
'
../../hooks/useCetakPdf
'
;
import
normalizePayloadCetakPdf
from
'
../../utils/normalizePayloadCetakPdf
'
;
interface
ModalCetakPdfDnProps
{
payload
?:
Record
<
string
,
any
>
;
isOpen
:
boolean
;
onClose
:
()
=>
void
;
}
const
ModalCetakPdfNr
:
React
.
FC
<
ModalCetakPdfDnProps
>
=
({
payload
,
isOpen
,
onClose
})
=>
{
const
[
pdfUrl
,
setPdfUrl
]
=
useState
<
string
|
null
>
(
null
);
const
[
loading
,
setLoading
]
=
useState
<
boolean
>
(
false
);
const
{
mutateAsync
}
=
useCetakPdfDn
({
onError
:
(
error
:
any
)
=>
{
enqueueSnackbar
(
error
?.
message
||
'
Gagal memuat PDF
'
,
{
variant
:
'
error
'
});
setLoading
(
false
);
},
onSuccess
:
(
res
:
any
)
=>
{
const
fileUrl
=
res
?.
url
||
res
?.
data
?.
url
;
if
(
!
fileUrl
)
{
enqueueSnackbar
(
'
URL PDF tidak ditemukan di respons API
'
,
{
variant
:
'
warning
'
});
setLoading
(
false
);
return
;
}
setPdfUrl
(
fileUrl
);
setLoading
(
false
);
enqueueSnackbar
(
res
?.
MsgStatus
||
'
PDF berhasil dibentuk
'
,
{
variant
:
'
success
'
});
},
});
useEffect
(()
=>
{
const
runCetak
=
async
()
=>
{
if
(
!
isOpen
||
!
payload
)
return
;
setLoading
(
true
);
setPdfUrl
(
null
);
try
{
// Payload sudah lengkap dari parent (sudah ada namaObjekPajak, pasalPPh, statusPPh)
const
normalized
=
normalizePayloadCetakPdf
(
payload
);
console
.
log
(
'
📤 Payload final cetak PDF:
'
,
normalized
);
await
mutateAsync
(
normalized
);
}
catch
(
err
)
{
console
.
error
(
'
❌ Error cetak PDF:
'
,
err
);
enqueueSnackbar
(
'
Gagal generate PDF
'
,
{
variant
:
'
error
'
});
setLoading
(
false
);
}
};
runCetak
();
},
[
isOpen
,
payload
,
mutateAsync
]);
return
(
<
DialogUmum
maxWidth=
"lg"
isOpen=
{
isOpen
}
onClose=
{
onClose
}
title=
"Detail Bupot Unifikasi (PDF)"
>
<
DialogContent
classes=
{
{
root
:
'
p-16 sm:p-24
'
}
}
>
{
loading
&&
(
<
Box
display=
"flex"
justifyContent=
"center"
alignItems=
"center"
height=
"60vh"
>
<
CircularProgress
/>
</
Box
>
)
}
{
!
loading
&&
pdfUrl
&&
(
<
iframe
src=
{
pdfUrl
}
style=
{
{
width
:
'
100%
'
,
height
:
'
80vh
'
,
border
:
'
none
'
,
borderRadius
:
8
,
}
}
title=
"Preview PDF Bupot"
/>
)
}
{
!
loading
&&
!
pdfUrl
&&
(
<
Box
textAlign=
"center"
color=
"text.secondary"
py=
{
4
}
>
PDF tidak tersedia untuk ditampilkan.
</
Box
>
)
}
</
DialogContent
>
</
DialogUmum
>
);
};
export
default
ModalCetakPdfNr
;
src/sections/bupot-21-26/bupot-26/components/dialog/ModalDelete.tsx
0 → 100644
View file @
9549ebd4
import
React
,
{
useEffect
,
useState
}
from
'
react
'
;
import
{
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
DialogProgressBar
from
'
src/shared/components/dialog/DialogProgressBar
'
;
import
useDialogProgressBar
from
'
src/shared/hooks/useDialogProgressBar
'
;
import
DialogConfirm
from
'
src/shared/components/dialog/DialogConfirm
'
;
import
type
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
useDeleteBupot26
from
'
../../hooks/useDelete
'
;
import
queryKey
from
'
src/sections/bupot-21-26/constant/queryKey
'
;
interface
ModalDeleteDnProps
{
dataSelected
?:
GridRowSelectionModel
;
setSelectionModel
?:
React
.
Dispatch
<
React
.
SetStateAction
<
GridRowSelectionModel
|
undefined
>>
;
tableApiRef
?:
React
.
MutableRefObject
<
any
>
;
isOpenDialogDelete
:
boolean
;
setIsOpenDialogDelete
:
(
v
:
boolean
)
=>
void
;
successMessage
?:
string
;
onConfirmDelete
?:
()
=>
Promise
<
void
>
|
void
;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const
normalizeSelection
=
(
sel
?:
any
):
(
string
|
number
)[]
=>
{
if
(
!
sel
)
return
[];
if
(
Array
.
isArray
(
sel
))
return
sel
as
(
string
|
number
)[];
if
(
sel
instanceof
Set
)
return
Array
.
from
(
sel
)
as
(
string
|
number
)[];
if
(
typeof
sel
===
'
object
'
)
{
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if
(
sel
.
ids
instanceof
Set
)
return
Array
.
from
(
sel
.
ids
)
as
(
string
|
number
)[];
// maybe it's a map-like object { '1': true, '2': true }
const
maybeIds
=
Object
.
keys
(
sel
).
filter
((
k
)
=>
k
!==
'
type
'
&&
k
!==
'
size
'
);
if
(
maybeIds
.
length
>
0
)
{
// try to convert numeric-like keys to number where applicable
return
maybeIds
.
map
((
k
)
=>
{
const
n
=
Number
(
k
);
return
Number
.
isNaN
(
n
)
?
k
:
n
;
});
}
}
return
[];
};
const
ModalDelete
:
React
.
FC
<
ModalDeleteDnProps
>
=
({
dataSelected
,
setSelectionModel
,
tableApiRef
,
isOpenDialogDelete
,
setIsOpenDialogDelete
,
successMessage
=
'
Data berhasil dihapus
'
,
onConfirmDelete
,
})
=>
{
const
queryClient
=
useQueryClient
();
// custom hooks for progress state
const
{
numberOfData
,
setNumberOfData
,
numberOfDataFail
,
numberOfDataProcessed
,
numberOfDataSuccess
,
processSuccess
,
processFail
,
resetToDefault
,
status
,
}
=
useDialogProgressBar
();
const
[
isOpenDialogProgressBar
,
setIsOpenDialogProgressBar
]
=
useState
(
false
);
// React Query mutation for delete
const
{
mutateAsync
}
=
useDeleteBupot26
({
onSuccess
:
()
=>
processSuccess
(),
onError
:
()
=>
processFail
(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const
handleMultipleDelete
=
async
()
=>
{
const
ids
=
normalizeSelection
(
dataSelected
);
return
Promise
.
allSettled
(
ids
.
map
(
async
(
id
)
=>
mutateAsync
({
id
:
String
(
id
)
,
feature
:
'
26
'
})));
};
const
clearSelection
=
()
=>
{
// clear grid selection via apiRef if available
tableApiRef
?.
current
?.
setRowSelectionModel
?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel
?.(
undefined
);
};
const
handleCloseModal
=
()
=>
{
setIsOpenDialogDelete
(
false
);
resetToDefault
();
};
const
onSubmit
=
async
()
=>
{
try
{
setIsOpenDialogProgressBar
(
true
);
await
handleMultipleDelete
();
enqueueSnackbar
(
successMessage
,
{
variant
:
'
success
'
});
await
onConfirmDelete
?.();
handleCloseModal
();
clearSelection
();
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
?.
message
||
'
Gagal menghapus data
'
,
{
variant
:
'
error
'
});
}
finally
{
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient
.
invalidateQueries
({
queryKey
:
queryKey
.
bupot26
.
all
(
''
)
});
}
};
useEffect
(()
=>
{
setNumberOfData
(
normalizeSelection
(
dataSelected
).
length
);
},
[
isOpenDialogDelete
,
dataSelected
,
setNumberOfData
]);
return
(
<>
<
DialogConfirm
fullWidth
maxWidth=
"xs"
title=
"Apakah Anda yakin akan menghapus data ini?"
description=
"Data yang sudah dihapus tidak dapat dikembalikan."
actionTitle=
"Hapus"
isOpen=
{
isOpenDialogDelete
}
isLoadingBtnSubmit=
{
false
}
handleClose=
{
handleCloseModal
}
handleSubmit=
{
onSubmit
}
/>
<
DialogProgressBar
isOpen=
{
isOpenDialogProgressBar
}
handleClose=
{
()
=>
{
handleCloseModal
();
setIsOpenDialogProgressBar
(
false
);
}
}
numberOfData=
{
numberOfData
}
numberOfDataProcessed=
{
numberOfDataProcessed
}
numberOfDataFail=
{
numberOfDataFail
}
numberOfDataSuccess=
{
numberOfDataSuccess
}
status=
{
status
}
/>
</>
);
};
export
default
ModalDelete
;
src/sections/bupot-21-26/bupot-26/components/dialog/ModalUpload.tsx
0 → 100644
View file @
9549ebd4
import
React
,
{
useEffect
,
useState
}
from
'
react
'
;
import
{
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
DialogProgressBar
from
'
src/shared/components/dialog/DialogProgressBar
'
;
import
useDialogProgressBar
from
'
src/shared/hooks/useDialogProgressBar
'
;
import
type
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
useUpload
from
'
../../hooks/useUpload
'
;
import
DialogUmum
from
'
src/shared/components/dialog/DialogUmum
'
;
import
Stack
from
'
@mui/material/Stack
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
import
{
useSelector
}
from
'
react-redux
'
;
import
type
{
RootState
}
from
'
src/store
'
;
import
Agreement
from
'
src/shared/components/agreement/Agreement
'
;
import
{
FormProvider
,
useForm
}
from
'
react-hook-form
'
;
import
Button
from
'
@mui/material/Button
'
;
interface
ModalUploadNrProps
{
dataSelected
?:
GridRowSelectionModel
;
setSelectionModel
?:
React
.
Dispatch
<
React
.
SetStateAction
<
GridRowSelectionModel
|
undefined
>>
;
tableApiRef
?:
React
.
MutableRefObject
<
any
>
;
isOpenDialogUpload
:
boolean
;
setIsOpenDialogUpload
:
(
v
:
boolean
)
=>
void
;
successMessage
?:
string
;
onConfirmUpload
?:
()
=>
Promise
<
void
>
|
void
;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const
normalizeSelection
=
(
sel
?:
any
):
(
string
|
number
)[]
=>
{
if
(
!
sel
)
return
[];
if
(
Array
.
isArray
(
sel
))
return
sel
as
(
string
|
number
)[];
if
(
sel
instanceof
Set
)
return
Array
.
from
(
sel
)
as
(
string
|
number
)[];
if
(
typeof
sel
===
'
object
'
)
{
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if
(
sel
.
ids
instanceof
Set
)
return
Array
.
from
(
sel
.
ids
)
as
(
string
|
number
)[];
// maybe it's a map-like object { '1': true, '2': true }
const
maybeIds
=
Object
.
keys
(
sel
).
filter
((
k
)
=>
k
!==
'
type
'
&&
k
!==
'
size
'
);
if
(
maybeIds
.
length
>
0
)
{
// try to convert numeric-like keys to number where applicable
return
maybeIds
.
map
((
k
)
=>
{
const
n
=
Number
(
k
);
return
Number
.
isNaN
(
n
)
?
k
:
n
;
});
}
}
return
[];
};
const
ModalUploadNr
:
React
.
FC
<
ModalUploadNrProps
>
=
({
dataSelected
,
setSelectionModel
,
tableApiRef
,
isOpenDialogUpload
,
setIsOpenDialogUpload
,
successMessage
=
'
Data berhasil diupload
'
,
onConfirmUpload
,
})
=>
{
const
queryClient
=
useQueryClient
();
const
uploadNr
=
useUpload
();
// custom hooks for progress state
const
{
numberOfData
,
setNumberOfData
,
numberOfDataFail
,
numberOfDataProcessed
,
numberOfDataSuccess
,
processSuccess
,
processFail
,
resetToDefault
,
status
,
}
=
useDialogProgressBar
();
const
[
isOpenDialogProgressBar
,
setIsOpenDialogProgressBar
]
=
useState
(
false
);
const
[
isCheckedAgreement
,
setIsCheckedAgreement
]
=
useState
<
boolean
>
(
false
);
const
signer
=
useSelector
((
state
:
RootState
)
=>
state
.
user
.
data
.
signer
);
const
{
mutateAsync
}
=
useUpload
({
onSuccess
:
()
=>
processSuccess
(),
onError
:
()
=>
processFail
(),
});
const
methods
=
useForm
({
defaultValues
:
{
signer
:
signer
||
''
,
},
});
// fungsi multiple delete -- gunakan normalized array of ids
const
handleMultipleDelete
=
async
()
=>
{
const
ids
=
normalizeSelection
(
dataSelected
);
return
Promise
.
allSettled
(
ids
.
map
(
async
(
id
)
=>
mutateAsync
({
id
:
String
(
id
)
})));
};
const
clearSelection
=
()
=>
{
// clear grid selection via apiRef if available
tableApiRef
?.
current
?.
setRowSelectionModel
?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel
?.(
undefined
);
};
const
handleCloseModal
=
()
=>
{
setIsOpenDialogUpload
(
false
);
resetToDefault
();
};
const
onSubmit
=
async
()
=>
{
try
{
setIsOpenDialogProgressBar
(
true
);
await
handleMultipleDelete
();
enqueueSnackbar
(
successMessage
,
{
variant
:
'
success
'
});
// ✅ refetch langsung setelah sukses
await
onConfirmUpload
?.();
handleCloseModal
();
clearSelection
();
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
?.
message
||
'
Gagal upload data
'
,
{
variant
:
'
error
'
});
}
finally
{
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient
.
invalidateQueries
({
queryKey
:
[
'
unifikasi
'
,
'
nr
'
]
});
}
};
useEffect
(()
=>
{
setNumberOfData
(
normalizeSelection
(
dataSelected
).
length
);
},
[
isOpenDialogUpload
,
dataSelected
,
setNumberOfData
]);
return
(
<>
<
FormProvider
{
...
methods
}
>
<
DialogUmum
isOpen=
{
isOpenDialogUpload
}
onClose=
{
handleCloseModal
}
title=
"Upload Bukti Potong"
>
<
Stack
spacing=
{
2
}
sx=
{
{
mt
:
2
}
}
>
<
Grid
size=
{
{
md
:
12
}
}
>
<
Field
.
Select
name=
"signer"
label=
"NPWP/NIK Penandatangan"
>
<
MenuItem
value=
{
signer
}
>
{
signer
}
</
MenuItem
>
</
Field
.
Select
>
</
Grid
>
<
Grid
size=
{
12
}
>
<
Agreement
isCheckedAgreement=
{
isCheckedAgreement
}
setIsCheckedAgreement=
{
setIsCheckedAgreement
}
text=
"Dengan ini saya menyatakan bahwa Bukti Pemotongan/Pemungutan Unifikasi telah saya isi dengan benar secara elektronik sesuai dengan"
/>
</
Grid
>
<
Stack
direction=
"row"
justifyContent=
"flex-end"
spacing=
{
1
}
mt=
{
1
}
>
<
Button
type=
"button"
disabled=
{
!
isCheckedAgreement
}
onClick=
{
onSubmit
}
loading=
{
uploadNr
.
isPending
}
variant=
"contained"
sx=
{
{
background
:
'
#143B88
'
}
}
>
Save
</
Button
>
</
Stack
>
</
Stack
>
</
DialogUmum
>
</
FormProvider
>
<
DialogProgressBar
isOpen=
{
isOpenDialogProgressBar
}
handleClose=
{
()
=>
{
handleCloseModal
();
setIsOpenDialogProgressBar
(
false
);
}
}
numberOfData=
{
numberOfData
}
numberOfDataProcessed=
{
numberOfDataProcessed
}
numberOfDataFail=
{
numberOfDataFail
}
numberOfDataSuccess=
{
numberOfDataSuccess
}
status=
{
status
}
/>
</>
);
};
export
default
ModalUploadNr
;
src/sections/bupot-21-26/bupot-26/components/rekamNr/DokumenReferensi.tsx
0 → 100644
View file @
9549ebd4
import
Divider
from
'
@mui/material/Divider
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
MenuItem
from
'
@mui/material/MenuItem
'
;
import
dayjs
from
'
dayjs
'
;
import
{
useEffect
}
from
'
react
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
import
{
useSelector
}
from
'
react-redux
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
type
{
RootState
}
from
'
src/store
'
;
export
const
METODE_PEMBAYARAN
=
[
{
value
:
'
DIRECT
'
,
label
:
'
Pembayaran Langsung
'
,
},
{
value
:
'
IMPREST
'
,
label
:
'
Uang Persediaan
'
,
},
];
const
DokumenReferensi
=
({
namaDokOptions
,
}:
{
namaDokOptions
:
{
value
:
string
;
label
:
string
;
}[];
})
=>
{
const
{
watch
,
setValue
}
=
useFormContext
<
Record
<
string
,
any
>>
();
const
nitku
=
useSelector
((
state
:
RootState
)
=>
state
.
user
.
data
.
nitku_trial
);
const
nitkuValue
=
watch
(
'
idTku
'
);
const
isNmrSP2D
=
watch
(
'
metodePembayaranBendahara
'
)
===
'
IMPREST
'
;
const
npwpLog
=
localStorage
.
getItem
(
'
npwp_log
'
)
||
''
;
const
isSp2d
=
npwpLog
.
slice
(
0
,
4
)
===
'
0001
'
;
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
.
Autocomplete
name=
"namaDok"
label=
"Nama Dokumen"
options=
{
namaDokOptions
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"nomorDok"
label=
"Nomor Dokumen"
/>
</
Grid
>
{
isSp2d
&&
(
<>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Autocomplete
name=
"metodePembayaranBendahara"
label=
"Metode Pembayaran SP2D"
options=
{
METODE_PEMBAYARAN
}
disableClearable=
{
false
}
onChange=
{
(
_
,
val
)
=>
{
setValue
(
'
metodePembayaranBendahara
'
,
val
);
setValue
(
'
nomorSP2D
'
,
''
);
}
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Text
name=
"nomorSP2D"
label=
"Nomor SP2D"
disabled=
{
isNmrSP2D
}
/>
</
Grid
>
</>
)
}
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
DatePicker
name=
"tglDok"
label=
"Tanggal Dokumen"
format=
"DD/MM/YYYY"
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-21-26/bupot-26/components/rekamNr/Identitas.tsx
0 → 100644
View file @
9549ebd4
import
Grid
from
'
@mui/material/Grid
'
;
import
dayjs
from
'
dayjs
'
;
import
{
useEffect
}
from
'
react
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
type
{
TCountryResult
}
from
'
../../types/types
'
;
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
}
=
useFormContext
();
const
tanggalPemotongan
=
watch
(
'
tglPemotongan
'
);
// 🧩 Auto isi Tahun & Masa Pajak berdasarkan tanggalPemotongan
useEffect
(()
=>
{
if
(
tanggalPemotongan
)
{
const
date
=
dayjs
(
tanggalPemotongan
);
setValue
(
'
tglDok
'
,
tanggalPemotongan
);
setValue
(
'
thnPajak
'
,
date
.
format
(
'
YYYY
'
));
setValue
(
'
masaPajak
'
,
date
.
format
(
'
MM
'
));
}
else
{
setValue
(
'
tglDok
'
,
''
);
setValue
(
'
thnPajak
'
,
''
);
setValue
(
'
masaPajak
'
,
''
);
}
},
[
tanggalPemotongan
,
setValue
]);
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
/>
</
Grid
>
<
Grid
size=
{
{
md
:
3
}
}
>
<
Field
.
DatePicker
name=
"masaPajak"
label=
"Masa Pajak"
views=
{
[
'
month
'
]
}
// ✅ valid prop
openTo=
"month"
format=
"MM"
disabled
/>
</
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
.
Autocomplete
name=
"negaraDipotong"
label=
"Negara"
options=
{
country
}
/>
</
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
>
</>
);
};
export
default
Identitas
;
src/sections/bupot-21-26/bupot-26/components/rekamNr/PanduanNrRekam.tsx
0 → 100644
View file @
9549ebd4
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_NR
}
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_NR
.
description
.
intro
}
</
Typography
>
<
Typography
variant=
"body2"
>
{
PANDUAN_REKAM_NR
.
description
.
textList
}
</
Typography
>
<
Box
component=
"ol"
sx=
{
{
pl
:
3
,
mb
:
2
}
}
>
{
PANDUAN_REKAM_NR
.
description
.
list
.
map
((
item
,
idx
)
=>
(
<
Typography
key=
{
`desc-${idx}`
}
variant=
"body2"
component=
"li"
>
{
item
}
</
Typography
>
))
}
</
Box
>
<
Typography
variant=
"body2"
sx=
{
{
mb
:
2
}
}
>
{
PANDUAN_REKAM_NR
.
description
.
closing
}
</
Typography
>
{
/* Bagian-bagian */
}
{
PANDUAN_REKAM_NR
.
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-21-26/bupot-26/components/rekamNr/PphDipotong.tsx
0 → 100644
View file @
9549ebd4
import
Divider
from
'
@mui/material/Divider
'
;
import
Grid
from
'
@mui/material/Grid
'
;
import
lodash
from
'
lodash
'
;
import
{
useEffect
}
from
'
react
'
;
import
{
useFormContext
,
useWatch
}
from
'
react-hook-form
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
{
RHFNumeric
}
from
'
src/components/hook-form/rhf-numeric
'
;
import
{
FG_FASILITAS_DN
,
FG_FASILITAS_OPTIONS
,
TARIF_0
}
from
'
../../constant
'
;
type
PPHDipotongProps
=
{
kodeObjectPajak
:
any
[];
fgFasilitasOptions
:
{
value
:
string
;
label
:
string
;
}[];
};
function
roundCustom
(
num
:
number
)
{
const
decimalPart
=
num
-
Math
.
floor
(
num
);
if
(
decimalPart
<=
0.5
)
return
Math
.
trunc
(
num
);
// Bulatkan ke bawah
return
Math
.
round
(
num
);
// Bulatkan ke bilangan bulat terdeka
}
const
PphDipotong
=
({
kodeObjectPajak
,
fgFasilitasOptions
}:
PPHDipotongProps
)
=>
{
const
{
setValue
,
control
}
=
useFormContext
<
Record
<
string
,
any
>>
();
const
perhitunganChanges
=
useWatch
({
control
,
name
:
[
'
penghasilanBruto
'
,
'
tarif
'
,
'
fgFasilitas
'
,
'
normaPenghasilanNeto
'
],
});
const
fgFasilitas
=
perhitunganChanges
[
2
].
value
;
useEffect
(()
=>
{
if
(
perhitunganChanges
.
filter
((
item
)
=>
lodash
.
isEmpty
(
item
)).
length
===
0
)
{
const
dpp
=
Number
(
perhitunganChanges
[
0
]
||
0
);
const
tarif
=
Number
(
perhitunganChanges
[
1
]
||
0
);
const
fasilitas
=
perhitunganChanges
[
2
].
value
;
const
normaPenghasilanNeto
=
Number
(
perhitunganChanges
[
3
]
||
0
);
const
valPphDipotong
=
dpp
*
(
normaPenghasilanNeto
/
100
)
*
(
tarif
/
100
);
const
perhitungan
=
TARIF_0
.
includes
(
fasilitas
)
?
0
:
valPphDipotong
;
setValue
(
'
pphDipotong
'
,
`
${
roundCustom
(
perhitungan
)}
`
,
{
shouldValidate
:
true
,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
perhitunganChanges
]);
return
(
<
Grid
container
rowSpacing=
{
2
}
columnSpacing=
{
2
}
>
<
Grid
sx=
{
{
mt
:
3
}
}
size=
{
{
md
:
6
}
}
>
<
Field
.
Autocomplete
name=
"kodeObjekPajak"
label=
"Kode Objek Pajak"
options=
{
kodeObjectPajak
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
12
}
}
>
<
Divider
sx=
{
{
fontWeight
:
'
bold
'
}
}
textAlign=
"left"
>
Fasilitas Pajak Penghasilan
</
Divider
>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
<
Field
.
Autocomplete
name=
"fgFasilitas"
label=
"Fasilitas"
options=
{
fgFasilitasOptions
}
onChange=
{
(
_
,
val
)
=>
{
setValue
(
'
fgFasilitas
'
,
val
);
if
(
val
.
value
!==
FG_FASILITAS_OPTIONS
.
FASILITAS_LAINNYA
)
setValue
(
'
normaPenghasilanNeto
'
,
'
100
'
);
if
(
val
.
value
===
FG_FASILITAS_OPTIONS
.
TANPA_FASILITAS
)
setValue
(
'
noDokLainnya
'
,
''
);
}
}
/>
</
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
}
}
>
<
RHFNumeric
name=
"normaPenghasilanNeto"
label=
"Perkiraan Penghasilan Netto (%)"
allowNegativeValue=
{
false
}
allowDecimalValue=
{
false
}
maxValue=
{
100
}
readOnly=
{
fgFasilitas
!==
FG_FASILITAS_OPTIONS
.
FASILITAS_LAINNYA
}
/>
</
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-21-26/bupot-26/components/toolbarCancel.tsx
0 → 100644
View file @
9549ebd4
import
React
from
'
react
'
;
import
{
IconButton
,
Tooltip
}
from
'
@mui/material
'
;
import
HighlightOffTwoToneIcon
from
'
@mui/icons-material/HighlightOffTwoTone
'
;
interface
ToolbarCancelProps
{
selectedRows
:
any
[];
selectedRowsData
:
any
[];
onCancel
:
(
ids
:
number
[])
=>
void
;
}
const
ToolbarCancel
:
React
.
FC
<
ToolbarCancelProps
>
=
({
selectedRows
,
selectedRowsData
,
onCancel
,
})
=>
{
// Logic sederhana
const
isEnabled
=
selectedRows
.
length
>
0
&&
selectedRowsData
.
every
((
row
:
any
)
=>
row
.
fgStatus
===
'
normal
'
||
row
.
fgStatus
===
'
amendment
'
);
const
handleClick
=
()
=>
{
if
(
!
isEnabled
)
return
;
const
ids
=
selectedRowsData
.
map
((
row
:
any
)
=>
row
.
id
).
filter
((
id
:
any
)
=>
id
!==
undefined
);
onCancel
(
ids
);
};
return
(
<
Tooltip
title=
{
isEnabled
?
`Batalkan ${selectedRows.length} data`
:
'
Pilih data yang valid
'
}
>
<
IconButton
onClick=
{
handleClick
}
disabled=
{
!
isEnabled
}
color=
{
isEnabled
?
'
error
'
:
'
default
'
}
>
<
HighlightOffTwoToneIcon
/>
</
IconButton
>
</
Tooltip
>
);
};
export
default
ToolbarCancel
;
src/sections/bupot-21-26/bupot-26/constant/index.tsx
0 → 100644
View file @
9549ebd4
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-26/hooks/useAdvancedFilter.tsx
0 → 100644
View file @
9549ebd4
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
[]
=
[];
const
joins
:
(
'
AND
'
|
'
OR
'
)[]
=
[];
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
);
let
expr
:
string
|
null
=
null
;
// --- DATE FIELDS ---
if
(
dateFields
.
has
(
fieldName
))
{
const
rawVal
=
f
.
value
;
if
(
!
rawVal
&&
!
/is empty|is not empty/i
.
test
(
op
))
continue
;
const
ymd
=
toDbDate
(
rawVal
as
string
|
Date
);
if
(
!
ymd
)
continue
;
if
(
/^is$/i
.
test
(
op
))
{
expr
=
`"
${
fieldName
}
" >= '
${
ymd
}
00:00:00' AND "
${
fieldName
}
" <= '
${
ymd
}
23:59:59'`
;
}
else
if
(
/is on or after/i
.
test
(
op
))
{
expr
=
`"
${
fieldName
}
" >= '
${
ymd
}
'`
;
}
else
if
(
/is on or before/i
.
test
(
op
))
{
expr
=
`"
${
fieldName
}
" <= '
${
ymd
}
'`
;
}
}
// --- EMPTY ---
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 ---
if
(
!
expr
&&
/is any of/i
.
test
(
op
))
{
let
values
:
Array
<
string
|
number
>
=
[];
if
(
Array
.
isArray
(
f
.
value
))
values
=
f
.
value
as
any
;
else
if
(
typeof
f
.
value
===
'
string
'
)
values
=
f
.
value
.
split
(
'
,
'
)
.
map
((
s
)
=>
s
.
trim
())
.
filter
(
Boolean
);
else
if
(
f
.
value
!=
null
)
values
=
[
f
.
value
as
any
];
if
(
values
.
length
>
0
)
{
if
(
fieldName
===
'
fgStatus
'
||
fieldName
===
'
fg_status
'
)
{
const
ors
=
values
.
map
((
v
)
=>
{
const
s
=
escape
(
String
(
v
).
toLowerCase
());
return
`LOWER("
${
fieldName
}
") LIKE LOWER('%
${
s
}
%')`
;
});
expr
=
`(
${
ors
.
join
(
'
OR
'
)}
)`
;
}
else
{
const
ors
=
values
.
map
((
v
)
=>
{
const
s
=
escape
(
String
(
v
).
toLowerCase
());
return
`LOWER("
${
fieldName
}
") = '
${
s
}
'`
;
});
expr
=
`(
${
ors
.
join
(
'
OR
'
)}
)`
;
}
}
}
// --- FGSTATUS special single-value is / is not ---
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
))
{
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 ---
if
(
!
expr
)
{
const
valRaw
=
f
.
value
==
null
?
''
:
String
(
f
.
value
);
if
(
valRaw
!==
''
)
{
const
valEscaped
=
escape
(
valRaw
.
toLowerCase
());
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
))
{
const
values
=
Array
.
isArray
(
f
.
value
)
?
(
f
.
value
as
any
[]).
map
((
v
)
=>
escape
(
String
(
v
).
toLowerCase
()))
:
[
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
))
{
expr
=
`LOWER("
${
fieldName
}
") = '
${
valEscaped
}
'`
;
}
else
{
expr
=
`LOWER("
${
fieldName
}
") = '
${
valEscaped
}
'`
;
}
}
}
if
(
expr
)
{
exprs
.
push
(
expr
);
const
joinBefore
=
f
.
join
??
(
exprs
.
length
>
1
?
'
AND
'
:
'
AND
'
);
joins
.
push
(
joinBefore
);
}
}
// combine expressions
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
;
src/sections/bupot-21-26/bupot-26/hooks/useCancel.tsx
0 → 100644
View file @
9549ebd4
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
type
{
TCancelNrRequest
,
TCancelNrResponse
}
from
'
../types/types
'
;
import
nrApi
from
'
../utils/api
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
const
useCancelNr
=
(
props
?:
any
)
=>
useMutation
<
TCancelNrResponse
,
Error
,
TCancelNrRequest
>
({
mutationKey
:
queryKey
.
bupot26
.
cancel
,
mutationFn
:
(
payload
)
=>
nrApi
.
cancel
(
payload
),
...
props
,
});
export
default
useCancelNr
;
src/sections/bupot-21-26/bupot-26/hooks/useCetakPdf.tsx
0 → 100644
View file @
9549ebd4
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
bupot26
from
'
../utils/api
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
const
useCetakPdf
=
(
options
?:
any
)
=>
useMutation
({
mutationKey
:
queryKey
.
bupot26
.
cetakPdf
(
options
),
mutationFn
:
async
(
params
:
any
)
=>
bupot26
.
cetakPdfDetail
(
params
),
...
options
,
});
export
default
useCetakPdf
;
src/sections/bupot-21-26/bupot-26/hooks/useDelete.tsx
0 → 100644
View file @
9549ebd4
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
type
{
TBaseResponseAPI
,
TDeleteNrRequest
}
from
'
../types/types
'
;
import
bupot26
from
'
../utils/api
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
const
useDelete
=
(
props
?:
any
)
=>
useMutation
<
TBaseResponseAPI
<
null
>
,
Error
,
TDeleteNrRequest
>
({
mutationKey
:
queryKey
.
bupot26
.
delete
,
mutationFn
:
(
payload
)
=>
bupot26
.
delete
(
payload
),
...
props
,
});
export
default
useDelete
;
src/sections/bupot-21-26/bupot-26/hooks/useGetBupot26.tsx
0 → 100644
View file @
9549ebd4
import
{
isEmpty
}
from
'
lodash
'
;
import
{
useQuery
}
from
'
@tanstack/react-query
'
;
import
type
{
// TGetListDataTableDnResult,
TGetListDataTableNr
,
TGetListDataTableNrResult
,
}
from
'
../types/types
'
;
import
{
FG_PDF_STATUS
,
FG_SIGN_STATUS
}
from
'
../constant
'
;
import
nrApi
from
'
../utils/api
'
;
import
dayjs
from
'
dayjs
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
export
type
TGetDnApiWrapped
=
{
data
:
TGetListDataTableNrResult
[];
total
:
number
;
pageSize
:
number
;
page
:
number
;
// 1-based
};
// ---------- helpers (unchanged, kept for completeness) ----------
export
const
transformFgStatusToFgSignStatus
=
(
fgStatus
:
any
)
=>
{
const
splittedFgStatus
=
fgStatus
?.
split
(
'
-
'
)
||
[];
if
(
splittedFgStatus
.
includes
(
'
SIGN
'
)
>
0
)
return
FG_SIGN_STATUS
.
FAILED
;
if
(
splittedFgStatus
.
includes
(
'
SIGNING IN PROGRESS
'
))
return
FG_SIGN_STATUS
.
IN_PROGRESS
;
if
(
fgStatus
===
'
DUPLICATE
'
)
return
FG_SIGN_STATUS
.
DUPLICATE
;
if
(
fgStatus
===
'
NOT_MATCH_STATUS
'
)
return
FG_SIGN_STATUS
.
NOT_MATCH_STATUS
;
if
(
fgStatus
===
'
NOT_MATCH_NILAI
'
)
return
FG_SIGN_STATUS
.
NOT_MATCH_NILAI
;
if
(
fgStatus
===
'
NOT_MATCH_IDBUPOT
'
)
return
FG_SIGN_STATUS
.
NOT_MATCH_IDBUPOT
;
switch
(
splittedFgStatus
[
1
])
{
case
'
document signed successfully
'
:
case
'
Done
'
:
return
FG_SIGN_STATUS
.
SIGNED
;
case
'
SIGNING_IN_PROGRESS
'
:
return
FG_SIGN_STATUS
.
IN_PROGRESS
;
case
'
DUPLICATE
'
:
return
FG_SIGN_STATUS
.
DUPLICATE
;
case
'
NOT_MATCH_STATUS
'
:
return
FG_SIGN_STATUS
.
NOT_MATCH_STATUS
;
case
'
NOT_MATCH_IDBUPOT
'
:
return
FG_SIGN_STATUS
.
NOT_MATCH_IDBUPOT
;
default
:
return
null
;
}
};
export
const
getFgStatusPdf
=
(
link
:
any
,
fgSignStatus
:
any
)
=>
{
if
(
!
link
||
[
FG_SIGN_STATUS
.
IN_PROGRESS
].
includes
(
fgSignStatus
))
return
FG_PDF_STATUS
.
TIDAK_TERSEDIA
;
if
(
!
link
.
includes
(
'
https://coretaxdjp.pajak.go.id/
'
))
return
FG_PDF_STATUS
.
BELUM_TERBENTUK
;
return
FG_PDF_STATUS
.
TERBENTUK
;
};
export
const
transformSortModelToSortApiPayload
=
(
transformedModel
:
any
)
=>
({
sortingMode
:
transformedModel
.
map
((
item
:
any
)
=>
item
.
field
).
join
(
'
,
'
),
sortingMethod
:
transformedModel
.
length
>
0
?
transformedModel
[
0
].
sort
:
'
desc
'
,
});
export
const
formatDateToDDMMYYYY
=
(
dateString
:
string
|
null
|
undefined
)
=>
{
if
(
!
dateString
)
return
''
;
const
date
=
new
Date
(
dateString
);
const
day
=
String
(
date
.
getDate
()).
padStart
(
2
,
'
0
'
);
const
month
=
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
'
0
'
);
const
year
=
date
.
getFullYear
();
return
`
${
day
}
/
${
month
}
/
${
year
}
`
;
};
const
normalisePropsGetNr
=
(
params
:
TGetListDataTableNr
)
=>
({
...
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
,
npwpPemotong
:
params
.
npwpPemotong
,
idBupot
:
params
.
idBupot
,
internal_id
:
params
.
internal_id
,
fgStatus
:
params
.
fgStatus
,
fgSignStatus
:
transformFgStatusToFgSignStatus
(
params
.
fgStatus
),
fgPdf
:
getFgStatusPdf
(
params
.
link
,
transformFgStatusToFgSignStatus
(
params
.
fgStatus
)),
// fgLapor: params.fgLapor,
revNo
:
params
.
revNo
,
thnPajak
:
params
.
tahunPajak
,
msPajak
:
params
.
masaPajak
,
kdObjPjk
:
params
.
kodeObjekPajak
,
noBupot
:
params
.
noBupot
,
idDipotong
:
params
.
userId
,
glAccount
:
params
.
glAccount
,
namaDipotong
:
params
.
nama
,
jmlBruto
:
params
.
dpp
,
pphDipotong
:
params
.
pphDipotong
,
created
:
params
.
created_by
,
fgKirimEmail
:
params
.
fgkirimemail
,
created_at
:
formatDateToDDMMYYYY
(
params
.
created_at
),
updated
:
params
.
updated_by
,
updated_at
:
formatDateToDDMMYYYY
(
params
.
updated_at
),
});
export
const
normalizeExistingNr
=
(
res
:
any
)
=>
({
// 🧾 Data Pajak Utama
tglPemotongan
:
res
.
tglpemotongan
??
''
,
thnPajak
:
res
.
tahunPajak
??
''
,
msPajak
:
res
.
masaPajak
??
''
,
// 👤 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
{
page
=
0
,
pageSize
=
params
.
limit
??
10
,
sort
,
filter
,
advanced
,
sortingMode
:
sortingModeParam
,
sortingMethod
:
sortingMethodParam
,
...
rest
}
=
params
;
let
sortPayload
:
any
;
let
sortingMode
=
sortingModeParam
||
''
;
let
sortingMethod
=
sortingMethodParam
||
''
;
if
(
sort
)
{
try
{
const
parsed
=
JSON
.
parse
(
sort
);
if
(
Array
.
isArray
(
parsed
)
&&
parsed
.
length
>
0
)
{
sortPayload
=
parsed
;
sortingMode
=
parsed
[
0
]?.
field
??
sortingMode
;
sortingMethod
=
parsed
[
0
]?.
sort
??
sortingMethod
;
}
}
catch
{
sortPayload
=
[];
}
}
return
{
page
:
page
+
1
,
limit
:
pageSize
,
advanced
:
typeof
advanced
===
'
string
'
&&
advanced
.
trim
()
!==
''
?
advanced
:
filter
&&
!
isEmpty
(
JSON
.
parse
(
filter
))
?
filter
:
undefined
,
...(
sortPayload
?
{
sort
:
sortPayload
}
:
{}),
sortingMode
,
sortingMethod
,
...
rest
,
feature
:
'
26
'
};
};
export
const
useGetBupot26
=
({
params
}:
{
params
:
any
})
=>
{
const
normalized
=
normalizeParams
(
params
);
return
useQuery
<
TGetDnApiWrapped
>
({
queryKey
:
queryKey
.
bupot26
.
all
(
normalized
),
queryFn
:
async
()
=>
{
const
res
:
any
=
await
nrApi
.
get
({
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
:
TGetListDataTableNrResult
[]
=
[];
const
normalizeWithWorker
=
()
=>
new
Promise
<
TGetListDataTableNrResult
[]
>
((
resolve
,
reject
)
=>
{
try
{
const
worker
=
new
Worker
(
new
URL
(
'
../workers/normalizeNr.worker.js
'
,
import
.
meta
.
url
),
{
type
:
'
module
'
}
);
worker
.
onmessage
=
(
e
)
=>
{
const
{
data
,
error
}
=
e
.
data
;
if
(
error
)
{
worker
.
terminate
();
reject
(
new
Error
(
error
));
}
else
{
worker
.
terminate
();
resolve
(
data
as
TGetListDataTableNrResult
[]);
}
};
worker
.
onerror
=
(
err
)
=>
{
worker
.
terminate
();
reject
(
err
);
};
worker
.
postMessage
(
rawData
);
}
catch
(
err
)
{
reject
(
err
);
}
});
try
{
if
(
typeof
Worker
!==
'
undefined
'
)
{
dataArray
=
await
normalizeWithWorker
();
}
else
{
console
.
warn
(
'
⚠️ Worker not supported, using sync normalization
'
);
dataArray
=
rawData
.
map
(
normalisePropsGetNr
)
as
unknown
as
TGetListDataTableNrResult
[];
}
}
catch
(
err
)
{
console
.
error
(
'
❌ Worker failed, fallback to sync normalize:
'
,
err
);
dataArray
=
rawData
.
map
(
normalisePropsGetNr
)
as
unknown
as
TGetListDataTableNrResult
[];
}
return
{
data
:
dataArray
,
total
,
pageSize
:
normalized
.
limit
,
page
:
normalized
.
page
,
};
},
placeholderData
:
(
prev
)
=>
prev
,
refetchOnWindowFocus
:
false
,
refetchOnMount
:
false
,
staleTime
:
0
,
gcTime
:
0
,
retry
:
false
,
});
};
export
const
useGetBupot26ById
=
(
id
:
string
,
options
=
{})
=>
useQuery
({
queryKey
:
queryKey
.
bupot26
.
detail
(
id
),
queryFn
:
async
()
=>
{
const
res
=
await
nrApi
.
getById
(
id
);
console
.
log
(
res
);
if
(
!
res
)
throw
new
Error
(
'
Data tidak ditemukan
'
);
const
normalized
=
normalizeExistingNr
(
res
);
console
.
log
(
'
✅ Normalized data:
'
,
normalized
);
return
normalized
;
},
enabled
:
!!
id
,
refetchOnWindowFocus
:
false
,
...
options
,
});
export
default
useGetBupot26
;
src/sections/bupot-21-26/bupot-26/hooks/usePphDipotong.tsx
0 → 100644
View file @
9549ebd4
/* eslint-disable @typescript-eslint/no-shadow */
import
{
useEffect
}
from
'
react
'
;
import
{
useFormContext
,
useWatch
}
from
'
react-hook-form
'
;
const
usePphDipotong
=
(
kodeObjekPajakSelected
?:
any
)
=>
{
const
{
watch
,
setValue
,
control
}
=
useFormContext
();
// ambil value dari form
const
fgFasilitas
=
watch
(
'
fgFasilitas
'
);
const
fgIdDipotong
=
watch
(
'
fgIdDipotong
'
);
// mapping statusPPh ke isFinal
const
isFinal
=
kodeObjekPajakSelected
?.
statuspph
?.
toLowerCase
()
===
'
final
'
?
1
:
0
;
const
updateTarifValues
=
()
=>
{
if
(
kodeObjekPajakSelected
)
{
let
valueTarif
=
Number
(
kodeObjekPajakSelected
.
tarif
)
||
0
;
if
(
fgFasilitas
===
'
6
'
)
{
valueTarif
=
0.5
;
}
else
if
(
fgFasilitas
===
'
8
'
)
{
valueTarif
=
0
;
}
setValue
(
'
tarif
'
,
valueTarif
,
{
shouldValidate
:
true
});
setValue
(
'
tarifLt
'
,
fgIdDipotong
===
'
1
'
&&
isFinal
===
0
?
'
100
'
:
'
0
'
,
{
shouldValidate
:
true
,
});
}
};
// watch field yang mempengaruhi perhitungan
const
handlerSetPphDipotong
=
useWatch
({
control
,
name
:
[
'
thnPajak
'
,
'
fgFasilitas
'
,
'
fgIdDipotong
'
,
'
jmlBruto
'
,
'
tarif
'
],
});
const
calculateAndSetPphDipotong
=
(
thnPajak
:
number
,
fgFasilitas
:
string
,
fgIdDipotong
:
string
,
jmlBruto
:
number
,
tarif
:
number
)
=>
{
if
(
kodeObjekPajakSelected
)
{
const
valTarif
=
thnPajak
<
2024
&&
fgIdDipotong
===
'
1
'
&&
isFinal
===
0
?
tarif
*
2
:
tarif
;
const
valPphDipotong
=
fgFasilitas
===
'
8
'
// contoh: fasilitas tertentu PPh 0
?
0
:
(
jmlBruto
*
valTarif
)
/
100
;
setValue
(
'
pphDipotong
'
,
Math
.
round
(
valPphDipotong
||
0
),
{
shouldValidate
:
true
,
});
}
};
useEffect
(()
=>
{
if
(
handlerSetPphDipotong
.
filter
((
item
)
=>
!
item
).
length
<
2
)
{
calculateAndSetPphDipotong
(
Number
(
handlerSetPphDipotong
[
0
]),
handlerSetPphDipotong
[
1
]
as
string
,
handlerSetPphDipotong
[
2
]
as
string
,
Number
(
handlerSetPphDipotong
[
3
]),
Number
(
handlerSetPphDipotong
[
4
])
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
handlerSetPphDipotong
]);
return
{
updateTarifValues
,
};
};
export
default
usePphDipotong
;
src/sections/bupot-21-26/bupot-26/hooks/useSave.tsx
0 → 100644
View file @
9549ebd4
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
dayjs
from
'
dayjs
'
;
import
type
{
TPostBupot26Request
}
from
'
../types/types
'
;
import
nrApi
from
'
../utils/api
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
const
transformParams
=
({
isPengganti
=
false
,
...
Data
}:
any
):
TPostBupot26Request
=>
{
const
{
id
,
idBupot
,
noBupot
,
npwpPemotong
,
idTku
,
masaPajak
,
tahunPajak
,
tinDipotong
,
namaDipotong
,
alamatDipotong
,
negaraDipotong
,
tglLahirDipotong
,
tmptLahirDipotong
,
nomorPaspor
,
nomorKitasKitap
,
fgFasilitas
,
noDokLainnya
,
kodeObjekPajak
,
pasalPph
,
statusPph
,
penghasilanBruto
,
normaPenghasilanNeto
,
tarif
,
pphDipotong
,
kap
,
kjs
,
metodePembayaranBendahara
,
nomorSP2D
,
tglPemotongan
,
userId
,
kanal
,
revNo
:
initialRevNo
,
}
=
Data
;
// 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
.
value
??
''
,
tglLahirDipotong
:
tglLahirDipotong
?
dayjs
(
tglLahirDipotong
).
format
(
'
DDMMYYYY
'
)
:
''
,
tmptLahirDipotong
:
tmptLahirDipotong
??
''
,
nomorPaspor
:
nomorPaspor
??
''
,
nomorKitasKitap
:
nomorKitasKitap
??
''
,
// Fasilitas
sertifikatInsentifDipotong
:
fgFasilitas
.
value
??
'
9
'
,
nomorSertifikatInsentif
:
noDokLainnya
??
''
,
// Objek Pajak
kodeObjekPajak
:
kodeObjekPajak
.
value
??
''
,
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
}
=
Data
;
// 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
.
value
,
nomorDokumen
:
nomorDok
,
tanggal_Dokumen
:
tanggalFormatted
,
},
];
})(),
metodePembayaranBendahara
:
metodePembayaranBendahara
??
''
,
nomorSP2D
:
nomorSP2D
??
''
,
tglPemotongan
:
tglPemotongan
?
dayjs
(
tglPemotongan
).
format
(
'
DDMMYYYY
'
)
:
''
,
userId
:
userId
??
''
,
kanal
:
kanal
??
''
,
revNo
,
feature
:
'
26
'
};
};
const
useSave
=
(
props
?:
any
)
=>
useMutation
({
mutationKey
:
queryKey
.
bupot26
.
draft
,
mutationFn
:
(
params
:
any
)
=>
nrApi
.
save
(
transformParams
(
params
)),
...
props
,
});
export
default
useSave
;
src/sections/bupot-21-26/bupot-26/hooks/useUpload.tsx
0 → 100644
View file @
9549ebd4
// hooks/useUpload.ts
import
{
useMutation
}
from
'
@tanstack/react-query
'
;
import
nrApi
from
'
../utils/api
'
;
import
queryKey
from
'
../../constant/queryKey
'
;
const
useUpload
=
(
props
?:
any
)
=>
useMutation
({
mutationKey
:
queryKey
.
bupot26
.
upload
,
mutationFn
:
(
payload
:
{
id
:
string
|
number
})
=>
nrApi
.
upload
(
payload
),
...
props
,
});
export
default
useUpload
;
src/sections/bupot-21-26/bupot-26/store/paginationStore.ts
0 → 100644
View file @
9549ebd4
import
{
create
}
from
'
zustand
'
;
console
.
log
(
'
✅ pagination store created
'
);
type
TableKey
=
string
;
interface
TablePagination
{
page
:
number
;
pageSize
:
number
;
}
interface
TableFilter
{
items
:
any
[];
}
interface
PaginationState
{
tables
:
Record
<
TableKey
,
TablePagination
>
;
filters
:
Record
<
TableKey
,
TableFilter
>
;
setPagination
:
(
table
:
TableKey
,
next
:
Partial
<
TablePagination
>
)
=>
void
;
resetPagination
:
(
table
:
TableKey
)
=>
void
;
setFilter
:
(
table
:
TableKey
,
next
:
Partial
<
TableFilter
>
)
=>
void
;
resetFilter
:
(
table
:
TableKey
)
=>
void
;
}
export
const
usePaginationStore
=
create
<
PaginationState
>
((
set
)
=>
({
tables
:
{},
filters
:
{},
setPagination
:
(
table
,
next
)
=>
set
((
state
)
=>
{
const
prev
=
state
.
tables
[
table
]
??
{
page
:
0
,
pageSize
:
10
};
return
{
tables
:
{
...
state
.
tables
,
[
table
]:
{
page
:
next
.
page
??
prev
.
page
,
pageSize
:
next
.
pageSize
??
prev
.
pageSize
,
},
},
};
}),
resetPagination
:
(
table
)
=>
set
((
state
)
=>
({
tables
:
{
...
state
.
tables
,
[
table
]:
{
page
:
0
,
pageSize
:
state
.
tables
[
table
]?.
pageSize
??
10
},
},
})),
setFilter
:
(
table
,
next
)
=>
set
((
state
)
=>
({
filters
:
{
...
state
.
filters
,
[
table
]:
{
items
:
next
.
items
??
state
.
filters
[
table
]?.
items
??
[],
},
},
})),
resetFilter
:
(
table
)
=>
set
((
state
)
=>
({
filters
:
{
...
state
.
filters
,
[
table
]:
{
items
:
[]
},
},
})),
}));
src/sections/bupot-21-26/bupot-26/types/types.ts
0 → 100644
View file @
9549ebd4
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
ActionItem
=
{
title
:
string
;
icon
:
React
.
ReactNode
;
func
?:
()
=>
void
;
disabled
?:
boolean
;
};
export
type
TDokReferensi
=
{
dokReferensi
:
string
;
nomorDokumen
:
string
;
tanggal_Dokumen
:
string
;
// format: DDMMYYYY
};
export
type
TPostBupot26Request
=
{
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
;
feature
:
'
26
'
};
export
type
TCountry
=
{
kode
:
string
;
nama
:
string
;
};
export
type
TCountryResult
=
TCountry
[];
export
type
TPostUpload
=
{
id
:
string
;
feature
:
'
26
'
};
export
type
TDeleteNrRequest
=
{
id
:
string
;
feature
:
'
26
'
};
export
type
TCancelNrRequest
=
{
id
:
string
|
number
;
tglPembatalan
:
string
;
// format: DDMMYYYY
feature
:
'
26
'
};
export
type
TCancelNrResponse
=
TBaseResponseAPI
<
{
id
:
string
|
number
;
feature
:
'
26
'
statusBatal
?:
string
;
message
?:
string
;
}
>
;
src/sections/bupot-21-26/bupot-26/utils/api.tsx
0 → 100644
View file @
9549ebd4
import
axios
from
'
axios
'
;
import
type
{
TBaseResponseAPI
,
TCancelNrRequest
,
TCancelNrResponse
,
TDeleteNrRequest
,
TGetListDataTableNrResult
,
TPostBupot26Request
,
}
from
'
../types/types
'
;
import
unifikasiClient
from
'
./unifikasiClient
'
;
const
bupot21
=
()
=>
{};
const
axiosCetakPdf
=
axios
.
create
({
baseURL
:
import
.
meta
.
env
.
VITE_APP_BASE_API_URL_CETAK
,
headers
:
{
Authorization
:
`Basic
${
window
.
btoa
(
'
admin:ortax123
'
)}
`
,
password
:
''
,
},
});
// API untuk get list table
bupot21
.
get
=
async
(
config
:
any
)
=>
{
const
{
data
:
{
message
,
metaPage
,
data
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
get
<
TBaseResponseAPI
<
TGetListDataTableNrResult
>
>
('IF_TXR_029/',
{
...
config
,
}
);
if (statusCode !== 200)
{
throw
new
Error
(
message
);
}
return
{
total
:
metaPage
?
Number
(
metaPage
.
totalRow
)
:
0
,
data
}
;
};
bupot21.save = async (config: TPostBupot26Request) =
>
{
const
{
data
:
{
message
,
data
,
code
},
}
=
await
unifikasiClient
.
post
<
TBaseResponseAPI
<
TPostBupot26Request
>>
(
'
/IF_TXR_029/
'
,
{
...
config
,
});
if
(
code
===
0
)
{
throw
new
Error
(
message
);
}
return
data
;
}
;
bupot21.getById = async (id: string) =
>
{
const
res
=
await
unifikasiClient
.
get
(
'
/IF_TXR_029/
'
,
{
params
:
{
id
,
feature
:
'
26
'
}
});
const
{
data
:
{
status
,
message
,
data
},
status
:
statusCode
,
}
=
res
;
if
(
statusCode
!==
200
||
status
?.
toLowerCase
()
!==
'
success
'
)
{
console
.
error
(
'
getNrId failed:
'
,
{
statusCode
,
status
,
message
});
throw
new
Error
(
message
||
'
Gagal mengambil data NR
'
);
}
const
dnData
=
Array
.
isArray
(
data
)
?
data
[
0
]
:
data
;
return
dnData
;
}
;
bupot21.upload = async (
{
id
}
:
{
id
:
string
|
number
}
) =
>
{
const
{
data
:
{
status
,
message
,
data
,
code
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
post
(
'
/IF_TXR_029/upload
'
,
{
id
,
feature
:
'
26
'
});
return
{
status
,
message
,
data
,
code
,
statusCode
};
}
;
bupot21.delete = async (payload: TDeleteNrRequest, config?: Record
<
string
,
any
>
): Promise
<
any
>
=
>
{
const
{
data
:
{
status
,
message
,
data
},
status
:
statusCode
,
}
=
await
unifikasiClient
.
post
<
TBaseResponseAPI
<
any
>>
(
'
/IF_TXR_029/delete
'
,
payload
,
{
...
config
,
});
if
(
statusCode
!==
200
||
status
?.
toLowerCase
()
===
'
error
'
)
{
throw
new
Error
(
message
||
'
Gagal menghapus data NR
'
);
}
return
data
;
}
;
bupot21.cancel = async (
{
id
,
tglPembatalan
}
: TCancelNrRequest): Promise
<
TCancelNrResponse
>
=
>
{
const
{
data
:
{
status
,
message
,
data
,
code
,
time
,
metaPage
,
total
},
}
=
await
unifikasiClient
.
post
(
'
/IF_TXR_029/batal
'
,
{
id
,
tglPembatalan
,
feature
:
'
26
'
,
});
console
.
log
(
'
Cancel NR response:
'
,
{
code
,
message
,
status
});
if
(
code
===
0
)
{
throw
new
Error
(
message
||
'
Gagal membatalkan data
'
);
}
return
{
status
,
message
,
data
,
code
,
time
,
metaPage
,
total
,
};
}
;
bupot21.cetakPdfDetail = async (payload: Record
<
string
,
any
>
) =
>
{
const
response
=
await
axiosCetakPdf
.
post
(
'
/report/ctas/bpnr
'
,
payload
);
const
body
=
response
.
data
;
if
(
!
response
||
response
.
status
!==
200
||
body
.
status
===
'
fail
'
||
body
.
status
===
'
error
'
||
body
.
status
===
'
0
'
)
{
throw
new
Error
(
body
.
message
||
'
System tidak dapat memenuhi permintaan, coba beberapa saat lagi
'
);
}
return
body
;
}
;
export default bupot21;
src/sections/bupot-21-26/bupot-26/utils/normalizePayloadCetakPdf.ts
0 → 100644
View file @
9549ebd4
import
dayjs
from
'
dayjs
'
;
import
{
FG_FASILITAS_DN
}
from
'
../constant
'
;
const
FASILITAS_LABEL_MAP
:
Record
<
string
,
string
>
=
{
[
FG_FASILITAS_DN
.
SKB_PPH_PASAL_22
]:
'
SKB PPh Pasal 22
'
,
[
FG_FASILITAS_DN
.
SKB_PPH_PASAL_23
]:
'
SKB PPh Pasal 23
'
,
[
FG_FASILITAS_DN
.
SKB_PPH_PHTB
]:
'
SKB PPh PHTB
'
,
[
FG_FASILITAS_DN
.
DTP
]:
'
DTP
'
,
[
FG_FASILITAS_DN
.
SKB_PPH_BUNGA_DEPOSITO_DANA_PENSIUN_TABUNGAN
]:
'
SKB PPh Bunga Deposito Dana Pensiun Tabungan
'
,
[
FG_FASILITAS_DN
.
SUKET_PP23_PP52
]:
'
Suket PP23/PP52
'
,
[
FG_FASILITAS_DN
.
SKD_WPLN
]:
'
SKD WPLN
'
,
[
FG_FASILITAS_DN
.
FASILITAS_LAINNYA
]:
'
Fasilitas Lainnya
'
,
[
FG_FASILITAS_DN
.
TANPA_FASILITAS
]:
'
Tanpa Fasilitas
'
,
[
FG_FASILITAS_DN
.
SKB_PPH_PASAL_21
]:
'
SKB PPh Pasal 21
'
,
[
FG_FASILITAS_DN
.
DTP_PPH_PASAL_21
]:
'
DTP PPh Pasal 21
'
,
};
const
formatTanggalIndo
=
(
isoDate
?:
string
):
string
=>
{
if
(
!
isoDate
)
return
''
;
return
dayjs
(
isoDate
).
locale
(
'
id
'
).
format
(
'
DD MMMM YYYY
'
);
};
/**
* Normalisasi payload Bupot Unifikasi agar sesuai format yang digunakan API cetak PDF
*/
export
const
normalizePayloadCetakPdf
=
(
payload
:
Record
<
string
,
any
>
)
=>
{
if
(
!
payload
)
return
payload
;
const
adjusted
=
{
...
payload
};
if
(
adjusted
.
tglpemotongan
)
{
adjusted
.
tglPemotongan
=
formatTanggalIndo
(
adjusted
.
tglpemotongan
);
// versi tampil
}
// === Konversi kode fasilitas ke label ===
const
fasilitasCode
=
adjusted
.
sertifikatInsentifDipotong
;
adjusted
.
sertifikatInsentifDipotong
=
FASILITAS_LABEL_MAP
[
fasilitasCode
]
||
fasilitasCode
||
''
;
// === Field default tambahan ===
adjusted
.
mixcode
=
adjusted
.
mixcode
||
'
mixcode
'
;
adjusted
.
qrcode
=
adjusted
.
qrcode
||
'
qrcode
'
;
adjusted
.
metodePembayaranBendahara
=
adjusted
.
metodePembayaranBendahara
||
'
-
'
;
adjusted
.
nomorSP2D
=
adjusted
.
nomorSP2D
||
'
-
'
;
adjusted
.
npwpDipotong
=
adjusted
.
npwp
||
''
;
adjusted
.
namaDipotong
=
adjusted
.
namaDipotong
||
''
;
adjusted
.
nitkuDipotong
=
adjusted
.
nik
||
''
;
adjusted
.
namaPemotong
=
adjusted
.
namaDipotong
||
''
;
adjusted
.
nitkuPemotong
=
adjusted
.
idTku
||
''
;
adjusted
.
penghasilanBruto
=
adjusted
.
penghasilanBruto
||
''
;
adjusted
.
tanggal_Dokumen
=
adjusted
.
dokumen_referensi
[
0
].
tanggal_Dokumen
;
adjusted
.
status
=
'
Proforma
'
;
adjusted
.
msPajak
=
adjusted
.
masaPajak
;
adjusted
.
thnPajak
=
adjusted
.
tahunPajak
;
adjusted
.
kdObjPjk
=
adjusted
.
kodeObjekPajak
;
adjusted
.
fgPdf
=
adjusted
.
fgPdf
===
'
TIDAK_TERSEDIA
'
?
'
2
'
:
adjusted
.
fgPdf
;
return
adjusted
;
};
export
default
normalizePayloadCetakPdf
;
src/sections/bupot-21-26/bupot-26/utils/unifikasiClient.tsx
0 → 100644
View file @
9549ebd4
import
axios
from
'
axios
'
;
const
BASE_URL
=
`https://nodesandbox.pajakexpress.id:1837`
;
const
unifikasiClient
=
axios
.
create
({
baseURL
:
BASE_URL
,
validateStatus
(
status
)
{
return
(
status
>=
200
&&
status
<
300
)
||
status
===
500
;
},
});
// Interceptor untuk selalu update token dari localStorage
unifikasiClient
.
interceptors
.
request
.
use
((
config
)
=>
{
const
jwtAccessToken
=
localStorage
.
getItem
(
'
jwt_access_token
'
);
const
xToken
=
localStorage
.
getItem
(
'
x-token
'
);
if
(
jwtAccessToken
)
{
config
.
headers
.
Authorization
=
`Bearer
${
jwtAccessToken
}
`
;
}
if
(
xToken
)
{
config
.
headers
[
'
x-token
'
]
=
xToken
;
}
return
config
;
});
export
default
unifikasiClient
;
src/sections/bupot-21-26/bupot-26/utils/utils.tsx
0 → 100644
View file @
9549ebd4
import
dayjs
from
'
dayjs
'
;
import
{
MIN_THN_PAJAK
}
from
'
../constant
'
;
export
const
currentYear
=
dayjs
().
year
();
export
const
getHighestStartingYear
=
(
thnAwalUnifikasi
:
any
)
=>
Math
.
max
(
MIN_THN_PAJAK
,
thnAwalUnifikasi
);
export
const
selectedInitialMonth
=
({
thnAwalUnifikasi
,
masaAwalUnifikasi
}:
any
)
=>
{
const
highestYear
=
getHighestStartingYear
(
thnAwalUnifikasi
);
return
highestYear
>
thnAwalUnifikasi
?
'
01
'
:
masaAwalUnifikasi
;
};
export
const
determineStartingMonth
=
({
thnPajak
,
thnAwalUnifikasi
,
masaAwalUnifikasi
}:
any
)
=>
{
const
highestYear
=
getHighestStartingYear
(
thnAwalUnifikasi
);
const
initialMonth
=
selectedInitialMonth
({
thnAwalUnifikasi
,
masaAwalUnifikasi
});
return
thnPajak
>=
highestYear
&&
thnPajak
<=
currentYear
?
initialMonth
:
''
;
};
src/sections/bupot-21-26/bupot-26/view/bupot-26-list-view.tsx
0 → 100644
View file @
9549ebd4
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-26/view/bupot-26-rekam-view.tsx
0 → 100644
View file @
9549ebd4
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-26/view/index.ts
0 → 100644
View file @
9549ebd4
export
*
from
'
./bupot-26-list-view
'
;
export
*
from
'
./bupot-26-rekam-view
'
;
src/sections/bupot-21-26/bupot-26/workers/normalizeNr.worker.js
0 → 100644
View file @
9549ebd4
// 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
)
});
}
};
src/sections/bupot-21-26/bupot-a1/components/rekam/Identitas.tsx
View file @
9549ebd4
...
...
@@ -22,10 +22,17 @@ const genderOptions = [
];
const
Identitas
=
({
isPengganti
,
kodeNegetaOptions
,
ptkpOptions
}:
IdentitasProps
)
=>
{
// const { dnId } = useParams();
const
{
setValue
,
watch
}
=
useFormContext
();
const
fgKaryawanAsing
=
watch
(
'
fgKaryawanAsing
'
);
const
handleFgPerhitunganChange
=
(
val
:
any
)
=>
{
const
newValue
=
val
.
target
.
value
;
const
newIsGrossUp
=
newValue
===
'
1
'
;
setValue
(
'
fgPerhitungan
'
,
newValue
);
setValue
(
'
isGrossUp
'
,
newIsGrossUp
);
};
return
(
<>
<
Grid
container
rowSpacing=
{
2
}
alignItems=
"center"
columnSpacing=
{
2
}
sx=
{
{
mb
:
4
}
}
>
...
...
@@ -65,6 +72,7 @@ const Identitas = ({ isPengganti, kodeNegetaOptions, ptkpOptions }: IdentitasPro
name=
"fgPerhitungan"
label=
"Metode Pemotongan"
options=
{
fgPerhitunganOptions
}
onChange=
{
handleFgPerhitunganChange
}
/>
<
Field
.
Autocomplete
...
...
@@ -75,22 +83,21 @@ const Identitas = ({ isPengganti, kodeNegetaOptions, ptkpOptions }: IdentitasPro
width
:
'
65%
'
,
}
}
renderOption=
{
(
props
,
option
,
state
,
ownerState
)
=>
{
console
.
log
(
"
🚀 ~ Identitas ~ option:
"
,
option
);
const
{
key
,
...
optionProps
}
=
props
;
return
(
<
Box
key=
{
key
}
sx=
{
{
letterSpacing
:
'
1.5px
'
,
}
}
component=
"li"
{
...
optionProps
}
>
{
ownerState
.
getOptionLabel
(
option
)
}
</
Box
>
);
}
}
console
.
log
(
'
🚀 ~ Identitas ~ option:
'
,
option
);
const
{
key
,
...
optionProps
}
=
props
;
return
(
<
Box
key=
{
key
}
sx=
{
{
letterSpacing
:
'
1.5px
'
,
}
}
component=
"li"
{
...
optionProps
}
>
{
ownerState
.
getOptionLabel
(
option
)
}
</
Box
>
);
}
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
6
}
}
>
...
...
src/sections/bupot-21-26/bupot-a1/components/rekam/PerhitunganA1.tsx
View file @
9549ebd4
import
{
CalculateRounded
}
from
'
@mui/icons-material
'
;
import
{
LoadingButton
}
from
'
@mui/lab
'
;
import
{
Divider
,
Grid
,
Stack
,
Typography
}
from
'
@mui/material
'
;
import
{
RHFNumeric
}
from
'
src/components/hook-form/rhf-numeric
'
;
import
{
FORM_FIELDS
,
FORM_SECTIONS
,
getFieldNameByIndex
,
isFieldReadOnly
}
from
'
../constan
t
'
;
import
dayjs
from
'
dayjs
'
;
import
{
memo
,
useEffect
}
from
'
reac
t
'
;
import
{
useFormContext
}
from
'
react-hook-form
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
{
memo
,
useEffect
,
useMemo
}
from
'
react
'
;
import
{
LoadingButton
}
from
'
@mui/lab
'
;
import
{
CalculateRounded
}
from
'
@mui/icons-material
'
;
import
{
RHFNumeric
}
from
'
src/components/hook-form/rhf-numeric
'
;
import
{
getHitungBulananErrorMessage
,
useHitungTahunanA1
as
hitungTahunanA1
,
}
from
'
src/sections/bupot-21-26/hitung
'
;
import
dayjs
from
'
dayjs
'
;
import
{
FORM_FIELDS
,
FORM_SECTIONS
,
getFieldNameByIndex
,
isFieldReadOnly
}
from
'
../constant
'
;
// ============================================
// REUSABLE COMPONENTS
...
...
@@ -146,7 +146,7 @@ const useRincianCalculations = () => {
// ✅ FIXED: Hook dipanggil di top-level component
// ============================================
const
PerhitunganA1Builder
=
memo
(
const
PerhitunganA1Builder
=
({
listInputs
=
[],
labelCols
=
4
,
...
...
@@ -183,9 +183,8 @@ const PerhitunganA1Builder = memo(
</
Stack
>
);
}
);
PerhitunganA1Builder
.
displayName
=
'
PerhitunganA1Builder
'
;
//
PerhitunganA1Builder.displayName = 'PerhitunganA1Builder';
// ============================================
// ✅ MAIN COMPONENT: Hook dipanggil di sini
...
...
@@ -197,6 +196,7 @@ export default function PerhitunganA1Container() {
const
fgPerhitungan
=
watch
(
'
fgPerhitungan
'
);
const
msPjkAwal
=
dayjs
(
watch
(
'
masaPajakAwal
'
)).
get
(
'
month
'
)
+
1
;
const
isMetodePemotonganSeTahun
=
watch
(
'
metodePemotongan
'
);
const
isGrossUp
=
watch
(
'
isGrossUp
'
);
useRincianCalculations
();
...
...
@@ -219,12 +219,7 @@ export default function PerhitunganA1Container() {
mutate
(
currentValues
as
any
);
};
const
handleGrossUpChange
=
(
_
:
any
,
checked
:
boolean
)
=>
{
setValue
(
'
fgPerhitungan
'
,
checked
?
'
1
'
:
'
0
'
);
setValue
(
'
isGrossUp
'
,
checked
);
};
const
listInputs
=
useMemo
(()
=>
{
const
listInputs
=
()
=>
{
const
result
:
any
[]
=
[];
let
globalIndex
=
0
;
...
...
@@ -253,16 +248,7 @@ export default function PerhitunganA1Container() {
key
:
fieldName
,
label
:
(
<
FormFieldLabel
number=
{
fieldNumber
}
text=
{
label
}
>
<
Field
.
Checkbox
name=
"isGrossUp"
label=
"Gross Up"
sx=
{
{
padding
:
0
}
}
slotProps=
{
{
checkbox
:
{
onChange
:
handleGrossUpChange
,
},
}
}
/>
<
Field
.
Checkbox
name=
"isGrossUp"
value=
{
isGrossUp
}
label=
"Gross Up"
sx=
{
{
padding
:
0
}
}
/>
</
FormFieldLabel
>
),
element
:
<
FormNumberInput
name=
{
fieldName
}
readOnly=
{
readOnly
}
/>,
...
...
@@ -304,14 +290,15 @@ export default function PerhitunganA1Container() {
});
return
result
;
},
[
fgPerhitungan
,
handleGrossUpChange
,
handleHitung
,
isMetodePemotonganSeTahun
,
isPending
,
msPjkAwal
,
]);
return
<
PerhitunganA1Builder
listInputs=
{
listInputs
}
labelCols=
{
9
}
inputCols=
{
3
}
/>;
};
useEffect
(()
=>
{
const
expectedFgPerhitungan
=
isGrossUp
?
'
1
'
:
'
0
'
;
if
(
fgPerhitungan
!==
expectedFgPerhitungan
)
{
setValue
(
'
fgPerhitungan
'
,
expectedFgPerhitungan
,
{
shouldValidate
:
false
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
isGrossUp
]);
// Hanya listen isGrossUp
return
<
PerhitunganA1Builder
listInputs=
{
listInputs
()
}
labelCols=
{
9
}
inputCols=
{
3
}
/>;
}
src/sections/bupot-21-26/bupot-a1/components/rekam/RincianPenghasilan.tsx
View file @
9549ebd4
...
...
@@ -35,16 +35,15 @@ const RincianPenghasilan = ({
const
tanggalPemotongan
=
watch
(
'
tglPemotongan
'
);
const
masaPajakAwal
=
watch
(
'
masaPajakAwal
'
);
const
masaPajakAkhir
=
watch
(
'
masaPajakAkhir
'
);
console
.
log
(
"
🚀 ~ RincianPenghasilan:
"
,
{
masaPajakAwal
,
masaPajakAkhir
});
useEffect
(()
=>
{
if
(
!
isPengganti
)
{
if
(
tanggalPemotongan
)
{
const
date
=
dayjs
(
tanggalPemotongan
);
setValue
(
'
tahunPajak
'
,
date
.
format
(
'
YYYY
'
));
setValue
(
'
masaPajak
'
,
date
.
format
(
'
MM
'
));
}
else
{
setValue
(
'
tahunPajak
'
,
''
);
setValue
(
'
masaPajak
'
,
''
);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
...
...
@@ -72,6 +71,7 @@ const RincianPenghasilan = ({
setValue
(
'
masaPajakAwal
'
,
'
01
'
)
setValue
(
'
masaPajakAkhir
'
,
'
12
'
)
setValue
(
'
noBupotSebelumnya
'
,
''
)
setValue
(
'
rincian14
'
,
'
0
'
)
}
}
}
slotProps=
{
{
...
...
@@ -113,11 +113,11 @@ const RincianPenghasilan = ({
<
Field
.
DatePicker
name=
"masaPajakAwal"
label=
"Masa Pajak Awal"
view
=
"month"
view
s=
{
[
'
month
'
]
}
// ✅ valid prop
format=
"MM"
openTo=
"month"
maxDate=
{
dayjs
(
masaPajakAkhir
)
}
// readOnly
={isMetodePemotonganSeTahun || isPengganti}
disabled
=
{
isMetodePemotonganSeTahun
||
isPengganti
}
/>
</
Grid
>
<
Grid
size=
{
{
md
:
1
}
}
>
...
...
@@ -131,11 +131,11 @@ const RincianPenghasilan = ({
<
Field
.
DatePicker
name=
"masaPajakAkhir"
label=
"Masa Pajak Akhir"
view=
'month'
views=
{
[
'
month
'
]
}
// ✅ valid prop
format=
"MM"
openTo=
"month"
minDate=
{
dayjs
(
masaPajakAwal
)
}
// readOnly
={isMetodePemotonganSeTahun || isPengganti}
disabled
=
{
isMetodePemotonganSeTahun
||
isPengganti
}
/>
</
Grid
>
...
...
src/sections/bupot-21-26/bupot-a1/view/tahunan-a1-rekam-view.tsx
View file @
9549ebd4
...
...
@@ -403,8 +403,10 @@ export const TahunanA1RekamView = () => {
const
jnsKelamin
=
dataResDetail
.
jnsKelamin
===
'
M
'
?
'
0
'
:
'
1
'
;
const
statusPtkp
=
`
${
dataResDetail
.
statusPtkp
}
/
${
dataResDetail
.
jmlPtkp
}
`
;
const
normalized
=
{
const
normalized
:
TahunanA1FormData
=
{
...
dataResDetail
,
masaPajakAwal
:
`
${
dayjs
(
dataResDetail
.
masaPajakAwal
,
'
MM
'
)}
`
,
masaPajakAkhir
:
`
${
dayjs
(
dataResDetail
.
masaPajakAkhir
,
'
MM
'
)}
`
,
idBupot
:
dataResDetail
.
idBupot
||
''
,
noBupot
:
dataResDetail
.
noBupot
||
''
,
revNo
:
dataResDetail
.
revNo
||
''
,
...
...
@@ -427,10 +429,11 @@ export const TahunanA1RekamView = () => {
label
:
''
,
},
metodePemotongan
:
`
${
dataResDetail
.
fgStatusPemotonganPph
}
`
,
fgPerhitungan
:
dataResDetail
.
tunjanganPPhGrossUp
===
'
NO
'
?
'
0
'
:
'
1
'
,
kdObjPjk
:
dataListKOP
.
filter
((
val
)
=>
val
.
value
===
dataResDetail
.
kodeObjekPajak
)[
0
],
fgFasilitas
:
fgFasilitasOptions
.
filter
((
val
)
=>
val
.
value
===
dataResDetail
.
fgFasilitas
)[
0
],
noDokLainnya
:
dataResDetail
.
noDokFasilitas
,
noDokLainnya
:
dataResDetail
.
noDokFasilitas
||
''
,
// Financial Details (Rincian)
rincian1
:
`
${
dataResDetail
.
gajiPensiun
||
0
}
`
,
...
...
@@ -458,9 +461,10 @@ export const TahunanA1RekamView = () => {
rincian23
:
`
${
dataResDetail
.
pph21KurangLebihBayar
||
0
}
`
,
idTku
:
MockNitku
.
filter
((
val
)
=>
val
.
value
===
dataResDetail
.
idTku
)[
0
],
};
}
as
unknown
as
TahunanA1FormData
;
if
(
isPengganti
)
{
normalized
[
'
id
'
]
=
undefined
;
normalized
[
'
idBupot
'
]
=
dataResDetail
.
idBupot
||
''
;
normalized
[
'
idBupot
'
]
=
dataResDetail
.
noBupot
||
''
;
normalized
[
'
revNo
'
]
=
`
${
Number
(
dataResDetail
.
revNo
)
||
0
}
`
;
...
...
src/sections/bupot-21-26/constant/queryKey.tsx
View file @
9549ebd4
...
...
@@ -2,6 +2,7 @@ export const appRootKey = 'bupot-21-26';
export
const
bulanan
=
'
bulanan
'
;
export
const
tahunanA1
=
'
tahunan-a1
'
;
export
const
bupotfinal
=
'
bupot-final-tidak-final
'
;
export
const
bupot26
=
'
bupot-26
'
;
const
queryKey
=
{
getKodeObjekPajak
:
(
params
:
any
)
=>
[
appRootKey
,
'
kode-objek-pajak
'
,
params
],
...
...
@@ -32,6 +33,15 @@ const queryKey = {
cancel
:
[
appRootKey
,
tahunanA1
,
'
cancel
'
],
cetakPdf
:
(
params
:
any
)
=>
[
appRootKey
,
tahunanA1
,
'
cetak-pdf
'
,
params
],
},
bupot26
:
{
all
:
(
params
:
any
)
=>
[
appRootKey
,
bupot26
,
params
],
detail
:
(
params
:
any
)
=>
[
appRootKey
,
bupot26
,
'
detail
'
,
params
],
draft
:
[
appRootKey
,
bupot26
,
'
draft
'
],
delete
:
[
appRootKey
,
bupot26
,
'
delete
'
],
upload
:
[
appRootKey
,
bupot26
,
'
upload
'
],
cancel
:
[
appRootKey
,
bupot26
,
'
cancel
'
],
cetakPdf
:
(
params
:
any
)
=>
[
appRootKey
,
bupot26
,
'
cetak-pdf
'
,
params
],
},
};
export
default
queryKey
;
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