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
4a474f6c
Commit
4a474f6c
authored
Oct 23, 2025
by
Rais Aryaguna
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: bulanan
parent
9ffe9ff5
Changes
14
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
2233 additions
and
804 deletions
+2233
-804
src/routes/sections/dashboard.tsx
src/routes/sections/dashboard.tsx
+1
-2
src/sections/bupot-21-26/DialogPenandatangan.tsx
src/sections/bupot-21-26/DialogPenandatangan.tsx
+0
-103
src/sections/bupot-21-26/bupot-bulanan/components/CustomColumnsButton.tsx
...ot-21-26/bupot-bulanan/components/CustomColumnsButton.tsx
+33
-0
src/sections/bupot-21-26/bupot-bulanan/components/CustomFilterButton.tsx
...pot-21-26/bupot-bulanan/components/CustomFilterButton.tsx
+1015
-0
src/sections/bupot-21-26/bupot-bulanan/components/CustomToolbar.tsx
...ns/bupot-21-26/bupot-bulanan/components/CustomToolbar.tsx
+69
-0
src/sections/bupot-21-26/bupot-bulanan/components/DialogPenandatangan.tsx
...ot-21-26/bupot-bulanan/components/DialogPenandatangan.tsx
+184
-0
src/sections/bupot-21-26/bupot-bulanan/constant/index.tsx
src/sections/bupot-21-26/bupot-bulanan/constant/index.tsx
+7
-14
src/sections/bupot-21-26/bupot-bulanan/constant/queryKey.tsx
src/sections/bupot-21-26/bupot-bulanan/constant/queryKey.tsx
+2
-1
src/sections/bupot-21-26/bupot-bulanan/hooks/useAdvanceSearch.tsx
...ions/bupot-21-26/bupot-bulanan/hooks/useAdvanceSearch.tsx
+0
-328
src/sections/bupot-21-26/bupot-bulanan/hooks/useAdvancedFilter.tsx
...ons/bupot-21-26/bupot-bulanan/hooks/useAdvancedFilter.tsx
+191
-0
src/sections/bupot-21-26/bupot-bulanan/hooks/useGetBulanan.ts
...sections/bupot-21-26/bupot-bulanan/hooks/useGetBulanan.ts
+2
-4
src/sections/bupot-21-26/bupot-bulanan/view/bulanan-list-view.tsx
...ions/bupot-21-26/bupot-bulanan/view/bulanan-list-view.tsx
+514
-332
src/sections/bupot-21-26/bupot-bulanan/view/bulanan-rekam-view.tsx
...ons/bupot-21-26/bupot-bulanan/view/bulanan-rekam-view.tsx
+74
-20
src/sections/bupot-21-26/paginationStore.ts
src/sections/bupot-21-26/paginationStore.ts
+141
-0
No files found.
src/routes/sections/dashboard.tsx
View file @
4a474f6c
...
...
@@ -140,8 +140,7 @@ export const dashboardRoutes: RouteObject[] = [
{
index
:
true
,
element
:
<
OverviewBupotBulananPage
/>
},
{
path
:
'
bulanan
'
,
element
:
<
OverviewBupotBulananPage
/>
},
{
path
:
'
bulanan/rekam
'
,
element
:
<
OverviewBupotBulananRekamPage
/>
},
{
path
:
'
bulanan/:id/ubah
'
,
element
:
<
OverviewBupotBulananRekamPage
/>
},
{
path
:
'
bulanan/:id/pengganti
'
,
element
:
<
OverviewBupotBulananRekamPage
/>
},
{
path
:
'
bulanan/:id/:type
'
,
element
:
<
OverviewBupotBulananRekamPage
/>
},
{
path
:
'
bupot-final
'
,
element
:
<
OverviewBupotFinalTdkFinalPage
/>
},
{
path
:
'
tahunan
'
,
element
:
<
OverviewBupotA1Page
/>
},
{
path
:
'
bupot-26
'
,
element
:
<
OverviewBupotPasal26Page
/>
},
...
...
src/sections/bupot-21-26/DialogPenandatangan.tsx
deleted
100644 → 0
View file @
9ffe9ff5
import
{
Close
}
from
'
@mui/icons-material
'
;
import
{
Dialog
,
DialogContent
,
DialogTitle
,
IconButton
,
Typography
}
from
'
@mui/material
'
;
// import { useState } from 'react';
import
{
useForm
}
from
'
react-hook-form
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
{
useAppSelector
}
from
'
src/store
'
;
interface
DialogPenandatanganProps
{
isOpen
:
boolean
;
onClose
:
()
=>
void
;
title
?:
string
;
// onSubmit: (data: any) => void;
// isLoadingButtonSubmit: boolean;
// agreementText?: string;
// isPembatalan: boolean;
// isPending: boolean;
// feature: string;
// isWarning: boolean;
// isCountDown: boolean;
}
export
default
function
DialogPenandatangan
({
isOpen
,
onClose
,
title
=
'
Penandatangan
'
,
}:
DialogPenandatanganProps
)
{
const
penandatanganOptions
=
useAppSelector
((
state
:
any
)
=>
state
.
user
.
data
.
signer_npwp
);
const
form
=
useForm
({
mode
:
'
all
'
,
});
// const [isCheckedAgreement, setIsCheckedAgreement] = useState(false);
const
handleClose
=
()
=>
{
form
.
reset
();
onClose
();
};
// const declareOptions = [
// { label: feature === 'spt faktur' ? 'PKP' : 'Wajib Pajak', value: 0 },
// { label: 'Wakil/Kuasa', value: 1 },
// ];
// const handleSubmitLocal = (data: any) => {
// if (isCountDown)
// setCountdown(30); // start countdown saat submit
// else setCountdown(null);
// onSubmit(data); // tetap panggil props onSubmit
// };
return
(
<
Dialog
fullWidth
maxWidth=
"md"
open=
{
isOpen
}
onClose=
{
handleClose
}
aria
-
labelledby=
"dialog-rekap"
>
<
DialogTitle
id=
"dialog-rekap"
>
<
Typography
textTransform=
"capitalize"
fontWeight=
"bold"
variant=
"inherit"
color=
"initial"
>
{
title
}
</
Typography
>
</
DialogTitle
>
<
IconButton
aria
-
label=
"close"
onClick=
{
handleClose
}
sx=
{
(
theme
:
any
)
=>
({
position
:
'
absolute
'
,
right
:
8
,
top
:
8
,
color
:
theme
.
palette
.
grey
[
500
],
})
}
>
<
Close
/>
</
IconButton
>
<
DialogContent
>
{
/* <form onSubmit={form.handleSubmit(handleSubmitLocal)}> */
}
<
Field
.
Autocomplete
name=
"nikNpwpTtd"
label=
"NPWP/NIK Penandatangan"
options=
{
[{
value
:
penandatanganOptions
,
label
:
`NAMA${penandatanganOptions}`
}]
}
sx=
{
{
background
:
'
white
'
}
}
/>
{
/*
<Agreement
isCheckedAgreement={isCheckedAgreement}
setIsCheckedAgreement={setIsCheckedAgreement}
text={agreementText}
/>
<LoadingButton
loading={isLoadingButtonSubmit}
disabled={!isCheckedAgreement}
variant="contained"
type="submit"
>
Save
</LoadingButton> */
}
{
/* </form> */
}
</
DialogContent
>
</
Dialog
>
);
}
src/sections/bupot-21-26/bupot-bulanan/components/CustomColumnsButton.tsx
0 → 100644
View file @
4a474f6c
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-bulanan/components/CustomFilterButton.tsx
0 → 100644
View file @
4a474f6c
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-bulanan/components/CustomToolbar.tsx
0 → 100644
View file @
4a474f6c
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-bulanan/components/DialogPenandatangan.tsx
0 → 100644
View file @
4a474f6c
import
{
LoadingButton
}
from
'
@mui/lab
'
;
import
{
Grid
,
MenuItem
,
Stack
}
from
'
@mui/material
'
;
import
type
{
GridRowSelectionModel
}
from
'
@mui/x-data-grid-premium
'
;
import
{
useQueryClient
}
from
'
@tanstack/react-query
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
{
useEffect
,
useState
}
from
'
react
'
;
import
{
FormProvider
,
useForm
}
from
'
react-hook-form
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
Agreement
from
'
src/shared/components/agreement/Agreement
'
;
import
DialogProgressBar
from
'
src/shared/components/dialog/DialogProgressBar
'
;
import
DialogUmum
from
'
src/shared/components/dialog/DialogUmum
'
;
import
useDialogProgressBar
from
'
src/shared/hooks/useDialogProgressBar
'
;
import
{
useAppSelector
}
from
'
src/store
'
;
import
queryKey
from
'
../constant/queryKey
'
;
import
useUploadBulanan
from
'
../hooks/useUploadeBulanan
'
;
interface
DialogPenandatanganProps
{
dataSelected
?:
GridRowSelectionModel
;
setSelectionModel
?:
React
.
Dispatch
<
React
.
SetStateAction
<
GridRowSelectionModel
|
undefined
>>
;
tableApiRef
?:
React
.
MutableRefObject
<
any
>
;
isOpenDialogUpload
:
boolean
;
setIsOpenDialogUpload
:
(
v
:
boolean
)
=>
void
;
successMessage
?:
string
;
onConfirmUpload
?:
()
=>
Promise
<
void
>
|
void
;
}
const
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
DialogPenandatangan
:
React
.
FC
<
DialogPenandatanganProps
>
=
({
dataSelected
,
setSelectionModel
,
tableApiRef
,
isOpenDialogUpload
,
setIsOpenDialogUpload
,
successMessage
=
'
Data berhasil diupload
'
,
onConfirmUpload
,
})
=>
{
const
[
isOpenDialogProgressBar
,
setIsOpenDialogProgressBar
]
=
useState
(
false
);
const
[
isCheckedAgreement
,
setIsCheckedAgreement
]
=
useState
<
boolean
>
(
false
);
const
signer
=
useAppSelector
((
state
:
any
)
=>
state
.
user
.
data
.
signer
);
const
queryClient
=
useQueryClient
();
const
methods
=
useForm
({
defaultValues
:
{
signer
:
signer
||
''
,
},
});
const
{
numberOfData
,
setNumberOfData
,
numberOfDataFail
,
numberOfDataProcessed
,
numberOfDataSuccess
,
processSuccess
,
processFail
,
resetToDefault
,
status
,
}
=
useDialogProgressBar
();
const
{
mutateAsync
,
isPending
}
=
useUploadBulanan
({
onSuccess
:
()
=>
processSuccess
(),
onError
:
()
=>
processFail
(),
});
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
'
});
handleCloseModal
();
clearSelection
();
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
?.
message
||
'
Gagal upload data
'
,
{
variant
:
'
error
'
});
}
finally
{
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient
.
invalidateQueries
({
queryKey
:
queryKey
.
bulanan
.
all
(
''
)
});
}
};
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
}
>
<
LoadingButton
type=
"button"
disabled=
{
!
isCheckedAgreement
}
// onClick={onSubmit}
onClick=
{
async
()
=>
{
if
(
onConfirmUpload
)
{
await
onConfirmUpload
();
setIsOpenDialogUpload
(
false
);
return
;
}
await
onSubmit
();
}
}
loading=
{
isPending
}
variant=
"contained"
sx=
{
{
background
:
'
#143B88
'
}
}
>
Save
</
LoadingButton
>
</
Stack
>
</
Stack
>
</
DialogUmum
>
</
FormProvider
>
<
DialogProgressBar
isOpen=
{
isOpenDialogProgressBar
}
handleClose=
{
()
=>
{
handleCloseModal
();
setIsOpenDialogProgressBar
(
false
);
}
}
numberOfData=
{
numberOfData
}
numberOfDataProcessed=
{
numberOfDataProcessed
}
numberOfDataFail=
{
numberOfDataFail
}
numberOfDataSuccess=
{
numberOfDataSuccess
}
status=
{
status
}
/>
</>
);
};
export
default
DialogPenandatangan
;
src/sections/bupot-21-26/bupot-bulanan/constant/index.tsx
View file @
4a474f6c
...
...
@@ -206,20 +206,6 @@ export const FG_BUPOT = {
BULANAN
:
'
1
'
,
};
// export const FG_STATUS = {
// NORMAL: '0',
// NORMAL_PENGGANTI: '1',
// DIGANTI: '2',
// BATAL: '3',
// HAPUS: '4',
// SUBMITTED: '5',
// DRAFT: '6',
// FAILED: '7',
// PENDING: '8',
// EXTERNAL: '9',
// ON_SCHEDULE: '10',
// };
export
const
FG_STATUS
:
Record
<
string
,
string
>
=
{
'
0
'
:
'
NORMAL
'
,
'
1
'
:
'
NORMAL_PENGGANTI
'
,
...
...
@@ -234,6 +220,13 @@ export const FG_STATUS: Record<string, string> = {
'
10
'
:
'
ON_SCHEDULE
'
,
};
export
const
FG_STATUS_BUPOT
=
{
DRAFT
:
'
DRAFT
'
,
NORMAL_DONE
:
'
NORMAL-Done
'
,
AMENDED
:
'
AMENDED
'
,
CANCELLED
:
'
CANCELLED
'
,
};
export
const
FG_PDF_STATUS
=
{
TERBENTUK
:
'
0
'
,
BELUM_TERBENTUK
:
'
1
'
,
...
...
src/sections/bupot-21-26/bupot-bulanan/constant/queryKey.tsx
View file @
4a474f6c
const
appRootKey
=
'
bupot
'
;
export
const
appRootKey
=
'
bupot-21-26
'
;
export
const
bulanan
=
'
bulanan
'
;
const
queryKey
=
{
getKodeObjekPajak
:
(
params
:
any
)
=>
[
appRootKey
,
'
kode-objek-pajak
'
,
params
],
...
...
src/sections/bupot-21-26/bupot-bulanan/hooks/useAdvanceSearch.tsx
deleted
100644 → 0
View file @
9ffe9ff5
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-bulanan/hooks/useAdvancedFilter.tsx
0 → 100644
View file @
4a474f6c
type
FilterItem
=
{
field
:
string
;
operator
:
string
;
value
?:
string
|
number
|
Array
<
string
|
number
>
|
null
;
join
?:
'
AND
'
|
'
OR
'
;
};
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
);
}
}
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
;
}
/**
* ✅ FIXED: Clean undefined values dan handle sorting dengan benar
*/
function
buildRequestParams
(
base
:
BaseParams
=
{},
advanced
:
string
)
{
const
out
:
BaseParams
=
{};
// ✅ Copy semua base params kecuali yang undefined
Object
.
keys
(
base
).
forEach
((
key
)
=>
{
if
(
base
[
key
]
!==
undefined
)
{
out
[
key
]
=
base
[
key
];
}
});
// ✅ Field mapping
if
(
'
noBupot
'
in
out
)
{
out
.
nomorBupot
=
out
.
noBupot
;
delete
out
.
noBupot
;
}
// ✅ Hanya tambahkan advanced jika ada isinya
if
(
advanced
&&
advanced
.
trim
()
!==
''
)
{
out
.
advanced
=
advanced
.
trim
();
}
// ✅ Clean up undefined sorting (jangan kirim ke backend)
if
(
out
.
sortingMode
===
undefined
)
{
delete
out
.
sortingMode
;
}
if
(
out
.
sortingMethod
===
undefined
)
{
delete
out
.
sortingMethod
;
}
return
out
;
}
return
{
buildAdvancedFilter
,
buildRequestParams
}
as
const
;
}
src/sections/bupot-21-26/bupot-bulanan/hooks/useGetBulanan.ts
View file @
4a474f6c
...
...
@@ -75,13 +75,11 @@ const normalisePropsGetBulanan = (params: TGetListDataTableDn) => ({
idDipotong
:
params
.
userId
,
});
const
normalisPropsParmas
GetDn
=
(
params
:
any
)
=>
{
const
normalisPropsParmas
=
(
params
:
any
)
=>
{
const
sorting
=
!
isEmpty
(
params
.
sort
)
?
transformSortModelToSortApiPayload
(
params
.
sort
)
:
{};
return
{
...
params
,
page
:
params
.
Page
,
limit
:
params
.
Limit
,
masaPajak
:
params
.
msPajak
||
null
,
tahunPajak
:
params
.
thnPajak
||
null
,
npwp
:
params
.
idDipotong
||
null
,
...
...
@@ -93,7 +91,7 @@ const useGetBulanan = ({ params, ...props }: any) => {
const
query
=
useQuery
<
TBaseResponseAPI
<
TGetListDataTableDnResult
>>
({
queryKey
:
queryKey
.
bulanan
.
all
(
params
),
queryFn
:
async
()
=>
{
const
response
=
await
bulananApi
.
getList
({
params
:
normalisPropsParmas
GetDn
(
params
)
});
const
response
=
await
bulananApi
.
getList
({
params
:
normalisPropsParmas
(
params
)
});
return
{
...
response
,
...
...
src/sections/bupot-21-26/bupot-bulanan/view/bulanan-list-view.tsx
View file @
4a474f6c
This diff is collapsed.
Click to expand it.
src/sections/bupot-21-26/bupot-bulanan/view/bulanan-rekam-view.tsx
View file @
4a474f6c
...
...
@@ -4,12 +4,11 @@ import Divider from '@mui/material/Divider';
import
Grid
from
'
@mui/material/Grid
'
;
import
Stack
from
'
@mui/material/Stack
'
;
import
dayjs
from
'
dayjs
'
;
import
{
Suspense
,
useMemo
,
useState
}
from
'
react
'
;
import
{
Suspense
,
use
Effect
,
use
Memo
,
useState
}
from
'
react
'
;
import
{
FormProvider
,
useForm
}
from
'
react-hook-form
'
;
import
{
CustomBreadcrumbs
}
from
'
src/components/custom-breadcrumbs
'
;
import
{
Field
}
from
'
src/components/hook-form
'
;
import
{
DashboardContent
}
from
'
src/layouts/dashboard
'
;
import
{
usePathname
}
from
'
src/routes/hooks
'
;
import
{
paths
}
from
'
src/routes/paths
'
;
import
Agreement
from
'
src/shared/components/agreement/Agreement
'
;
import
HeadingRekam
from
'
src/shared/components/HeadingRekam
'
;
...
...
@@ -20,15 +19,17 @@ import JumlahPerhitunganForm from '../components/rekam/JumlahPerhitunganForm';
import
PanduanDnRekam
from
'
../components/rekam/PanduanDnRekam
'
;
import
PerhitunganPPhPasal21
from
'
../components/rekam/PerhitunganPPhPasal21
'
;
import
{
ActionRekam
,
FG_FASILITAS_PPH_21
,
FG_FASILITAS_PPH_21_TEXT
,
KODE_OBJEK_PAJAK
,
KODE_OBJEK_PAJAK_TEXT
,
}
from
'
../constant
'
;
import
{
checkCurrentPage
}
from
'
../utils/utils
'
;
import
useSaveBulanan
from
'
../hooks/useSaveBulanan
'
;
import
DialogPenandatangan
from
'
../../DialogPenandatangan
'
;
import
DialogPenandatangan
from
'
../components/DialogPenandatangan
'
;
import
useUploadBulanan
from
'
../hooks/useUploadeBulanan
'
;
import
{
useNavigate
,
useParams
}
from
'
react-router
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
useGetBulanan
from
'
../hooks/useGetBulanan
'
;
const
bulananSchema
=
z
.
object
({
...
...
@@ -168,16 +169,21 @@ const bulananSchema = z
);
export
const
BulananRekamView
=
()
=>
{
// const { id } = useParams
();
const
pathname
=
usePathnam
e
();
const
{
id
,
type
}
=
useParams
<
{
id
?:
string
;
type
?:
'
ubah
'
|
'
pengganti
'
|
'
new
'
}
>
();
const
navigate
=
useNavigat
e
();
const
{
mutate
:
saveBulanan
,
isPending
:
isSaving
}
=
useSaveBulanan
();
const
{
mutate
:
saveBulanan
,
isPending
:
isSaving
}
=
useSaveBulanan
({
onSuccess
:
()
=>
enqueueSnackbar
(
'
Data berhasil disimpan
'
,
{
variant
:
'
success
'
}),
});
const
{
mutate
:
uploadBulanan
,
isPending
:
isUpload
}
=
useUploadBulanan
();
const
[
isOpenPanduan
,
setIsOpenPanduan
]
=
useState
<
boolean
>
(
false
);
const
[
isCheckedAgreement
,
setIsCheckedAgreement
]
=
useState
<
boolean
>
(
false
);
const
[
isOpenDialogPenandatangan
,
setIsOpenDialogPenandatangan
]
=
useState
(
false
);
const
actionRekam
=
checkCurrentPage
(
pathname
);
const
isEdit
=
type
===
'
ubah
'
;
const
isPengganti
=
type
===
'
pengganti
'
;
const
dataListKOP
=
useMemo
(
()
=>
[
KODE_OBJEK_PAJAK
.
BULANAN_01
,
KODE_OBJEK_PAJAK
.
BULANAN_02
,
KODE_OBJEK_PAJAK
.
BULANAN_03
].
map
(
...
...
@@ -189,6 +195,15 @@ export const BulananRekamView = () => {
[]
);
const
{
data
:
existingBulanan
,
isLoading
:
isLoadingBulanan
}
=
useGetBulanan
({
params
:
{
page
:
1
,
limit
:
1
,
id
,
},
enabled
:
!!
id
,
});
type
BpuFormData
=
z
.
infer
<
typeof
bulananSchema
>
;
const
handleOpenPanduan
=
()
=>
setIsOpenPanduan
(
!
isOpenPanduan
);
...
...
@@ -236,13 +251,22 @@ export const BulananRekamView = () => {
defaultValues
,
});
console
.
log
(
'
🚀 ~ BulananRekamView ~ methods:
'
,
methods
.
formState
.
errors
);
useEffect
(()
=>
{
if
((
isEdit
||
isPengganti
)
&&
existingBulanan
&&
!
isLoadingBulanan
)
{
const
normalized
=
{
...
existingBulanan
,
};
methods
.
reset
(
normalized
as
any
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
isEdit
,
isPengganti
,
existingBulanan
,
isLoadingBulanan
]);
const
handleDraft
=
async
(
data
:
BpuFormData
)
=>
{
// Transform data sesuai dengan struktur yang dibutuhkan
const
transformedData
=
{
...
data
,
id
:
actionRekam
===
ActionRekam
.
UBAH
?
data
.
id
:
undefined
,
id
:
isEdit
||
isPengganti
?
data
.
id
:
undefined
,
msPajak
:
data
.
masaPajak
,
thnPajak
:
data
.
tahunPajak
,
passportNo
:
data
.
passport
||
''
,
...
...
@@ -263,13 +287,42 @@ export const BulananRekamView = () => {
await
saveBulanan
(
transformedData
);
};
const
handleUploud
=
async
(
data
:
BpuFormData
)
=>
{
try
{
const
response
=
await
handleDraft
(
data
);
uploadBulanan
({
id
:
response
?.
id
??
''
,
});
enqueueSnackbar
(
'
Berhasil Menyimpan Data
'
,
{
variant
:
'
success
'
});
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
.
message
,
{
variant
:
'
error
'
});
}
finally
{
navigate
(
'
/pph21/bulanan
'
);
}
};
const
handleClickUpload
=
async
()
=>
{
setIsOpenDialogPenandatangan
(
true
);
};
const
SubmitRekam
=
async
(
data
:
BpuFormData
)
=>
{
try
{
const
respon
=
await
handleDraft
(
data
);
console
.
log
({
respon
});
enqueueSnackbar
(
isEdit
?
'
Data berhasil diperbarui
'
:
isPengganti
?
'
Data pengganti berhasil disimpan
'
:
'
Data berhasil disimpan
'
,
{
variant
:
'
success
'
}
);
navigate
(
'
/pph21/bulanan
'
);
}
catch
(
error
:
any
)
{
enqueueSnackbar
(
error
.
message
||
'
Gagal menyimpan data
'
,
{
variant
:
'
error
'
});
console
.
error
(
'
❌ SaveDn error:
'
,
error
);
}
};
const
MockNitku
=
[
...
...
@@ -345,7 +398,7 @@ export const BulananRekamView = () => {
type=
"button"
disabled=
{
!
isCheckedAgreement
}
onClick=
{
methods
.
handleSubmit
(
handleClickUpload
)
}
loading=
{
isSaving
}
loading=
{
isSaving
||
isUpload
}
variant=
"contained"
sx=
{
{
background
:
'
#143B88
'
}
}
>
...
...
@@ -361,12 +414,13 @@ export const BulananRekamView = () => {
</
Grid
>
</
Grid
>
</
DashboardContent
>
{
isOpenDialogPenandatangan
&&
(
<
DialogPenandatangan
isOpen=
{
isOpenDialogPenandatangan
}
onClose=
{
()
=>
{
setIsOpenDialogPenandatangan
(
false
);
}
}
isOpenDialogUpload=
{
isOpenDialogPenandatangan
}
setIsOpenDialogUpload=
{
setIsOpenDialogPenandatangan
}
onConfirmUpload=
{
()
=>
methods
.
handleSubmit
(
handleUploud
)()
}
/>
)
}
</>
);
};
src/sections/bupot-21-26/paginationStore.ts
0 → 100644
View file @
4a474f6c
import
{
create
}
from
'
zustand
'
;
import
{
devtools
,
persist
}
from
'
zustand/middleware
'
;
import
{
produce
}
from
'
immer
'
;
type
TableKey
=
string
;
interface
TablePagination
{
page
:
number
;
// 0-based untuk MUI DataGrid
pageSize
:
number
;
}
interface
PaginationState
{
tables
:
Record
<
TableKey
,
TablePagination
>
;
}
interface
PaginationActions
{
setPagination
:
(
table
:
TableKey
,
next
:
Partial
<
TablePagination
>
)
=>
void
;
resetPagination
:
(
table
:
TableKey
)
=>
void
;
resetAllPaginations
:
()
=>
void
;
getPagination
:
(
table
:
TableKey
)
=>
TablePagination
;
removePagination
:
(
table
:
TableKey
)
=>
void
;
}
type
PaginationStore
=
PaginationState
&
PaginationActions
;
// ✅ Default untuk MUI DataGrid (0-based)
const
DEFAULT_PAGINATION
:
Readonly
<
TablePagination
>
=
Object
.
freeze
({
page
:
0
,
// 0-based untuk MUI
pageSize
:
10
,
});
const
STORAGE_KEY
=
'
pagination-storage
'
;
export
const
usePaginationStore
=
create
<
PaginationStore
>
()(
devtools
(
persist
(
(
set
,
get
)
=>
({
tables
:
{},
setPagination
:
(
table
,
next
)
=>
{
set
(
produce
<
PaginationStore
>
((
draft
)
=>
{
const
current
=
draft
.
tables
[
table
]
??
{
...
DEFAULT_PAGINATION
};
draft
.
tables
[
table
]
=
{
page
:
next
.
page
??
current
.
page
,
pageSize
:
next
.
pageSize
??
current
.
pageSize
,
};
}),
false
,
{
type
:
'
SET_PAGINATION
'
,
table
,
next
}
);
},
resetPagination
:
(
table
)
=>
{
set
(
produce
<
PaginationStore
>
((
draft
)
=>
{
const
currentPageSize
=
draft
.
tables
[
table
]?.
pageSize
??
DEFAULT_PAGINATION
.
pageSize
;
draft
.
tables
[
table
]
=
{
page
:
DEFAULT_PAGINATION
.
page
,
pageSize
:
currentPageSize
,
};
}),
false
,
{
type
:
'
RESET_PAGINATION
'
,
table
}
);
},
resetAllPaginations
:
()
=>
{
set
({
tables
:
{}
},
false
,
{
type
:
'
RESET_ALL_PAGINATIONS
'
});
},
getPagination
:
(
table
)
=>
{
const
state
=
get
();
return
state
.
tables
[
table
]
??
{
...
DEFAULT_PAGINATION
};
},
removePagination
:
(
table
)
=>
{
set
(
produce
<
PaginationStore
>
((
draft
)
=>
{
delete
draft
.
tables
[
table
];
}),
false
,
{
type
:
'
REMOVE_PAGINATION
'
,
table
}
);
},
}),
{
name
:
STORAGE_KEY
,
partialize
:
(
state
)
=>
({
tables
:
state
.
tables
}),
}
),
{
name
:
'
PaginationStore
'
,
enabled
:
process
.
env
.
NODE_ENV
===
'
development
'
}
)
);
// ============================================================================
// CUSTOM HOOKS WITH 1-BASED CONVERSION
// ============================================================================
/**
* ✅ Hook dengan konversi otomatis ke 1-based untuk backend
* MUI DataGrid: 0-based (page 0, 1, 2, ...)
* Backend API: 1-based (page 1, 2, 3, ...)
*/
export
const
useTablePagination
=
(
tableKey
:
TableKey
)
=>
{
const
pagination
=
usePaginationStore
((
s
)
=>
s
.
tables
[
tableKey
]
??
DEFAULT_PAGINATION
);
const
setPagination
=
usePaginationStore
((
s
)
=>
s
.
setPagination
);
const
resetPagination
=
usePaginationStore
((
s
)
=>
s
.
resetPagination
);
return
[
pagination
,
// untuk MUI DataGrid (0-based)
(
next
:
Partial
<
TablePagination
>
)
=>
setPagination
(
tableKey
,
next
),
()
=>
resetPagination
(
tableKey
),
]
as
const
;
};
/**
* ✅ Hook khusus yang return page dalam format 1-based untuk API
*/
export
const
useTablePaginationForAPI
=
(
tableKey
:
TableKey
)
=>
{
const
pagination
=
usePaginationStore
((
s
)
=>
s
.
tables
[
tableKey
]
??
DEFAULT_PAGINATION
);
return
{
page
:
pagination
.
page
+
1
,
// Convert to 1-based
pageSize
:
pagination
.
pageSize
,
limit
:
pagination
.
pageSize
,
// alias
};
};
export
const
useTablePage
=
(
tableKey
:
TableKey
)
=>
usePaginationStore
((
s
)
=>
s
.
tables
[
tableKey
]?.
page
??
DEFAULT_PAGINATION
.
page
);
export
const
useTablePageSize
=
(
tableKey
:
TableKey
)
=>
usePaginationStore
((
s
)
=>
s
.
tables
[
tableKey
]?.
pageSize
??
DEFAULT_PAGINATION
.
pageSize
);
export
const
createTableKey
=
(...
parts
:
string
[]):
TableKey
=>
parts
.
filter
(
Boolean
).
join
(
'
-
'
);
export
type
{
TableKey
,
TablePagination
,
PaginationStore
};
export
{
DEFAULT_PAGINATION
};
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