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
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
// import React from 'react';
// import {
// IconButton,
// Popover,
// Stack,
// TextField,
// MenuItem,
// Button,
// Badge,
// Divider,
// IconButton as MuiIconButton,
// Autocomplete,
// Chip,
// } from '@mui/material';
// import { FilterList as FilterListIcon, Close as CloseIcon } from '@mui/icons-material';
// import { GridFilterModel, GridColDef, GridFilterItem } from '@mui/x-data-grid-premium';
// interface StatusOption {
// value: string;
// label: string;
// }
// interface Props {
// columns: GridColDef[];
// filterModel: GridFilterModel;
// setFilterModel: (model: GridFilterModel) => void;
// statusOptions?: StatusOption[]; // for fgStatus label lookup & options
// debounceMs?: number;
// }
// // helper operator lists
// const TEXT_OPS = [
// { value: 'contains', label: 'contains' },
// { value: 'equals', label: 'equals' },
// { value: 'is empty', label: 'is empty' },
// { value: 'is not empty', label: 'is not empty' },
// { value: 'is any of', label: 'is any of' },
// ];
// const NUMERIC_OPS = [
// { value: '=', label: '=' },
// { value: '>=', label: '>=' },
// { value: '<=', label: '<=' },
// ];
// const STATUS_OPS = [
// { value: 'is', label: 'is' },
// { value: 'is not', label: 'is not' },
// { value: 'is any of', label: 'is any of' },
// ];
// const DATE_OPS = [
// { value: 'is', label: 'is' },
// { value: 'is on or after', label: 'is on or after' },
// { value: 'is on or before', label: 'is on or before' },
// ];
// const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
// const dateFields = new Set(['created_at', 'updated_at']);
// function getOperatorOptionsForField(field?: string) {
// if (!field) return TEXT_OPS;
// if (field === 'fgStatus') return STATUS_OPS;
// if (numericFields.has(field)) return NUMERIC_OPS;
// if (dateFields.has(field)) return DATE_OPS;
// // default text-like
// return TEXT_OPS;
// }
// function defaultOperatorForField(field?: string) {
// if (!field) return 'contains';
// if (field === 'fgStatus') return 'is';
// if (numericFields.has(field)) return '=';
// if (dateFields.has(field)) return 'is';
// return 'contains';
// }
// // Memoized component
// export const CustomFilterButton: React.FC<Props> = React.memo(
// ({ columns, filterModel, setFilterModel, statusOptions = [], debounceMs = 400 }) => {
// const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
// // Local copy for editing inside popover (do not flush to parent on open)
// const [localItems, setLocalItems] = React.useState<GridFilterModel['items']>(
// () => filterModel?.items ?? []
// );
// // Keep local items in sync when parent filterModel changes externally
// React.useEffect(() => {
// setLocalItems(filterModel?.items ?? []);
// }, [filterModel?.items]);
// const debounceRef = React.useRef<number | null>(null);
// const clearDebounce = React.useCallback(() => {
// if (debounceRef.current !== null) {
// window.clearTimeout(debounceRef.current);
// debounceRef.current = null;
// }
// }, []);
// const applyImmediate = React.useCallback(
// (items: GridFilterModel['items']) => {
// clearDebounce();
// setFilterModel({ items });
// },
// [clearDebounce, setFilterModel]
// );
// const applyDebounced = React.useCallback(
// (items: GridFilterModel['items']) => {
// clearDebounce();
// debounceRef.current = window.setTimeout(() => {
// setFilterModel({ items });
// debounceRef.current = null;
// }, debounceMs) as unknown as number;
// },
// [clearDebounce, debounceMs, setFilterModel]
// );
// React.useEffect(() => clearDebounce, [clearDebounce]);
// // active filters count badge
// const activeCount =
// Number(
// (filterModel?.items || []).filter((i) => {
// if (!i || !i.field) return false;
// // count only if operator does not require no-value OR (value present) OR operator is 'is any of' with some values
// const op = String(i.operator ?? '').toLowerCase();
// if (op === 'is empty' || op === 'is not empty') return true;
// if (op === 'is any of')
// return Array.isArray(i.value) ? (i.value as any[]).length > 0 : !!i.value;
// return i.value !== undefined && i.value !== '';
// }).length
// ) || 0;
// const getCol = React.useCallback(
// (field?: string) => columns.find((c) => c.field === field),
// [columns]
// );
// // stable ref for the button so anchor stays valid across parent re-renders
// const buttonRef = React.useRef<HTMLButtonElement | null>(null);
// // OPEN: do NOT update parent state here (to avoid parent re-render before popover opens)
// const handleClick = (e: React.MouseEvent<HTMLElement>) => {
// e.stopPropagation();
// if (!filterModel?.items?.length) {
// const init = [
// {
// field: columns[0]?.field ?? '',
// operator: defaultOperatorForField(columns[0]?.field),
// value: '',
// },
// ];
// setLocalItems(init);
// } else {
// setLocalItems(filterModel.items ?? []);
// }
// setAnchorEl(buttonRef.current);
// };
// const handleClose = () => {
// setAnchorEl(null);
// };
// // add/remove operate only on localItems now (no immediate parent update)
// const addFilter = React.useCallback(() => {
// setLocalItems((prev) => {
// const next = [
// ...(prev || []),
// {
// field: columns[0]?.field ?? '',
// operator: defaultOperatorForField(columns[0]?.field),
// value: '',
// },
// ];
// return next;
// });
// }, [columns]);
// const removeFilter = React.useCallback((index: number) => {
// setLocalItems((prev) => (prev || []).filter((_, i) => i !== index));
// }, []);
// const handleClearAll = React.useCallback(() => {
// clearDebounce();
// setLocalItems([]);
// setFilterModel({ items: [] });
// }, [clearDebounce, setFilterModel]);
// // updateFilter: when user changes field/operator/value inside popover
// const updateFilter = React.useCallback(
// (index: number, key: keyof GridFilterModel['items'][0], value: any) => {
// setLocalItems((prev) => {
// const next = Array.isArray(prev) ? [...prev] : [];
// const old = next[index] ?? {
// field: columns[0]?.field ?? '',
// operator: defaultOperatorForField(columns[0]?.field),
// value: '',
// };
// let updated = { ...old, [key]: value };
// // if field changed -> reset operator/value sensibly
// if (key === 'field') {
// const newField = value as string;
// const defOp = defaultOperatorForField(newField) as any;
// updated = { ...updated, field: newField, operator: defOp, value: '' };
// next[index] = updated;
// return next;
// }
// // if operator changed -> reset value if operator is an empty-kind or multi-kind
// if (key === 'operator') {
// const op = String(value).toLowerCase();
// if (op === 'is empty' || op === 'is not empty') {
// updated = { ...updated, operator: value, value: '' };
// next[index] = updated;
// // persist operator-only change? we'll debounce on value changes; operator change itself doesn't require immediate apply
// return next;
// }
// if (op === 'is any of') {
// // set value to array if not already
// if (!Array.isArray(updated.value)) updated.value = [];
// updated = { ...updated, operator: value };
// next[index] = updated;
// return next;
// }
// // other operators: keep value but ensure type (string)
// updated = { ...updated, operator: value, value: String(updated.value ?? '') };
// next[index] = updated;
// return next;
// }
// // if value changed -> decide immediate vs debounce depending on column type
// next[index] = updated;
// const items = next;
// const col = getCol(items[index].field as string);
// // detect singleSelect
// const isSingleSelect =
// !!col && (col.type === 'singleSelect' || Array.isArray((col as any).valueOptions));
// if (isSingleSelect) {
// applyImmediate(items);
// } else {
// applyDebounced(items);
// }
// return items;
// });
// },
// [applyDebounced, applyImmediate, columns, getCol]
// );
// // APPLY button: persist localItems to parent and close
// const handleApply = React.useCallback(() => {
// clearDebounce();
// setFilterModel({ items: localItems || [] });
// setAnchorEl(null);
// }, [localItems, clearDebounce, setFilterModel]);
// return (
// <>
// <Badge badgeContent={activeCount} color="primary" overlap="circular">
// <IconButton
// size="small"
// ref={buttonRef}
// onMouseDown={(e) => e.stopPropagation()}
// onClick={handleClick}
// sx={{ color: '#123375' }}
// aria-label="filter"
// >
// <FilterListIcon />
// </IconButton>
// </Badge>
// <Popover
// open={Boolean(anchorEl)}
// anchorEl={anchorEl}
// onClose={handleClose}
// keepMounted
// anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
// transformOrigin={{ vertical: 'top', horizontal: 'right' }}
// PaperProps={{ sx: { p: 2, minWidth: 540, maxWidth: '95vw' } }}
// >
// <Stack gap={2}>
// {(localItems || []).map((item, idx) => {
// const colDef = getCol(item.field as string);
// const operatorOptions = getOperatorOptionsForField(item.field);
// const op = String(item.operator ?? defaultOperatorForField(item.field));
// // value options for select fields
// const valueOptions: string[] =
// (colDef && (colDef as any).valueOptions) ||
// (colDef && colDef.type === 'singleSelect'
// ? ((colDef as any).valueOptions ?? [])
// : []);
// // if fgStatus, source options from statusOptions prop
// const statusVals = statusOptions.map((s) => s.value);
// const isNoValueOp = op === 'is empty' || op === 'is not empty';
// const isAnyOfOp = op === 'is any of';
// const isDateField = dateFields.has(item.field as string);
// const isNumericField = numericFields.has(item.field as string);
// const isStatusField = item.field === 'fgStatus';
// return (
// <Stack
// key={idx}
// direction="row"
// spacing={1}
// alignItems="center"
// sx={{ width: '100%' }}
// >
// {/* Column */}
// <TextField
// select
// size="small"
// label="Column"
// value={item.field ?? ''}
// onChange={(e) => updateFilter(idx, 'field', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ minWidth: 180 }}
// >
// {columns.map((col) => (
// <MenuItem key={String(col.field)} value={col.field}>
// {col.headerName ?? String(col.field)}
// </MenuItem>
// ))}
// </TextField>
// {/* Operator */}
// <TextField
// select
// size="small"
// label="Operator"
// value={op}
// onChange={(e) => updateFilter(idx, 'operator', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ minWidth: 160 }}
// >
// {operatorOptions.map((o) => (
// <MenuItem key={o.value} value={o.value}>
// {o.label}
// </MenuItem>
// ))}
// </TextField>
// {/* Value */}
// {isNoValueOp ? (
// // show disabled placeholder when operator does not require a value
// <TextField
// size="small"
// label="Value"
// placeholder="No value required"
// value=""
// disabled
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// />
// ) : isAnyOfOp ? (
// // multi-value input (free text or from options)
// <Autocomplete
// multiple
// freeSolo
// size="small"
// options={
// isStatusField ? statusVals : ((valueOptions as string[] | undefined) ?? [])
// }
// value={
// Array.isArray(item.value) ? item.value : item.value ? [item.value] : []
// }
// onChange={(_, values) => {
// updateFilter(idx, 'value', values);
// }}
// renderTags={(value: any[], getTagProps) =>
// value.map((option: any, index: number) => (
// <Chip
// variant="outlined"
// label={String(option)}
// {...getTagProps({ index })}
// />
// ))
// }
// renderInput={(params) => (
// <TextField
// {...params}
// label="Value(s)"
// placeholder="Select values..."
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// />
// )}
// sx={{ flex: 1, minWidth: 160 }}
// />
// ) : isDateField ? (
// // date input (yyyy-mm-dd)
// <TextField
// size="small"
// label="Value"
// type="date"
// value={item.value ? String(item.value).slice(0, 10) : ''}
// onChange={(e) => updateFilter(idx, 'value', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// />
// ) : colDef &&
// (colDef.type === 'singleSelect' ||
// Array.isArray((colDef as any).valueOptions)) ? (
// // single select (enum)
// <TextField
// select
// size="small"
// label="Value"
// value={item.value ?? ''}
// onChange={(e) => updateFilter(idx, 'value', e.target.value)}
// InputLabelProps={{ shrink: true }}
// sx={{ flex: 1, minWidth: 160 }}
// >
// {((colDef as any).valueOptions ?? (isStatusField ? statusVals : [])).map(
// (opt: any) => (
// <MenuItem key={String(opt)} value={opt}>
// {isStatusField
// ? (statusOptions.find((s) => s.value === opt)?.label ?? String(opt))
// : String(opt)}
// </MenuItem>
// )
// )}
// </TextField>
// ) : (
// // normal text / numeric input
// <TextField
// size="small"
// label="Value"
// placeholder="Filter Value"
// value={item.value ?? ''}
// onChange={(e) => {
// const v = e.target.value;
// if (isNumericField) {
// // allow only digits
// const digits = v.replace(/[^0-9]/g, '');
// updateFilter(idx, 'value', digits);
// } else {
// updateFilter(idx, 'value', v);
// }
// }}
// InputLabelProps={{ shrink: true }}
// inputProps={isNumericField ? { inputMode: 'numeric', pattern: '[0-9]*' } : {}}
// sx={{ flex: 1, minWidth: 160 }}
// />
// )}
// <MuiIconButton
// size="small"
// onClick={(e) => {
// e.stopPropagation();
// removeFilter(idx);
// }}
// >
// <CloseIcon fontSize="small" />
// </MuiIconButton>
// </Stack>
// );
// })}
// <Divider />
// <Stack direction="row" justifyContent="space-between" alignItems="center">
// <Button
// size="small"
// variant="outlined"
// onClick={(e) => {
// e.stopPropagation();
// addFilter();
// }}
// >
// + Add filter
// </Button>
// <Stack direction="row" spacing={1}>
// <Button
// size="small"
// onClick={(e) => {
// e.stopPropagation();
// handleClearAll();
// }}
// >
// Clear
// </Button>
// <Button
// size="small"
// variant="contained"
// onClick={(e) => {
// e.stopPropagation();
// handleApply();
// }}
// >
// Apply
// </Button>
// </Stack>
// </Stack>
// </Stack>
// </Popover>
// </>
// );
// }
// );
// export default CustomFilterButton;
import
React
from
'
react
'
;
import
{
IconButton
,
Popover
,
Stack
,
TextField
,
MenuItem
,
Button
,
Badge
,
Divider
,
IconButton
as
MuiIconButton
,
Autocomplete
,
Chip
,
}
from
'
@mui/material
'
;
import
{
FilterList
as
FilterListIcon
,
Close
as
CloseIcon
}
from
'
@mui/icons-material
'
;
import
type
{
GridFilterModel
,
GridColDef
}
from
'
@mui/x-data-grid-premium
'
;
interface
StatusOption
{
value
:
string
;
label
:
string
;
}
interface
Props
{
columns
:
GridColDef
[];
filterModel
:
GridFilterModel
;
setFilterModel
:
(
model
:
GridFilterModel
)
=>
void
;
statusOptions
?:
StatusOption
[];
// for fgStatus label lookup & options
debounceMs
?:
number
;
}
type
LocalFilterItem
=
{
field
:
string
;
operator
:
string
;
value
?:
any
;
join
?:
'
AND
'
|
'
OR
'
;
// new: join operator (applies before this item, except first)
};
// helper operator lists
const
TEXT_OPS
=
[
{
value
:
'
contains
'
,
label
:
'
contains
'
},
{
value
:
'
equals
'
,
label
:
'
equals
'
},
{
value
:
'
is empty
'
,
label
:
'
is empty
'
},
{
value
:
'
is not empty
'
,
label
:
'
is not empty
'
},
{
value
:
'
is any of
'
,
label
:
'
is any of
'
},
];
const
NUMERIC_OPS
=
[
{
value
:
'
=
'
,
label
:
'
=
'
},
{
value
:
'
>=
'
,
label
:
'
>=
'
},
{
value
:
'
<=
'
,
label
:
'
<=
'
},
];
const
STATUS_OPS
=
[
{
value
:
'
is
'
,
label
:
'
is
'
},
{
value
:
'
is not
'
,
label
:
'
is not
'
},
{
value
:
'
is any of
'
,
label
:
'
is any of
'
},
];
const
DATE_OPS
=
[
{
value
:
'
is
'
,
label
:
'
is
'
},
{
value
:
'
is on or after
'
,
label
:
'
is on or after
'
},
{
value
:
'
is on or before
'
,
label
:
'
is on or before
'
},
];
const
numericFields
=
new
Set
([
'
masaPajak
'
,
'
tahunPajak
'
,
'
dpp
'
,
'
pphDipotong
'
]);
const
dateFields
=
new
Set
([
'
created_at
'
,
'
updated_at
'
]);
function
getOperatorOptionsForField
(
field
?:
string
)
{
if
(
!
field
)
return
TEXT_OPS
;
if
(
field
===
'
fgStatus
'
)
return
STATUS_OPS
;
if
(
numericFields
.
has
(
field
))
return
NUMERIC_OPS
;
if
(
dateFields
.
has
(
field
))
return
DATE_OPS
;
return
TEXT_OPS
;
}
function
defaultOperatorForField
(
field
?:
string
)
{
if
(
!
field
)
return
'
contains
'
;
if
(
field
===
'
fgStatus
'
)
return
'
is
'
;
if
(
numericFields
.
has
(
field
))
return
'
=
'
;
if
(
dateFields
.
has
(
field
))
return
'
is
'
;
return
'
contains
'
;
}
// Memoized component
export
const
CustomFilterButton
:
React
.
FC
<
Props
>
=
React
.
memo
(
({
columns
,
filterModel
,
setFilterModel
,
statusOptions
=
[],
debounceMs
=
400
})
=>
{
const
[
anchorEl
,
setAnchorEl
]
=
React
.
useState
<
HTMLElement
|
null
>
(
null
);
const
[
localItems
,
setLocalItems
]
=
React
.
useState
<
LocalFilterItem
[]
>
(
()
=>
(
filterModel
?.
items
as
any
)
??
[]
);
React
.
useEffect
(()
=>
{
setLocalItems
((
filterModel
?.
items
as
any
)
??
[]);
},
[
filterModel
?.
items
]);
const
debounceRef
=
React
.
useRef
<
number
|
null
>
(
null
);
const
clearDebounce
=
React
.
useCallback
(()
=>
{
if
(
debounceRef
.
current
!==
null
)
{
window
.
clearTimeout
(
debounceRef
.
current
);
debounceRef
.
current
=
null
;
}
},
[]);
const
applyImmediate
=
React
.
useCallback
(
(
items
:
LocalFilterItem
[])
=>
{
clearDebounce
();
setFilterModel
({
items
:
items
as
any
});
},
[
clearDebounce
,
setFilterModel
]
);
const
applyDebounced
=
React
.
useCallback
(
(
items
:
LocalFilterItem
[])
=>
{
clearDebounce
();
debounceRef
.
current
=
window
.
setTimeout
(()
=>
{
setFilterModel
({
items
:
items
as
any
});
debounceRef
.
current
=
null
;
},
debounceMs
)
as
unknown
as
number
;
},
[
clearDebounce
,
debounceMs
,
setFilterModel
]
);
React
.
useEffect
(()
=>
clearDebounce
,
[
clearDebounce
]);
const
activeCount
=
Number
(
((
filterModel
?.
items
as
any
)
||
[]).
filter
((
i
:
any
)
=>
{
if
(
!
i
||
!
i
.
field
)
return
false
;
const
op
=
String
(
i
.
operator
??
''
).
toLowerCase
();
if
(
op
===
'
is empty
'
||
op
===
'
is not empty
'
)
return
true
;
if
(
op
===
'
is any of
'
)
return
Array
.
isArray
(
i
.
value
)
?
(
i
.
value
as
any
[]).
length
>
0
:
!!
i
.
value
;
return
i
.
value
!==
undefined
&&
i
.
value
!==
''
;
}).
length
)
||
0
;
const
getCol
=
React
.
useCallback
(
(
field
?:
string
)
=>
columns
.
find
((
c
)
=>
c
.
field
===
field
),
[
columns
]
);
const
buttonRef
=
React
.
useRef
<
HTMLButtonElement
|
null
>
(
null
);
const
handleClick
=
(
e
:
React
.
MouseEvent
<
HTMLElement
>
)
=>
{
e
.
stopPropagation
();
if
(
!
filterModel
?.
items
?.
length
)
{
const
init
=
[
{
field
:
columns
[
0
]?.
field
??
''
,
operator
:
defaultOperatorForField
(
columns
[
0
]?.
field
),
value
:
''
,
join
:
undefined
,
}
as
LocalFilterItem
,
];
setLocalItems
(
init
);
}
else
{
setLocalItems
((
filterModel
?.
items
as
any
)
??
[]);
}
setAnchorEl
(
buttonRef
.
current
);
};
const
handleClose
=
()
=>
{
setAnchorEl
(
null
);
};
const
addFilter
=
React
.
useCallback
(()
=>
{
setLocalItems
((
prev
)
=>
{
const
next
=
[
...(
prev
||
[]),
{
field
:
columns
[
0
]?.
field
??
''
,
operator
:
defaultOperatorForField
(
columns
[
0
]?.
field
),
value
:
''
,
join
:
'
AND
'
,
// default join for newly added filters
}
as
LocalFilterItem
,
];
return
next
;
});
},
[
columns
]);
const
removeFilter
=
React
.
useCallback
((
index
:
number
)
=>
{
setLocalItems
((
prev
)
=>
(
prev
||
[]).
filter
((
_
,
i
)
=>
i
!==
index
));
},
[]);
const
handleClearAll
=
React
.
useCallback
(()
=>
{
clearDebounce
();
setLocalItems
([]);
setFilterModel
({
items
:
[]
});
},
[
clearDebounce
,
setFilterModel
]);
const
updateFilter
=
React
.
useCallback
(
(
index
:
number
,
key
:
keyof
LocalFilterItem
,
value
:
any
)
=>
{
setLocalItems
((
prev
)
=>
{
const
next
=
Array
.
isArray
(
prev
)
?
[...
prev
]
:
[];
const
old
=
next
[
index
]
??
{
field
:
columns
[
0
]?.
field
??
''
,
operator
:
defaultOperatorForField
(
columns
[
0
]?.
field
),
value
:
''
,
join
:
index
===
0
?
undefined
:
'
AND
'
,
};
let
updated
=
{
...
old
,
[
key
]:
value
};
if
(
key
===
'
field
'
)
{
const
newField
=
value
as
string
;
const
defOp
=
defaultOperatorForField
(
newField
)
as
any
;
updated
=
{
...
updated
,
field
:
newField
,
operator
:
defOp
,
value
:
''
};
next
[
index
]
=
updated
;
return
next
;
}
if
(
key
===
'
operator
'
)
{
const
op
=
String
(
value
).
toLowerCase
();
if
(
op
===
'
is empty
'
||
op
===
'
is not empty
'
)
{
updated
=
{
...
updated
,
operator
:
value
,
value
:
''
};
next
[
index
]
=
updated
;
return
next
;
}
if
(
op
===
'
is any of
'
)
{
if
(
!
Array
.
isArray
(
updated
.
value
))
updated
.
value
=
[];
updated
=
{
...
updated
,
operator
:
value
};
next
[
index
]
=
updated
;
return
next
;
}
updated
=
{
...
updated
,
operator
:
value
,
value
:
String
(
updated
.
value
??
''
)
};
next
[
index
]
=
updated
;
return
next
;
}
next
[
index
]
=
updated
;
const
items
=
next
;
const
col
=
getCol
(
items
[
index
].
field
as
string
);
const
isSingleSelect
=
!!
col
&&
(
col
.
type
===
'
singleSelect
'
||
Array
.
isArray
((
col
as
any
).
valueOptions
));
if
(
isSingleSelect
)
{
applyImmediate
(
items
);
}
else
{
applyDebounced
(
items
);
}
return
items
;
});
},
[
applyDebounced
,
applyImmediate
,
columns
,
getCol
]
);
const
handleApply
=
React
.
useCallback
(()
=>
{
clearDebounce
();
// Persist localItems to parent; keep join property so hook can use it
setFilterModel
({
items
:
localItems
as
any
});
setAnchorEl
(
null
);
},
[
localItems
,
clearDebounce
,
setFilterModel
]);
return
(
<>
<
Badge
badgeContent=
{
activeCount
}
color=
"primary"
overlap=
"circular"
>
<
IconButton
size=
"small"
ref=
{
buttonRef
}
onMouseDown=
{
(
e
)
=>
e
.
stopPropagation
()
}
onClick=
{
handleClick
}
sx=
{
{
color
:
'
#123375
'
}
}
aria
-
label=
"filter"
>
<
FilterListIcon
/>
</
IconButton
>
</
Badge
>
<
Popover
open=
{
Boolean
(
anchorEl
)
}
anchorEl=
{
anchorEl
}
onClose=
{
handleClose
}
keepMounted
anchorOrigin=
{
{
vertical
:
'
bottom
'
,
horizontal
:
'
left
'
}
}
transformOrigin=
{
{
vertical
:
'
top
'
,
horizontal
:
'
right
'
}
}
PaperProps=
{
{
sx
:
{
p
:
2
,
minWidth
:
640
,
maxWidth
:
'
95vw
'
}
}
}
>
<
Stack
gap=
{
2
}
>
{
(
localItems
||
[]).
map
((
item
,
idx
)
=>
{
const
colDef
=
getCol
(
item
.
field
as
string
);
const
operatorOptions
=
getOperatorOptionsForField
(
item
.
field
);
const
op
=
String
(
item
.
operator
??
defaultOperatorForField
(
item
.
field
));
const
valueOptions
:
string
[]
=
(
colDef
&&
(
colDef
as
any
).
valueOptions
)
||
(
colDef
&&
colDef
.
type
===
'
singleSelect
'
?
((
colDef
as
any
).
valueOptions
??
[])
:
[]);
const
statusVals
=
statusOptions
.
map
((
s
)
=>
s
.
value
);
const
isNoValueOp
=
op
===
'
is empty
'
||
op
===
'
is not empty
'
;
const
isAnyOfOp
=
op
===
'
is any of
'
;
const
isDateField
=
dateFields
.
has
(
item
.
field
as
string
);
const
isNumericField
=
numericFields
.
has
(
item
.
field
as
string
);
const
isStatusField
=
item
.
field
===
'
fgStatus
'
;
return
(
<
Stack
key=
{
idx
}
direction=
"row"
spacing=
{
1
}
alignItems=
"center"
sx=
{
{
width
:
'
100%
'
}
}
>
{
/* Join select (show for idx > 0) */
}
{
idx
>
0
?
(
<
TextField
select
size=
"small"
label=
""
value=
{
item
.
join
??
'
AND
'
}
onChange=
{
(
e
)
=>
updateFilter
(
idx
,
'
join
'
,
e
.
target
.
value
as
'
AND
'
|
'
OR
'
)
}
sx=
{
{
width
:
72
}
}
InputLabelProps=
{
{
shrink
:
true
}
}
>
<
MenuItem
value=
"AND"
>
AND
</
MenuItem
>
<
MenuItem
value=
"OR"
>
OR
</
MenuItem
>
</
TextField
>
)
:
(
<
div
style=
{
{
width
:
72
}
}
/>
)
}
{
/* Column */
}
<
TextField
select
size=
"small"
label=
"Column"
value=
{
item
.
field
??
''
}
onChange=
{
(
e
)
=>
updateFilter
(
idx
,
'
field
'
,
e
.
target
.
value
)
}
InputLabelProps=
{
{
shrink
:
true
}
}
sx=
{
{
minWidth
:
220
}
}
>
{
columns
.
map
((
col
)
=>
(
<
MenuItem
key=
{
String
(
col
.
field
)
}
value=
{
col
.
field
}
>
{
col
.
headerName
??
String
(
col
.
field
)
}
</
MenuItem
>
))
}
</
TextField
>
{
/* Operator */
}
<
TextField
select
size=
"small"
label=
"Operator"
value=
{
op
}
onChange=
{
(
e
)
=>
updateFilter
(
idx
,
'
operator
'
,
e
.
target
.
value
)
}
InputLabelProps=
{
{
shrink
:
true
}
}
sx=
{
{
minWidth
:
200
}
}
>
{
operatorOptions
.
map
((
o
)
=>
(
<
MenuItem
key=
{
o
.
value
}
value=
{
o
.
value
}
>
{
o
.
label
}
</
MenuItem
>
))
}
</
TextField
>
{
/* Value */
}
{
isNoValueOp
?
(
<
TextField
size=
"small"
label=
"Value"
placeholder=
"No value required"
value=
""
disabled
InputLabelProps=
{
{
shrink
:
true
}
}
sx=
{
{
flex
:
1
,
minWidth
:
160
}
}
/>
)
:
isAnyOfOp
?
(
<
Autocomplete
multiple
freeSolo
size=
"small"
options=
{
isStatusField
?
statusVals
:
((
valueOptions
as
string
[]
|
undefined
)
??
[])
}
value=
{
Array
.
isArray
(
item
.
value
)
?
item
.
value
:
item
.
value
?
[
item
.
value
]
:
[]
}
onChange=
{
(
_
,
values
)
=>
{
updateFilter
(
idx
,
'
value
'
,
values
);
}
}
renderTags=
{
(
value
:
any
[],
getTagProps
)
=>
value
.
map
((
option
:
any
,
index
:
number
)
=>
(
<
Chip
variant=
"outlined"
label=
{
String
(
option
)
}
{
...
getTagProps
({
index
})}
/>
))
}
renderInput=
{
(
params
)
=>
(
<
TextField
{
...
params
}
label=
"Value(s)"
placeholder=
"Select values..."
InputLabelProps=
{
{
shrink
:
true
}
}
sx=
{
{
flex
:
1
,
minWidth
:
160
}
}
/>
)
}
sx=
{
{
flex
:
1
,
minWidth
:
160
}
}
/>
)
:
isDateField
?
(
<
TextField
size=
"small"
label=
"Value"
type=
"date"
value=
{
item
.
value
?
String
(
item
.
value
).
slice
(
0
,
10
)
:
''
}
onChange=
{
(
e
)
=>
updateFilter
(
idx
,
'
value
'
,
e
.
target
.
value
)
}
InputLabelProps=
{
{
shrink
:
true
}
}
sx=
{
{
flex
:
1
,
minWidth
:
160
}
}
/>
)
:
colDef
&&
(
colDef
.
type
===
'
singleSelect
'
||
Array
.
isArray
((
colDef
as
any
).
valueOptions
))
?
(
<
TextField
select
size=
"small"
label=
"Value"
value=
{
item
.
value
??
''
}
onChange=
{
(
e
)
=>
updateFilter
(
idx
,
'
value
'
,
e
.
target
.
value
)
}
InputLabelProps=
{
{
shrink
:
true
}
}
sx=
{
{
flex
:
1
,
minWidth
:
160
}
}
>
{
((
colDef
as
any
).
valueOptions
??
(
isStatusField
?
statusVals
:
[])).
map
(
(
opt
:
any
)
=>
(
<
MenuItem
key=
{
String
(
opt
)
}
value=
{
opt
}
>
{
isStatusField
?
(
statusOptions
.
find
((
s
)
=>
s
.
value
===
opt
)?.
label
??
String
(
opt
))
:
String
(
opt
)
}
</
MenuItem
>
)
)
}
</
TextField
>
)
:
(
<
TextField
size=
"small"
label=
"Value"
placeholder=
"Filter Value"
value=
{
item
.
value
??
''
}
onChange=
{
(
e
)
=>
{
const
v
=
e
.
target
.
value
;
if
(
isNumericField
)
{
const
digits
=
v
.
replace
(
/
[^
0-9
]
/g
,
''
);
updateFilter
(
idx
,
'
value
'
,
digits
);
}
else
{
updateFilter
(
idx
,
'
value
'
,
v
);
}
}
}
InputLabelProps=
{
{
shrink
:
true
}
}
inputProps=
{
isNumericField
?
{
inputMode
:
'
numeric
'
,
pattern
:
'
[0-9]*
'
}
:
{}
}
sx=
{
{
flex
:
1
,
minWidth
:
160
}
}
/>
)
}
<
MuiIconButton
size=
"small"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
removeFilter
(
idx
);
}
}
>
<
CloseIcon
fontSize=
"small"
/>
</
MuiIconButton
>
</
Stack
>
);
})
}
<
Divider
/>
<
Stack
direction=
"row"
justifyContent=
"space-between"
alignItems=
"center"
>
<
Button
size=
"small"
variant=
"outlined"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
addFilter
();
}
}
>
+ Add filter
</
Button
>
<
Stack
direction=
"row"
spacing=
{
1
}
>
<
Button
size=
"small"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
handleClearAll
();
}
}
>
Clear
</
Button
>
<
Button
size=
"small"
variant=
"contained"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
handleApply
();
}
}
>
Apply
</
Button
>
</
Stack
>
</
Stack
>
</
Stack
>
</
Popover
>
</>
);
}
);
export
default
CustomFilterButton
;
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
// /* eslint-disable no-lonely-if */
// import {
// escapeForPostgres,
// filterModelByOperator,
// transformModelOperatorToSqlOperator,
// } from '@pjap/shared/data-grid-premium/util';
// import { isArray } from 'lodash';
// /**
// * Configuration object for customizing how individual fields are processed during search
// *
// * @typedef {Object} FieldConfig
// * @property {string} [type] - Data type of the field ('string', 'date', 'number', etc.)
// * Used for applying appropriate transformations and SQL operations
// *
// * @property {function} [transformValue] - Custom function to transform field values before SQL generation
// * Signature: (value, modelOperator) => transformedValue
// * Example: Convert date formats, escape special characters
// *
// * @property {function} [transformField] - Custom function to transform field names
// * Signature: (fieldName, value) => transformedFieldName
// * Example: Map UI field names to database column names
// *
// * @property {string|string[]} [fieldMapping] - Direct mapping to different database field name(s)
// * String: Maps to single field
// * Array: Maps to multiple fields (OR condition)
// *
// * @property {function|string} [operator] - Custom operator transformation
// * Function: (sqlOperator, originalOperator, value) => newOperator
// * String: Fixed operator to use (e.g., 'LIKE', 'IN')
// *
// * @property {function} [additionalQuery] - Generates additional SQL conditions for complex searches
// * Signature: (value) => additionalSqlCondition
// * Example: Add related table joins or complex WHERE clauses
// *
// * @property {string} [additionalQueryLogicOperator='AND'] - Logic operator ('AND' or 'OR') to combine
// * main query with additional query
// */
// /**
// * Configuration options for the useAdvancedSearch hook
// *
// * @typedef {Object} UseAdvancedSearchOptions
// * @property {Object.<string, FieldConfig>} [fieldConfigs] - Field-specific configurations
// * Key: field name, Value: FieldConfig object
// *
// * @property {function} [globalValueTransform] - Global transformation applied to all field values
// * Signature: (fieldName, value) => transformedValue
// * Applied when no field-specific transform exists
// *
// * @property {function} [globalFieldTransform] - Global transformation applied to all field names
// * Signature: (fieldName) => transformedFieldName
// * Applied when no field-specific transform exists
// *
// * @property {Object.<string, string>} [defaultFieldTypes] - Default data types for fields
// * Key: field name, Value: type string
// * Used when field type not specified in fieldConfigs
// */
// /**
// * Advanced Search Hook
// *
// * Provides functionality to convert DataGrid Premium filter models into SQL WHERE clauses
// * with extensive customization options for field transformations, value processing, and
// * complex query generation.
// *
// * MAIN USE CASES:
// * 1. Convert frontend filter UI state to backend SQL queries
// * 2. Handle complex field mappings (UI field → database column)
// * 3. Apply custom value transformations (dates, case sensitivity, escaping)
// * 4. Generate complex queries with joins and additional conditions
// *
// * @param {UseAdvancedSearchOptions} options - Configuration options for customizing search behavior
// * @returns {Object} Object containing the main query generation function
// */
// const useAdvancedSearch = (options = {}) => {
// // Extract configuration options with defaults
// const {
// fieldConfigs = {}, // Field-specific configurations
// globalValueTransform, // Global value transformation function
// globalFieldTransform, // Global field name transformation function
// defaultFieldTypes = {}, // Default field type mappings
// } = options;
// /**
// * Transform Field Values
// *
// * Applies transformations to field values based on priority:
// * 1. Field-specific transformValue function (highest priority)
// * 2. Global value transformation function
// * 3. Built-in type-based transformations (lowest priority)
// *
// * BUILT-IN TRANSFORMATIONS:
// * - 'date': Removes hyphens and underscores (2023-01-01 → 20230101)
// * - 'string': Converts to lowercase and escapes for PostgreSQL
// * - default: Returns value unchanged
// *
// * @param {string} field - The field name being processed
// * @param {any} value - The original field value from the filter
// * @param {string} modelOperator - The operator from the DataGrid model
// * @returns {any} The transformed value ready for SQL generation
// */
// const transformValue = (field, value = '', modelOperator) => {
// // Skip transformation for null/undefined values
// // if (!value) return value;
// // Priority 1: Field-specific transformation
// if (fieldConfigs[field]?.transformValue) {
// return fieldConfigs[field].transformValue(value, modelOperator);
// }
// // Priority 2: Global transformation
// if (globalValueTransform) {
// return globalValueTransform(field, value);
// }
// // Priority 3: Built-in type-based transformations
// const fieldType = fieldConfigs[field]?.type || defaultFieldTypes[field] || 'string';
// switch (fieldType) {
// case 'date':
// // Remove date separators for database storage format
// return value.replace(/-/g, '').replace(/_/g, '');
// case 'string':
// // Convert to lowercase and escape special PostgreSQL characters
// return escapeForPostgres(typeof value === 'string' ? value.toLowerCase() : value);
// default:
// // No transformation for other types (numbers, booleans, etc.)
// return value;
// }
// };
// /**
// * Transform Field Names
// *
// * Maps UI field names to database column names with priority:
// * 1. Direct fieldMapping configuration (highest priority)
// * 2. Field-specific transformField function
// * 3. Global field transformation function
// * 4. Original field name (lowest priority)
// *
// * FIELD MAPPING EXAMPLES:
// * - String mapping: "userName" → "user_name"
// * - Array mapping: "fullName" → ["first_name", "last_name"] (creates OR condition)
// *
// * @param {string} field - The original field name from the UI
// * @param {any} value - The field value (may influence transformation)
// * @returns {string|string[]} The transformed field name(s) for database queries
// */
// const transformField = (field, value) => {
// // Priority 1: Direct field mapping
// if (fieldConfigs[field]?.fieldMapping) {
// return fieldConfigs[field].fieldMapping;
// }
// // Priority 2: Field-specific transformation function
// if (fieldConfigs[field]?.transformField) {
// return fieldConfigs[field].transformField(field, value);
// }
// // Priority 3: Global field transformation
// if (globalFieldTransform) {
// return globalFieldTransform(field);
// }
// // Priority 4: Use original field name
// return field;
// };
// /**
// * Transform SQL Operators
// *
// * Converts DataGrid operators to appropriate SQL operators with custom logic support.
// * Allows field-specific operator overrides for special cases.
// *
// * OPERATOR EXAMPLES:
// * - DataGrid "contains" → SQL "LIKE"
// * - DataGrid "equals" → SQL "="
// * - Custom: Force "ILIKE" for case-insensitive searches
// *
// * @param {string} field - The field name being processed
// * @param {string} operator - The original operator from DataGrid
// * @param {any} value - The field value (may influence operator choice)
// * @returns {string} The SQL operator to use in the query
// */
// const transformOperator = (field, operator, value) => {
// // Check for field-specific operator configuration
// if (fieldConfigs[field]?.operator) {
// // If operator config is a function, call it with context
// if (typeof fieldConfigs[field].operator === 'function') {
// console.log('ini kesini lagi yah');
// return fieldConfigs[field].operator(
// transformModelOperatorToSqlOperator(operator), // Converted SQL operator
// operator, // Original DataGrid operator
// value // Field value for context
// );
// }
// console.log('ini keskip berarti');
// // If operator config is a string, use it directly
// return fieldConfigs[field].operator;
// }
// // Use default operator transformation
// return operator;
// };
// const buildQuery = (item, transformedField, fieldValue) => {
// // Transform the field value with all configured transformations
// const value = transformValue(item.columnField, fieldValue, item.operatorValue);
// // Transform the operator for this field
// const operator = transformOperator(item.columnField, item.operatorValue, fieldValue);
// console.log('ini operator datagrid', operator);
// // Get the field type for proper SQL generation
// const fieldType =
// fieldConfigs[item.columnField]?.type || defaultFieldTypes[item.columnField] || 'string';
// let baseQuery;
// // Handle multiple field mappings (when field is an array)
// if (Array.isArray(transformedField)) {
// // Check if we have multiple values for multiple fields
// if (typeof value === 'object') {
// // Map each field to its corresponding value (parallel arrays)
// // Example: fields=["first_name", "last_name"], values={0: "John", 1: "Doe"}
// // Result: first_name = 'John' AND last_name = 'Doe'
// baseQuery = transformedField
// .map((f, index) => {
// const newValue = Object.values(value)[index];
// return filterModelByOperator(f, operator, newValue, fieldType);
// })
// .join(' AND ');
// } else {
// // Same value applied to all fields (OR condition)
// // Example: fields=["first_name", "last_name"], value="John"
// // Result: first_name LIKE '%John%' OR last_name LIKE '%John%'
// baseQuery = transformedField
// .map((f) => filterModelByOperator(f, operator, value, fieldType))
// .join(' OR ');
// }
// } else {
// // Single field mapping
// // Handle custom operators that need special SQL construction
// if (fieldConfigs[item.columnField]?.operator) {
// baseQuery = `LOWER("${transformedField}") ${operator} ${value}`;
// } else {
// baseQuery = filterModelByOperator(transformedField, operator, value, fieldType);
// }
// }
// // Handle additional query conditions (for complex search scenarios)
// if (fieldConfigs[item.columnField]?.additionalQuery) {
// // Generate additional SQL condition
// const additionalQuery = fieldConfigs[item.columnField].additionalQuery(fieldValue);
// // Get the logic operator for combining base and additional queries
// const logicOperator = fieldConfigs[item.columnField]?.additionalQueryLogicOperator || 'AND';
// // Combine base and additional queries if additional query exists
// if (additionalQuery) {
// return `(${[baseQuery, additionalQuery].filter(Boolean).join(` ${logicOperator} `)})`;
// }
// }
// return baseQuery;
// };
// /**
// * Generate SQL Query from DataGrid Filter Model
// *
// * Main function that converts a DataGrid Premium filter model into a SQL WHERE clause.
// * Handles complex scenarios including:
// * - Multiple field mappings (OR conditions)
// * - Custom operators and value transformations
// * - Additional query conditions with configurable logic operators
// * - Proper SQL escaping and type handling
// *
// * FILTER MODEL STRUCTURE:
// * {
// * items: [
// * {
// * columnField: "fieldName",
// * operatorValue: "contains",
// * value: "searchTerm"
// * }
// * ],
// * linkOperator: "AND" // or "OR"
// * }
// *
// * @param {Object} model - The filter model from DataGrid Premium
// * @param {Array} model.items - Array of individual filter conditions
// * @param {string} model.linkOperator - Logic operator to combine multiple conditions ("AND" or "OR")
// * @returns {string} Generated SQL WHERE clause (without the "WHERE" keyword)
// */
// const generateSqlQueryByDatagridPremiumModel = (model) => {
// // Process each filter item in the model
// const advanced = model.items
// ?.map((item) => {
// // Transform the field name (UI → database column)
// const field = transformField(item.columnField, item.value);
// if (item.operatorValue === 'isAnyOf' && isArray(item.value)) {
// if (item.value.length > 0) {
// const query = item.value.map((v) => `${buildQuery(item, field, v)}`).join(' OR ');
// return `(${query})`;
// }
// return '';
// }
// return buildQuery(item, field, item.value);
// })
// // Remove any empty/null queries
// .filter(Boolean)
// // Join all conditions with the model's link operator (AND/OR)
// .join(` ${model.linkOperator} `);
// return advanced;
// };
// // Return the main query generation function
// return {
// generateSqlQueryByDatagridPremiumModel,
// };
// };
// export default useAdvancedSearch;
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
import
{
ArticleTwoTone
,
AutorenewTwoTone
,
DeleteSweepTwoTone
,
EditNoteTwoTone
,
FileOpenTwoTone
,
HighlightOffTwoTone
,
UploadFileTwoTone
,
}
from
'
@mui/icons-material
'
;
import
Button
from
'
@mui/material/Button
'
;
import
type
{
GridColDef
,
GridFilterModel
,
GridPaginationModel
,
GridRowSelectionModel
,
GridSortModel
,
GridToolbarProps
,
}
from
'
@mui/x-data-grid-premium
'
;
import
{
DataGridPremium
,
useGridApiRef
}
from
'
@mui/x-data-grid-premium
'
;
import
dayjs
from
'
dayjs
'
;
import
{
isEmpty
}
from
'
lodash
'
;
import
{
enqueueSnackbar
}
from
'
notistack
'
;
import
{
startTransition
,
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'
react
'
;
import
{
unstable_batchedUpdates
}
from
'
react-dom
'
;
import
{
useNavigate
}
from
'
react-router
'
;
import
{
CustomBreadcrumbs
}
from
'
src/components/custom-breadcrumbs
'
;
import
{
DashboardContent
}
from
'
src/layouts/dashboard
'
;
import
{
RouterLink
}
from
'
src/routes/components
'
;
import
{
paths
}
from
'
src/routes/paths
'
;
import
type
{
GridColDef
,
GridFilterModel
,
GridSortModel
}
from
'
@mui/x-data-grid-premium
'
;
import
{
DataGridPremium
}
from
'
@mui/x-data-grid-premium
'
;
import
{
useMemo
,
useState
}
from
'
react
'
;
import
StatusChip
from
'
src/sections/bupot-unifikasi/bupot-dn/components/StatusChip
'
;
import
TableHeaderLabel
from
'
src/shared/components/TableHeaderLabel
'
;
import
{
formatRupiah
}
from
'
src/shared/FormatRupiah/FormatRupiah
'
;
import
{
useDebounce
,
useThrottle
}
from
'
src/shared/hooks/useDebounceThrottle
'
;
import
{
createTableKey
,
useTablePagination
}
from
'
../../paginationStore
'
;
import
{
CustomToolbar
}
from
'
../components/CustomToolbar
'
;
import
{
FG_STATUS_BUPOT
}
from
'
../constant
'
;
import
{
appRootKey
,
bulanan
}
from
'
../constant/queryKey
'
;
import
{
useAdvancedFilter
}
from
'
../hooks/useAdvancedFilter
'
;
import
useGetBulanan
from
'
../hooks/useGetBulanan
'
;
import
{
isEmpty
}
from
'
lodash
'
;
// import CustomToolbarDn from '../components/customToolbarDn';
// import CustomToolbar, { CustomFilterButton } from '../components/customToolbarDn2';
const
numberIDR
=
(
number
:
string
)
=>
new
Intl
.
NumberFormat
(
'
id-ID
'
,
{
currency
:
'
IDR
'
,
// style: 'currency'
}).
format
(
Number
(
number
));
export
type
IColumnGrid
=
GridColDef
&
{
field
:
...
...
@@ -48,95 +67,21 @@ export type IColumnGrid = GridColDef & {
valueOptions
?:
string
[];
};
export
function
BulananListView
()
{
// const [tabs1, setTabs1] = useState<number>(1);
// const [tabs2, setTabs2] = useState<number>(0);
const
[
paginationModel
,
setPaginationModel
]
=
useState
({
page
:
0
,
// 0-based index
pageSize
:
10
,
});
const
[
filterModel
,
setFilterModel
]
=
useState
<
GridFilterModel
>
({
items
:
[]
});
const
[
sortModel
,
setSortModel
]
=
useState
<
GridSortModel
>
([]);
// const [rowSelectionModel, setRowSelectionModel] = useState<any>([]);
// const [rowSelectionModel, setRowSelectionModel] =
// useState<GridRowSelectionModel>(new Set<GridRowId>());
// const navigate = useNavigate();
// Enhance tabs dengan navigate
// const enhancedTabsTop = TABS_TOP_UNIFIKASI.map((tab) =>
// tab.value ? { ...tab, onClick: () => navigate(tab.value) } : tab
// );
// const enhancedTabsChoice = TABS_CHOICE.map((tab) => ({
// ...tab,
// onClick: () => navigate(tab.value),
// }));
const
buildAdvancedFilter
=
(
filters
?:
GridFilterModel
[
'
items
'
])
=>
{
if
(
!
filters
||
filters
.
length
===
0
)
return
''
;
return
filters
.
map
((
f
)
=>
{
if
(
!
f
.
value
||
!
f
.
field
)
return
null
;
const
field
=
`LOWER("
${
f
.
field
}
")`
;
const
val
=
String
(
f
.
value
).
toLowerCase
();
switch
(
f
.
operator
)
{
case
'
contains
'
:
return
`
${
field
}
LIKE '%
${
val
}
%'`
;
case
'
equals
'
:
case
'
is
'
:
return
`
${
field
}
= '
${
val
}
'`
;
case
'
isNot
'
:
return
`
${
field
}
<> '
${
val
}
'`
;
default
:
return
null
;
}
})
.
filter
(
Boolean
)
.
join
(
'
AND
'
);
};
const
{
data
,
isLoading
}
=
useGetBulanan
({
params
:
{
Page
:
paginationModel
.
page
+
1
,
// API biasanya 1-based
Limit
:
paginationModel
.
pageSize
,
advanced
:
buildAdvancedFilter
(
filterModel
?.
items
),
sortingMode
:
sortModel
[
0
]?.
field
,
sortingMethod
:
sortModel
[
0
]?.
sort
,
},
refetchOnWindowFocus
:
false
,
});
const
totalRows
=
data
?.
total
||
0
;
const
rows
=
useMemo
(()
=>
data
?.
data
||
[],
[
data
?.
data
]);
// const handleChange = (event: React.SyntheticEvent, newValue: number) => {
// setTabs1(newValue);
// };
// const handleChange2 = (event: React.SyntheticEvent, newValue: number) => {
// setTabs2(newValue);
// };
type
Status
=
'
draft
'
|
'
normal
'
|
'
cancelled
'
|
'
amendment
'
;
// type aman
type
Status
=
'
draft
'
|
'
normal
'
|
'
cancelled
'
|
'
amendment
'
;
type
StatusOption
=
{
type
StatusOption
=
{
value
:
Status
;
label
:
string
;
};
};
const
statusOptions
:
StatusOption
[]
=
[
const
statusOptions
:
StatusOption
[]
=
[
{
value
:
'
draft
'
,
label
:
'
Draft
'
},
{
value
:
'
normal
'
,
label
:
'
Normal
'
},
{
value
:
'
cancelled
'
,
label
:
'
Dibatalkan
'
},
{
value
:
'
amendment
'
,
label
:
'
Normal Pengganti
'
},
];
];
const
columns
:
IColumnGrid
[]
=
[
const
columns
:
IColumnGrid
[]
=
[
{
field
:
'
fgStatus
'
,
headerName
:
'
Status
'
,
...
...
@@ -149,6 +94,7 @@ export function BulananListView() {
const
option
=
statusOptions
.
find
((
opt
)
=>
opt
.
value
===
params
.
value
);
return
option
?
option
.
label
:
(
params
.
value
as
string
);
},
renderCell
:
({
value
,
row
})
=>
<
StatusChip
value=
{
value
}
revNo=
{
row
.
revNo
}
/>,
},
{
field
:
'
noBupot
'
,
...
...
@@ -230,7 +176,7 @@ export function BulananListView() {
if
(
params
==
null
)
{
return
'
0
'
;
}
return
numberIDR
(
params
);
return
formatRupiah
(
params
);
},
},
{
...
...
@@ -244,7 +190,7 @@ export function BulananListView() {
if
(
params
==
null
)
{
return
'
0
'
;
}
return
numberIDR
(
params
);
return
formatRupiah
(
params
);
},
},
{
...
...
@@ -257,7 +203,7 @@ export function BulananListView() {
if
(
params
==
null
)
{
return
'
0
'
;
}
return
numberIDR
(
params
);
return
formatRupiah
(
params
);
},
},
{
...
...
@@ -273,40 +219,322 @@ export function BulananListView() {
headerAlign
:
'
center
'
,
minWidth
:
150
,
},
{
field
:
'
created_at
'
,
headerName
:
'
Created At
'
,
width
:
150
},
{
field
:
'
updated
'
,
headerName
:
'
Updated
'
,
width
:
150
},
{
field
:
'
updated_at
'
,
headerName
:
'
Update At
'
,
width
:
150
},
{
field
:
'
created_at
'
,
headerName
:
'
Created At
'
,
minWidth
:
150
,
valueFormatter
:
(
params
)
=>
dayjs
(
params
).
format
(
'
DD/MM/YYYY h:mm
'
),
},
{
field
:
'
updated
'
,
headerName
:
'
Updated
'
,
minWidth
:
150
,
},
{
field
:
'
updated_at
'
,
headerName
:
'
Update At
'
,
minWidth
:
150
,
valueFormatter
:
(
params
)
=>
dayjs
(
params
).
format
(
'
DD/MM/YYYY h:mm
'
),
},
{
field
:
'
keterangan1
'
,
headerName
:
'
Keterangan 1
'
,
minWidth
:
200
,
flex
:
1
,
minWidth
:
150
,
},
{
field
:
'
keterangan2
'
,
headerName
:
'
Keterangan 2
'
,
minWidth
:
200
,
flex
:
1
,
minWidth
:
150
,
},
{
field
:
'
keterangan3
'
,
headerName
:
'
Keterangan 3
'
,
minWidth
:
200
,
flex
:
1
,
minWidth
:
150
,
},
{
field
:
'
keterangan4
'
,
headerName
:
'
Keterangan 4
'
,
minWidth
:
200
,
flex
:
1
,
minWidth
:
150
,
},
{
field
:
'
keterangan5
'
,
headerName
:
'
Keterangan 5
'
,
minWidth
:
200
,
flex
:
1
,
minWidth
:
150
,
},
];
export
function
BulananListView
()
{
const
apiRef
=
useGridApiRef
();
const
dataSelectedRef
=
useRef
<
any
[]
>
([]);
const
navigate
=
useNavigate
();
const
TABLE_KEY
=
useMemo
(()
=>
createTableKey
(
appRootKey
,
bulanan
),
[]);
const
[
paginationState
,
setPaginationState
,
resetPaginationState
]
=
useTablePagination
(
TABLE_KEY
);
const
{
page
,
pageSize
}
=
paginationState
;
// State management
const
[
filterModel
,
setFilterModel
]
=
useState
<
GridFilterModel
>
({
items
:
[]
});
const
[
sortModel
,
setSortModel
]
=
useState
<
GridSortModel
>
([]);
const
[
rowSelectionModel
,
setRowSelectionModel
]
=
useState
<
GridRowSelectionModel
|
undefined
>
(
undefined
);
const
[
selectionVersion
,
setSelectionVersion
]
=
useState
(
0
);
const
[
modals
,
setModals
]
=
useState
({
delete
:
false
,
upload
:
false
,
cancel
:
false
,
preview
:
false
,
});
const
[
previewPayload
,
setPreviewPayload
]
=
useState
<
Record
<
string
,
any
>
|
undefined
>
(
undefined
);
const
{
buildAdvancedFilter
,
buildRequestParams
}
=
useAdvancedFilter
();
const
params
=
useMemo
(()
=>
{
const
advanced
=
buildAdvancedFilter
(
filterModel
.
items
);
const
baseParams
=
{
page
:
page
+
1
,
limit
:
pageSize
,
sortingMode
:
sortModel
[
0
]?.
field
,
sortingMethod
:
sortModel
[
0
]?.
sort
,
};
return
buildRequestParams
(
baseParams
,
advanced
);
},
[
page
,
pageSize
,
sortModel
,
filterModel
.
items
,
buildAdvancedFilter
,
buildRequestParams
]);
// Data fetching
const
{
data
,
isLoading
,
refetch
}
=
useGetBulanan
({
params
,
refetchOnWindowFocus
:
false
,
});
const
rows
=
useMemo
(
()
=>
(
Array
.
isArray
(
data
?.
data
)
?
data
.
data
.
filter
(
Boolean
)
:
[]),
[
data
?.
data
]
);
const
totalRows
=
Number
(
data
?.
total
??
0
);
const
idStatusMapRef
=
useRef
<
Map
<
string
|
number
,
string
>>
(
new
Map
());
useEffect
(()
=>
{
if
(
!
rows
.
length
)
{
idStatusMapRef
.
current
.
clear
();
return
;
}
const
newMap
=
new
Map
<
string
|
number
,
string
>
();
rows
.
forEach
((
r
:
any
)
=>
{
const
id
=
String
(
r
.
id
??
r
.
internal_id
??
''
);
if
(
id
)
newMap
.
set
(
id
,
r
?.
fgStatus
??
''
);
});
idStatusMapRef
.
current
=
newMap
;
},
[
rows
]);
const
handlePaginationChange
=
useCallback
(
(
model
:
GridPaginationModel
)
=>
{
if
(
model
.
pageSize
!==
pageSize
)
{
// Reset to first page when page size changes
setPaginationState
({
page
:
0
,
pageSize
:
model
.
pageSize
});
}
else
{
setPaginationState
({
page
:
model
.
page
});
}
},
];
[
pageSize
,
setPaginationState
]
);
const
handleFilterChange
=
useCallback
(
(
model
:
GridFilterModel
)
=>
{
setFilterModel
(
model
);
resetPaginationState
();
// Reset ke page 0 saat filter berubah
},
[
resetPaginationState
]
);
const
handleSortChange
=
useCallback
(
(
model
:
GridSortModel
)
=>
{
setSortModel
(
model
);
resetPaginationState
();
// Reset ke page 0 saat sort berubah
},
[
resetPaginationState
]
);
const
throttledPaginationChange
=
useThrottle
(
handlePaginationChange
,
250
);
const
debouncedFilterChange
=
useDebounce
(
handleFilterChange
,
400
);
const
debouncedSortChange
=
useDebounce
(
handleSortChange
,
400
);
const
toggleModal
=
useCallback
((
modalName
:
keyof
typeof
modals
,
value
:
boolean
)
=>
{
setModals
((
prev
)
=>
({
...
prev
,
[
modalName
]:
value
}));
},
[]);
const
handleEditData
=
useCallback
(
(
type
=
'
ubah
'
)
=>
{
const
selectedRow
=
dataSelectedRef
.
current
[
0
];
if
(
!
selectedRow
)
{
enqueueSnackbar
(
'
Pilih data yang akan diubah
'
,
{
variant
:
'
warning
'
});
return
;
}
navigate
(
`/pph21/bulanan/
${
selectedRow
.
id
}
/
${
type
}
`
);
},
[
navigate
]
);
const
handleOpenPreview
=
useCallback
(()
=>
{
const
selectedRow
=
dataSelectedRef
.
current
?.[
0
];
if
(
!
selectedRow
)
{
enqueueSnackbar
(
'
Pilih 1 baris untuk melihat detail
'
,
{
variant
:
'
warning
'
});
return
;
}
setPreviewPayload
(
selectedRow
);
toggleModal
(
'
preview
'
,
true
);
},
[
toggleModal
]);
const
throttledSelectionChange
=
useThrottle
((
newSelection
:
any
)
=>
{
if
(
!
apiRef
.
current
)
return
;
const
ids
=
newSelection
?.
ids
instanceof
Set
?
Array
.
from
(
newSelection
.
ids
)
:
newSelection
||
[];
const
selectedData
=
ids
.
map
((
id
:
any
)
=>
apiRef
.
current
!
.
getRow
(
id
)).
filter
(
Boolean
);
// Batch updates
unstable_batchedUpdates
(()
=>
{
dataSelectedRef
.
current
=
selectedData
;
setRowSelectionModel
(
newSelection
);
setSelectionVersion
((
v
)
=>
v
+
1
);
});
},
150
);
useEffect
(()
=>
{
const
api
=
apiRef
.
current
;
if
(
!
api
)
return
;
const
handleRowsSet
=
()
=>
{
const
exec
=
()
=>
{
const
ids
=
api
.
state
?.
rowSelection
?.
ids
instanceof
Set
?
Array
.
from
(
api
.
state
.
rowSelection
.
ids
)
:
[];
const
updatedSelected
=
ids
.
map
((
id
)
=>
api
.
getRow
(
id
)).
filter
(
Boolean
);
dataSelectedRef
.
current
=
updatedSelected
;
setSelectionVersion
((
v
)
=>
v
+
1
);
};
if
((
window
as
any
).
requestIdleCallback
)
{
(
window
as
any
).
requestIdleCallback
(
exec
);
}
else
{
setTimeout
(
exec
,
0
);
}
};
const
unsubscribe
=
api
.
subscribeEvent
(
'
rowsSet
'
,
handleRowsSet
);
// eslint-disable-next-line consistent-return
return
()
=>
unsubscribe
();
},
[
apiRef
]);
const
validatedActions
=
useMemo
(()
=>
{
const
dataSelected
=
dataSelectedRef
.
current
;
const
count
=
dataSelected
.
length
;
const
hasSelection
=
count
>
0
;
if
(
!
hasSelection
)
{
return
{
canDetail
:
false
,
canEdit
:
false
,
canDelete
:
false
,
canUpload
:
false
,
canReplacement
:
false
,
canCancel
:
false
,
};
}
const
allDraft
=
dataSelected
.
every
((
d
)
=>
d
.
fgStatus
===
FG_STATUS_BUPOT
.
DRAFT
);
const
allNormal
=
dataSelected
.
every
((
d
)
=>
d
.
fgStatus
===
FG_STATUS_BUPOT
.
NORMAL_DONE
);
const
firstItem
=
dataSelected
[
0
];
return
{
canDetail
:
count
===
1
,
canEdit
:
count
===
1
&&
allDraft
,
canDelete
:
hasSelection
&&
allDraft
,
canUpload
:
hasSelection
&&
allDraft
,
canReplacement
:
count
===
1
&&
firstItem
?.
fgStatus
===
FG_STATUS_BUPOT
.
NORMAL_DONE
,
canCancel
:
hasSelection
&&
allNormal
,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
dataSelectedRef
.
current
]);
const
actions
=
useMemo
(
()
=>
[
[
{
title
:
'
Refresh List
'
,
icon
:
<
AutorenewTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
()
=>
startTransition
(()
=>
void
refetch
()),
},
{
title
:
'
Edit
'
,
icon
:
<
EditNoteTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
()
=>
handleEditData
(
'
ubah
'
),
disabled
:
!
validatedActions
.
canEdit
,
},
{
title
:
'
Detail
'
,
icon
:
<
ArticleTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
handleOpenPreview
,
disabled
:
!
validatedActions
.
canDetail
,
},
{
title
:
'
Hapus
'
,
icon
:
<
DeleteSweepTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
()
=>
toggleModal
(
'
delete
'
,
true
),
disabled
:
!
validatedActions
.
canDelete
,
},
],
[
{
title
:
'
Upload
'
,
icon
:
<
UploadFileTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
()
=>
toggleModal
(
'
upload
'
,
true
),
disabled
:
!
validatedActions
.
canUpload
,
},
{
title
:
'
Pengganti
'
,
icon
:
<
FileOpenTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
()
=>
handleEditData
(
'
pengganti
'
),
disabled
:
!
validatedActions
.
canReplacement
,
},
{
title
:
'
Batal
'
,
icon
:
<
HighlightOffTwoTone
sx=
{
{
width
:
26
,
height
:
26
}
}
/>,
func
:
()
=>
toggleModal
(
'
cancel
'
,
true
),
disabled
:
!
validatedActions
.
canCancel
,
},
],
],
[
validatedActions
,
refetch
,
handleEditData
,
handleOpenPreview
,
toggleModal
]
);
const
pinnedColumns
=
useMemo
(()
=>
({
left
:
[
'
__check__
'
,
'
fgStatus
'
,
'
noBupot
'
]
}),
[]);
const
ToolbarWrapper
:
React
.
FC
<
GridToolbarProps
>
=
useCallback
(
(
gridToolbarProps
)
=>
(
<
CustomToolbar
actions=
{
actions
}
columns=
{
columns
}
filterModel=
{
filterModel
}
setFilterModel=
{
setFilterModel
}
statusOptions=
{
statusOptions
}
{
...
gridToolbarProps
}
/>
),
[
actions
,
filterModel
]
);
const
paginationModel
:
GridPaginationModel
=
useMemo
(
()
=>
({
page
,
pageSize
}),
[
page
,
pageSize
]
);
return
(
<
DashboardContent
>
...
...
@@ -326,91 +554,45 @@ export function BulananListView() {
<
TableHeaderLabel
label=
"Daftar Bupot Bulanan"
/>
<
DataGridPremium
sx=
{
{
border
:
1
,
borderColor
:
'
divider
'
,
borderRadius
:
2
,
'
& .MuiDataGrid-cell
'
:
{
borderColor
:
'
divider
'
,
},
'
& .MuiDataGrid-columnHeaders
'
:
{
borderColor
:
'
divider
'
,
},
}
}
apiRef=
{
apiRef
}
checkboxSelection
rows=
{
rows
}
columns=
{
columns
}
loading=
{
isLoading
}
rowCount=
{
totalRows
}
initialState=
{
{
pagination
:
{
paginationModel
:
{
pageSize
:
10
,
page
:
0
}
},
}
}
disableVirtualization
pagination
pageSizeOptions=
{
[
5
,
10
,
15
,
25
,
50
,
100
,
250
,
500
,
750
,
100
]
}
paginationMode=
"server"
onPaginationModelChange=
{
setPaginationModel
}
paginationModel=
{
paginationModel
}
onPaginationModelChange=
{
throttledPaginationChange
}
pageSizeOptions=
{
[
5
,
10
,
15
,
25
,
50
,
100
]
}
filterMode=
"server"
onFilterModelChange=
{
setFilterModel
}
onFilterModelChange=
{
debouncedFilterChange
}
sortingMode=
"server"
onSortModelChange=
{
setSortModel
}
pinnedColumns=
{
{
left
:
[
'
__check__
'
,
'
fgStatus
'
,
'
noBupot
'
],
}
}
onSortModelChange=
{
debouncedSortChange
}
rowSelectionModel=
{
rowSelectionModel
}
onRowSelectionModelChange=
{
throttledSelectionChange
}
pinnedColumns=
{
pinnedColumns
}
cellSelection
// slots={{
// toolbar: () => (
// <CustomToolbar
// actions={[
// [
// {
// title: 'Edit',
// icon: <EditNoteTwoTone sx={{ width: 26, height: 26 }} />,
// func: () => {},
// disabled: true,
// },
// {
// title: 'Detail',
// icon: <ArticleTwoTone sx={{ width: 26, height: 26 }} />,
// func: () => {},
// disabled: true,
// },
// {
// title: 'Hapus',
// icon: <DeleteSweepTwoTone sx={{ width: 26, height: 26 }} />,
// func: () => {},
// disabled: false,
// },
// ],
// [
// {
// title: 'Upload',
// icon: <UploadFileTwoTone sx={{ width: 26, height: 26 }} />,
// func: () => {},
// disabled: false,
// },
// {
// title: 'Pengganti',
// icon: <FileOpenTwoTone sx={{ width: 26, height: 26 }} />,
// func: () => {},
// disabled: false,
// },
// {
// title: 'Batal',
// icon: <HighlightOffTwoTone sx={{ width: 26, height: 26 }} />,
// func: () => {},
// disabled: true,
// },
// ],
// ]}
// columns={columns}
// filterModel={filterModel}
// setFilterModel={setFilterModel}
// statusOptions={statusOptions.map((s) => ({ value: s.value, label: s.label }))}
// />
// ),
// }}
slots=
{
{
toolbar
:
ToolbarWrapper
}
}
sx=
{
{
border
:
1
,
borderColor
:
'
divider
'
,
borderRadius
:
2
,
mt
:
3
,
'
& .MuiDataGrid-cell
'
:
{
borderColor
:
'
divider
'
,
userSelect
:
'
text
'
,
cursor
:
'
text
'
,
},
'
& .MuiDataGrid-columnHeaders
'
:
{
borderColor
:
'
divider
'
},
}
}
/>
{
modals
.
delete
&&
<
div
>
Modatal delete
</
div
>
}
{
modals
.
upload
&&
<
div
>
Modatal upload
</
div
>
}
{
modals
.
cancel
&&
<
div
>
Modatal cancel
</
div
>
}
{
modals
.
preview
&&
previewPayload
&&
<
div
>
Modatal preview
</
div
>
}
</
DashboardContent
>
);
}
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