Commit eb0ac62b authored by Rais Aryaguna's avatar Rais Aryaguna

Merge remote-tracking branch 'origin/main' into develop

parents 6dfd3390 3b0dd65d
declare module 'demos-react-captcha' {
import * as React from 'react';
export interface CaptchaProps {
onRefresh?: () => void;
onChange?: (verified: boolean) => void;
placeholder?: string;
length?: number;
width?: number;
height?: number;
className?: string;
}
const Captcha: React.FC<CaptchaProps>;
export default Captcha;
}
......@@ -13,6 +13,7 @@ import { Snackbar } from 'src/components/snackbar';
import { ProgressBar } from 'src/components/progress-bar';
import { MotionLazy } from 'src/components/animate/motion-lazy';
import { SettingsDrawer, defaultSettings, SettingsProvider } from 'src/components/settings';
import { SnackbarProvider } from 'notistack';
import { Provider } from 'react-redux';
......@@ -59,12 +60,19 @@ export default function App({ children }: AppProps) {
modeStorageKey={themeConfig.modeStorageKey}
defaultMode={themeConfig.defaultMode}
>
<MotionLazy>
<Snackbar />
<ProgressBar />
<SettingsDrawer defaultSettings={defaultSettings} />
{children}
</MotionLazy>
<SnackbarProvider
maxSnack={3}
autoHideDuration={3000}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
preventDuplicate
>
<MotionLazy>
<Snackbar />
<ProgressBar />
<SettingsDrawer defaultSettings={defaultSettings} />
{children}
</MotionLazy>
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
</SettingsProvider>
......
......@@ -61,14 +61,12 @@ import { JWT_STORAGE_KEY } from './constant';
// }
// }
interface JwtPayload {
exp: number;
[key: string]: any;
}
export function isValidToken(accessToken: string): boolean {
console.log('Access Token:', accessToken);
if (!accessToken) {
console.log('Token is missing');
return false;
......@@ -76,8 +74,7 @@ export function isValidToken(accessToken: string): boolean {
try {
const decoded = jwtDecode<JwtPayload>(accessToken); // <-- tentukan tipe
console.log('Decoded token:', decoded);
if (!decoded || typeof decoded.exp !== 'number') {
console.log('Token has no exp field');
return false;
......@@ -112,33 +109,6 @@ export function tokenExpired(exp: number) {
}, timeLeft);
}
// ----------------------------------------------------------------------
// export async function setSession(accessToken: string | null) {
// try {
// if (accessToken) {
// sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
// axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
// const decodedToken = jwtDecode(accessToken); // ~3 days by minimals server
// if (decodedToken && 'exp' in decodedToken) {
// tokenExpired(decodedToken.exp);
// } else {
// throw new Error('Invalid access token!');
// }
// } else {
// sessionStorage.removeItem(JWT_STORAGE_KEY);
// delete axios.defaults.headers.common.Authorization;
// }
// } catch (error) {
// console.error('Error during set session:', error);
// throw error;
// }
// }
export const setSession = async (
access_token: string | null,
xToken?: string | null
......@@ -165,6 +135,3 @@ export const setSession = async (
return 'success';
};
import { createSlice } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { signInWithPassword } from '../context/jwt';
import axios from 'axios';
import { signInWithPassword } from '../context/jwt';
import axios from 'axios';
import { setUserData } from './userSlice';
import { ApiResponse } from '../types/api';
import { User } from '../types/user';
interface SignInParams {
email: string;
password: string;
......@@ -20,24 +19,24 @@ export const submitLogin =
// Panggil API login baru
await signInWithPassword({ email, password });
const token = localStorage.getItem('jwt_access_token');
const xToken = localStorage.getItem('x-token');
const token = localStorage.getItem('jwt_access_token');
const xToken = localStorage.getItem('x-token');
// Ambil data user dari response API
const res = await axios.post<ApiResponse<{ user: User }>>(
// Ambil data user dari response API
const res = await axios.post<ApiResponse<{ user: User }>>(
'https://nodesandbox.pajakexpress.id:1837/sandbox/userinfo',
null,
{
headers: {
Authorization: `Bearer ${token}`,
'x-token': xToken,
headers: {
Authorization: `Bearer ${token}`,
'x-token': xToken,
'Content-Type': 'application/x-www-form-urlencoded',
},
},
}
);
);
const userInfo = res.data?.data?.user;
console.log(userInfo)
console.log(userInfo);
if (!userInfo) throw new Error('User data not found');
......@@ -54,23 +53,23 @@ export const submitLogin =
quota: userInfo.quota,
start_date: userInfo.start_date,
end_date: userInfo.end_date,
updated_at:userInfo.updated_at,
updated_at: userInfo.updated_at,
x_token: userInfo.x_token,
signer_npwp:userInfo.signer_npwp,
signer:userInfo.signer,
created_at:userInfo.created_at,
address:userInfo.address
signer_npwp: userInfo.signer_npwp,
signer: userInfo.signer,
created_at: userInfo.created_at,
address: userInfo.address,
nitku_trial: userInfo.nitku_trial,
},
loginRedirectUrl: '/dashboard',
};
dispatch(setUserData(mappedUser));
console.log("User state updated:", mappedUser);
console.log('User state updated:', mappedUser);
return dispatch(loginSuccess(mappedUser));
} catch (error) {
console.error(error);
// return dispatch(loginError(error));
dispatch(loginError(error));
throw error;
}
......@@ -94,7 +93,7 @@ const loginSlice = createSlice({
state.errors = [];
},
},
// extraReducers: {},
// extraReducers: {},
});
export const { loginSuccess, loginError } = loginSlice.actions;
......
......@@ -3,21 +3,22 @@ import { createSlice } from '@reduxjs/toolkit';
const initialState = {
role: [] as string[],
data: {
id: "",
customer_name: "",
email: "",
npwp_trial: "",
company_name: "",
phone: "",
id: '',
customer_name: '',
email: '',
npwp_trial: '',
company_name: '',
phone: '',
quota: 0,
start_date: null as string | null,
end_date: null as string | null,
updated_at: null as string | null,
x_token: "",
signer_npwp: "",
signer: "",
x_token: '',
signer_npwp: '',
signer: '',
created_at: null as string | null,
address: ""
address: '',
nitku_trial: '',
},
loginRedirectUrl: null as string | null,
};
......@@ -27,12 +28,12 @@ const userSlice = createSlice({
initialState,
reducers: {
setUserData: (state, action) => ({
...state,
...action.payload,
data: {
...state,
...action.payload,
data: {
...state.data,
...action.payload.data,
},
},
}),
userLoggedOut: () => initialState,
},
......
export interface User{
id?: string;
email: string;
passwd?: string;
npwp_trial?: string;
api_key?: string;
api_secret?: string;
created_at?: string;
updated_at?: string;
company_name?: string;
address: string;
x_token?: string;
signer?: string;
signer_npwp?: string;
phone: string;
quota?: number;
start_date?: string;
end_date?: string;
access_level?: number;
customer_name: string;
contact_person?: string;
}
\ No newline at end of file
export interface User {
id?: string;
email: string;
passwd?: string;
npwp_trial?: string;
api_key?: string;
api_secret?: string;
created_at?: string;
updated_at?: string;
company_name?: string;
address: string;
x_token?: string;
signer?: string;
signer_npwp?: string;
phone: string;
quota?: number;
start_date?: string;
end_date?: string;
access_level?: number;
customer_name: string;
contact_person?: string;
nitku_trial?: string;
}
......@@ -30,8 +30,6 @@ import { FormHead } from '../../components/form-head';
import useLogin from './useLogin';
import { useNavigate } from 'react-router';
// ----------------------------------------------------------------------
export type SignInSchemaType = z.infer<typeof SignInSchema>;
......@@ -70,7 +68,7 @@ export function JwtSignInView() {
const loginAction = useLogin();
const methods = useForm({
const methods = useForm({
mode: 'onChange',
defaultValues: {
email: '',
......@@ -106,35 +104,28 @@ export function JwtSignInView() {
const timer = setInterval(() => {
setCountdown(updateCountdown);
}, 1000);
// eslint-disable-next-line consistent-return
return () => clearInterval(timer);
}, [isLocked]);
const onSubmit = handleSubmit(async (formData) => {
try {
if(isLocked){
setErrorMessage(`Kesempatan habis! Tunggu ${countdown} detik`)
if (isLocked) {
setErrorMessage(`Kesempatan habis! Tunggu ${countdown} detik`);
return;
}
if (isMaxLogin >= 3) {
setIsLocked(true);
setErrorMessage("Kesempatan Anda sudah habis, tunggu 15 detik lagi")
setErrorMessage('Kesempatan Anda sudah habis, tunggu 15 detik lagi');
return;
}
const data = await loginAction.mutateAsync(formData);
navigate("/dashboard")
// await signInWithPassword({ email: data.email, password: data.password });
// await checkUserSession?.();
console.log(data)
navigate('/dashboard');
// router.refresh();
console.log(data);
} catch (error) {
console.error(error);
const feedbackMessage = getErrorMessage(error);
......@@ -143,7 +134,6 @@ export function JwtSignInView() {
}
});
const renderForm = () => (
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
......@@ -183,7 +173,6 @@ export function JwtSignInView() {
<VisibilityProvider show>
<NewReCaptcha isValidCaptcha={isValidCaptcha} setIsValidCaptcha={setIsValidCaptcha} />
</VisibilityProvider>
</Box>
<Button
......
......@@ -8,9 +8,9 @@ import { submitLogin } from 'src/auth/store/loginSlice';
import { endpoints } from 'src/lib/axios';
import type { AppDispatch } from 'src/store.tsx';
const getUserInfo = async (token:any) => {
const getUserInfo = async (token: any) => {
axios.defaults.headers.common.Authorization = `Bearer ${window.atob(token)}`;
const res = await axios.post(endpoints.auth.me)
const res = await axios.post(endpoints.auth.me);
const { data } = await res.data;
return data.user;
......@@ -28,20 +28,19 @@ const useLogin = () => {
const mutation = useMutation({
mutationKey: ['login'],
// eslint-disable-next-line consistent-return
mutationFn: async (params:SignInParams) => {
const { payload: loginActionPayload } = await dispatch(submitLogin(params));
return loginActionPayload
mutationFn: async (params: SignInParams) => {
const { payload: loginActionPayload } = await dispatch(submitLogin(params));
return loginActionPayload;
},
onSuccess:(data)=>{
console.log("Login Berhasil 123", data)
navigate("/dashboard");
navigate(0)
// window.location.href = "/dashboard";
onSuccess: (data) => {
console.log('Login Berhasil', data);
navigate('/dashboard');
navigate(0);
// window.location.href = "/dashboard";
},
onError: (error: any) => {
console.error('Login failed', error);
},
onError:(error:any)=>{
console.error('Login failed', error)
}
});
return mutation;
};
......
......@@ -74,6 +74,62 @@ export function RHFNumeric({
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
// FormHelperTextProps={{ sx: { color: 'error.main' } }}
// sx={{
// input: {
// textAlign: 'right',
// ...(readOnly && {
// backgroundColor: '#f6f6f6',
// color: '#1C252E',
// WebkitTextFillColor: '#1C252E',
// }),
// },
// ...(readOnly && {
// '& .MuiInputLabel-root': {
// color: '#1C252E',
// },
// '& .Mui-disabled': {
// WebkitTextFillColor: '#1C252E',
// color: '#1C252E',
// opacity: 1,
// backgroundColor: '#f6f6f6',
// },
// }),
// }}
// sx={{
// input: {
// textAlign: 'right',
// ...(readOnly && {
// backgroundColor: '#f6f6f6',
// color: '#1C252E',
// WebkitTextFillColor: '#1C252E',
// }),
// },
// ...(readOnly && {
// '& .MuiInputLabel-root': {
// color: '#1C252E',
// },
// '& .Mui-disabled': {
// WebkitTextFillColor: '#1C252E',
// color: '#1C252E',
// opacity: 1,
// backgroundColor: '#f6f6f6',
// },
// }),
// // ✅ tambahan untuk kondisi error
// '& .Mui-error': {
// color: (theme) => theme.palette.error.main,
// },
// '& .MuiOutlinedInput-root.Mui-error .MuiOutlinedInput-notchedOutline': {
// borderColor: (theme) => theme.palette.error.main,
// },
// '& .MuiFormHelperText-root.Mui-error': {
// color: (theme) => theme.palette.error.main,
// },
// }}
sx={{
input: {
textAlign: 'right',
......@@ -83,17 +139,31 @@ export function RHFNumeric({
WebkitTextFillColor: '#1C252E',
}),
},
// === Disabled normal (tanpa error) ===
...(readOnly && {
'& .MuiInputLabel-root': {
color: '#1C252E',
'& .MuiOutlinedInput-root.Mui-disabled:not(.Mui-error)': {
backgroundColor: '#f6f6f6',
'& .MuiInputBase-input.Mui-disabled': {
WebkitTextFillColor: '#1C252E',
color: '#1C252E',
},
},
'& .Mui-disabled': {
WebkitTextFillColor: '#1C252E',
'& .MuiInputLabel-root.Mui-disabled:not(.Mui-error)': {
color: '#1C252E',
opacity: 1,
backgroundColor: '#f6f6f6',
},
}),
// === Disabled + Error (prioritas lebih tinggi) ===
'& .MuiOutlinedInput-root.Mui-disabled.Mui-error .MuiOutlinedInput-notchedOutline': {
borderColor: (theme) => theme.palette.error.main,
},
'& .MuiInputLabel-root.Mui-disabled.Mui-error': {
color: (theme) => theme.palette.error.main,
},
'& .MuiFormHelperText-root.Mui-error': {
color: (theme) => theme.palette.error.main,
},
}}
{...props}
/>
......
......@@ -44,28 +44,14 @@ export type AccountDrawerProps = IconButtonProps & {
};
export function AccountDrawer({ data = [], sx, ...other }: AccountDrawerProps) {
const user = useSelector((state: RootState) => state.user);
console.log(user)
console.log(user);
const pathname = usePathname();
const { value: open, onFalse: onClose, onTrue: onOpen } = useBoolean();
// const renderAvatar = () => (
// <AnimateBorder
// sx={{ mb: 2, p: '6px', width: 96, height: 96, borderRadius: '50%' }}
// slotProps={{
// primaryBorder: { size: 120, sx: { color: 'primary.main' } },
// }}
// >
// <Avatar src={user?.photoURL} alt={user?.displayName} sx={{ width: 1, height: 1 }}>
// {user?.displayName?.charAt(0).toUpperCase()}
// </Avatar>
// </AnimateBorder>
// );
const renderList = () => (
<MenuList
disablePadding
......@@ -158,7 +144,7 @@ export function AccountDrawer({ data = [], sx, ...other }: AccountDrawerProps) {
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
mb:3,
mb: 3,
}}
>
{/* {renderAvatar()} */}
......
import { CONFIG } from 'src/global-config';
// import { CONFIG } from 'src/global-config';
import { AboutView } from 'src/sections/about/view';
// import { AboutView } from 'src/sections/about/view';
// ----------------------------------------------------------------------
// // ----------------------------------------------------------------------
const metadata = { title: `About us - ${CONFIG.appName}` };
// const metadata = { title: `About us - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
// export default function Page() {
// return (
// <>
// <title>{metadata.title}</title>
<AboutView />
</>
);
}
// <AboutView />
// </>
// );
// }
import { CONFIG } from 'src/global-config';
import { DashboardView } from 'src/sections/dashboard/view';
// import { OverviewAppView } from 'src/sections/overview/app/view';
......@@ -11,7 +12,8 @@ export default function OverviewAppPage() {
<>
<title>{metadata.title}</title>
{/* <OverviewAppView /> */}
aaa
{/* aaa */}
<DashboardView />
</>
);
}
......@@ -119,6 +119,7 @@ export const dashboardRoutes: RouteObject[] = [
{ index: true, element: <OverviewUnifikasiDnPage /> },
{ path: 'dn', element: <OverviewUnifikasiDnPage /> },
{ path: 'dn/new', element: <OverviewUnifikasiRekamDnPage /> },
{ path: 'dn/:id/:type', element: <OverviewUnifikasiRekamDnPage /> },
{ path: 'nr', element: <OverviewUnifikasiNrPage /> },
{ path: 'nr/new', element: <OverviewUnifikasiRekamNrPage /> },
{ path: 'ssp', element: <OverviewUnifikasiSspPage /> },
......
import React from 'react';
import { GridPreferencePanelsValue, useGridApiContext } from '@mui/x-data-grid-premium';
import { IconButton, Tooltip } from '@mui/material';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
export default function CustomColumnsButton() {
// ✅ React.memo: cegah render ulang tanpa alasan
const CustomColumnsButton: React.FC = React.memo(() => {
const apiRef = useGridApiContext();
const handleClick = () => {
apiRef.current?.showPreferences('columns' as GridPreferencePanelsValue); // buka panel Columns
};
// ✅ 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">
......@@ -23,4 +27,6 @@ export default function CustomColumnsButton() {
</IconButton>
</Tooltip>
);
}
});
export default CustomColumnsButton;
import * as React from 'react';
import { GridToolbarContainer } from '@mui/x-data-grid-premium';
import { GridToolbarContainer, GridToolbarProps } from '@mui/x-data-grid-premium';
import { Stack, Divider, IconButton, Tooltip } from '@mui/material';
import { ActionItem } from '../types/types';
import { CustomFilterButton } from './CustomFilterButton';
import CustomColumnsButton from './CustomColumnsButton'; // your custom columns button (open columns popover or showPreferences)
import CustomColumnsButton from './CustomColumnsButton';
interface CustomToolbarProps {
interface CustomToolbarProps extends GridToolbarProps {
actions?: ActionItem[][];
columns: any[]; // GridColDef[]
filterModel: any;
......@@ -13,13 +13,15 @@ interface CustomToolbarProps {
statusOptions?: { value: string; label: string }[];
}
export const CustomToolbar: React.FC<CustomToolbarProps> = ({
// ✅ 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={{
......@@ -28,8 +30,8 @@ export const CustomToolbar: React.FC<CustomToolbarProps> = ({
alignItems: 'center',
p: 1.5,
}}
{...gridToolbarProps}
>
{/* LEFT: custom action groups */}
<Stack direction="row" alignItems="center" gap={1}>
{actions.map((group, groupIdx) => (
<Stack key={groupIdx} direction="row" gap={0.5} alignItems="center">
......@@ -52,7 +54,6 @@ export const CustomToolbar: React.FC<CustomToolbarProps> = ({
))}
</Stack>
{/* RIGHT: columns button + custom filter */}
<Stack direction="row" alignItems="center" gap={0.5}>
<CustomColumnsButton />
<CustomFilterButton
......@@ -64,4 +65,4 @@ export const CustomToolbar: React.FC<CustomToolbarProps> = ({
</Stack>
</GridToolbarContainer>
);
};
});
import React from 'react';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
type Props = { value?: string; revNo?: number };
const StatusChip: React.FC<Props> = ({ value, revNo }) => {
if (!value) return <Chip label="" size="small" />;
if (value === 'NORMAL-Done' && revNo !== 0) {
return (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
}}
>
<Chip
label="Normal Pengganti"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
paddingRight: '5px',
}}
/>
<Chip
label={revNo}
size="small"
variant="filled"
sx={{
position: 'absolute',
top: -6,
right: -6,
backgroundColor: '#1976d2',
color: '#fff',
borderRadius: '50%',
fontWeight: 500,
width: 18,
height: 18,
minWidth: 0,
border: '2px solid #fff',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',
'& .MuiChip-label': {
padding: 0,
fontSize: '0.65rem',
lineHeight: 1,
},
}}
/>
</Box>
);
}
if (value === 'NORMAL-Done' && revNo === 0) {
return (
<Chip
label="Normal"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: '500',
}}
/>
);
}
if (value === 'AMENDED') {
return (
<Chip
label="Diganti"
size="small"
variant="outlined"
sx={{
color: '#fff',
backgroundColor: '#f38c28',
borderRadius: '8px',
fontWeight: 500,
border: 'none',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.15)',
}}
/>
);
}
if (value === 'CANCELLED') {
return (
<Chip
label="Dibatalkan"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: '500',
}}
/>
);
}
if (value === 'DRAFT') {
return (
<Chip
label="Draft"
size="small"
variant="outlined"
sx={{
borderColor: '#9e9e9e',
color: '#616161',
borderRadius: '8px',
}}
/>
);
}
return <Chip label={value} size="small" />;
};
export default React.memo(StatusChip);
// components/CancelConfirmationDialog.tsx
import React from 'react';
import {
Dialog,
......@@ -21,21 +20,21 @@ const CancelConfirmationDialog: React.FC<CancelConfirmationDialogProps> = ({
onClose,
onConfirm,
selectedCount,
}) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Konfirmasi Pembatalan</DialogTitle>
<DialogContent>
<Typography>
Apakah Anda yakin ingin membatalkan {selectedCount} data yang dipilih?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Batal</Button>
<Button onClick={onConfirm} color="error" variant="contained">
Ya, Batalkan
</Button>
</DialogActions>
</Dialog>
);
};
}) => (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Konfirmasi Pembatalan</DialogTitle>
<DialogContent>
<Typography>
Apakah Anda yakin ingin membatalkan {selectedCount} data yang dipilih?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Batal</Button>
<Button onClick={onConfirm} color="error" variant="contained">
Ya, Batalkan
</Button>
</DialogActions>
</Dialog>
);
export default CancelConfirmationDialog;
import React, { useEffect, useState } from 'react';
import { enqueueSnackbar } from 'notistack';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogContent from '@mui/material/DialogContent';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import useCetakPdfDn from '../../hooks/useCetakPdfDn';
import normalizePayloadCetakPdf from '../../utils/normalizePayloadCetakPdf';
interface ModalCetakPdfDnProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
const formatTanggalIndo = (isoDate: string | undefined | null): string => {
if (!isoDate) return '';
const date = new Date(isoDate);
const formatter = new Intl.DateTimeFormat('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
});
return formatter.format(date);
};
const ModalCetakPdfDn: React.FC<ModalCetakPdfDnProps> = ({ payload, isOpen, onClose }) => {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { mutateAsync } = useCetakPdfDn({
onError: (error: any) => {
enqueueSnackbar(error?.message || 'Gagal memuat PDF', { variant: 'error' });
setLoading(false);
},
onSuccess: (res: any) => {
const fileUrl = res?.url || res?.data?.url;
if (!fileUrl) {
enqueueSnackbar('URL PDF tidak ditemukan di respons API', { variant: 'warning' });
setLoading(false);
return;
}
setPdfUrl(fileUrl);
setLoading(false);
enqueueSnackbar(res?.MsgStatus || 'PDF berhasil dibentuk', { variant: 'success' });
},
});
useEffect(() => {
const runCetak = async () => {
if (!isOpen || !payload) return;
setLoading(true);
setPdfUrl(null);
try {
// Payload sudah lengkap dari parent (sudah ada namaObjekPajak, pasalPPh, statusPPh)
const normalized = normalizePayloadCetakPdf(payload);
console.log('📤 Payload final cetak PDF:', normalized);
await mutateAsync(normalized);
} catch (err) {
console.error('❌ Error cetak PDF:', err);
enqueueSnackbar('Gagal generate PDF', { variant: 'error' });
setLoading(false);
}
};
runCetak();
}, [isOpen, payload, mutateAsync]);
return (
<DialogUmum
maxWidth="lg"
isOpen={isOpen}
onClose={onClose}
title="Detail Bupot Unifikasi (PDF)"
>
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
{loading && (
<Box display="flex" justifyContent="center" alignItems="center" height="60vh">
<CircularProgress />
</Box>
)}
{!loading && pdfUrl && (
<iframe
src={pdfUrl}
style={{
width: '100%',
height: '80vh',
border: 'none',
borderRadius: 8,
}}
title="Preview PDF Bupot"
/>
)}
{!loading && !pdfUrl && (
<Box textAlign="center" color="text.secondary" py={4}>
PDF tidak tersedia untuk ditampilkan.
</Box>
)}
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakPdfDn;
import React, { useEffect, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import dnApi from '../../utils/api';
import queryKey from '../../constant/queryKey';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useDeleteDn from '../../hooks/useDeleteDn';
interface ModalDeleteDnProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogDelete,
setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus',
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useDeleteDn({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogDelete(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogDelete, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin akan menghapus data ini?"
description="Data yang sudah dihapus tidak dapat dikembalikan."
actionTitle="Hapus"
isOpen={isOpenDialogDelete}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={onSubmit}
/>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalDeleteDn;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useUpload from '../../hooks/useUpload';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Grid';
import { Field } from 'src/components/hook-form';
import MenuItem from '@mui/material/MenuItem';
import { useSelector } from 'react-redux';
import { RootState } from 'src/store';
import Agreement from 'src/shared/components/agreement/Agreement';
import { FormProvider, useForm } from 'react-hook-form';
import { LoadingButton } from '@mui/lab';
interface ModalUploadDnProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
// onConfirmUpload?: () => void;
onConfirmUpload?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
}) => {
const queryClient = useQueryClient();
const uploadDn = useUpload();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const signer = useSelector((state: RootState) => state.user.data.signer);
const { mutateAsync } = useUpload({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
}
};
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={uploadDn.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 ModalUploadDn;
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import React from 'react';
import React, { useEffect } from 'react';
import { Field } from 'src/components/hook-form';
import { JENIS_DOKUMEN } from '../../constant';
import dayjs from 'dayjs';
import { useSelector } from 'react-redux';
import { RootState } from 'src/store';
import { useFormContext } from 'react-hook-form';
const DokumenReferensi = () => {
const MockNitku = [
{
nama: '1091031210912281000000',
},
{
nama: '1091031210912281000001',
},
];
const { watch, setValue } = useFormContext<Record<string, any>>();
const nitku = useSelector((state: RootState) => state.user.data.nitku_trial);
const nitkuValue = watch('idTku');
useEffect(() => {
if (!nitkuValue && nitku) {
setValue('idTku', nitku);
}
}, [nitku, nitkuValue, setValue]);
return (
<>
<Grid sx={{ mb: 3 }} container rowSpacing={2} columnSpacing={2}>
......@@ -35,15 +41,16 @@ const DokumenReferensi = () => {
<Field.Text name="nomorDok" label="Nomor Dokumen" />
</Grid>
<Grid size={{ md: 6 }}>
<Field.DatePicker name="tglDok" label="Tanggal Dokumen" />
<Field.DatePicker
name="tglDok"
label="Tanggal Dokumen"
maxDate={dayjs()}
minDate={dayjs('2025-01-01')}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Select name="idTku" label="NITKU Pemotong">
{MockNitku.map((item, index) => (
<MenuItem key={index} value={item.nama}>
{item.nama}
</MenuItem>
))}
<MenuItem value={nitku}>{nitku}</MenuItem>
</Field.Select>
</Grid>
</Grid>
......
import { FC } from 'react';
import { FC, Fragment, memo } from 'react';
import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material';
import { ChevronRightRounded, CloseRounded } from '@mui/icons-material';
import { m } from 'framer-motion';
......@@ -9,16 +9,11 @@ interface PanduanDnRekamProps {
isOpen: boolean;
}
const PanduanDnRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => {
return (
<Box position="sticky">
{/* Tombol toggle */}
<Box
height="100%"
display={isOpen ? 'none' : 'flex'}
justifyContent="center"
alignItems="center"
>
const PanduanDnRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => (
<Box position="sticky">
{/* Tombol toggle */}
{!isOpen && (
<Box height="100%" display="flex" justifyContent="center" alignItems="center">
<Button
variant="contained"
sx={{
......@@ -48,105 +43,116 @@ const PanduanDnRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => {
</span>
</Button>
</Box>
)}
{/* Konten panduan */}
{isOpen && (
<m.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1, transition: { delay: 0.2 } }}
>
<Card>
<CardHeader
avatar={
<img src="/assets/icon_panduan_penggunaan_1.svg" alt="Panduan" loading="lazy" />
}
sx={{
{/* Konten panduan */}
{isOpen && (
<m.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1, transition: { delay: 0.2 } }}
>
<Card>
<CardHeader
avatar={
<img src="/assets/icon_panduan_penggunaan_1.svg" alt="Panduan" loading="lazy" />
}
sx={{
backgroundColor: '#123375',
color: '#FFFFFF',
p: 2,
'& .MuiCardHeader-title': { fontSize: 18 },
}}
action={
<IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}>
<CloseRounded />
</IconButton>
}
title="Panduan Penggunaan"
/>
<CardContent
sx={{
maxHeight: 300,
overflow: 'auto',
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': {
backgroundColor: '#f0f0f0',
borderRadius: 8,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: '#123375',
color: '#FFFFFF',
padding: '16px',
'& .MuiCardHeader-title': {
fontSize: 18,
},
}}
action={
<IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}>
<CloseRounded />
</IconButton>
}
title="Panduan Penggunaan"
/>
borderRadius: 8,
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: '#0d2858',
},
scrollbarWidth: 'thin',
scrollbarColor: '#123375 #f0f0f0',
}}
>
{/* Deskripsi Form */}
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}>
<strong>Deskripsi Form:</strong>
<br />
{PANDUAN_REKAM_DN.description.intro}
</Typography>
<CardContent
sx={{
maxHeight: 300,
overflow: 'auto',
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': { backgroundColor: '#f0f0f0', borderRadius: 8 },
'&::-webkit-scrollbar-thumb': { backgroundColor: '#123375', borderRadius: 8 },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: '#0d2858' },
scrollbarWidth: 'thin',
scrollbarColor: '#123375 #f0f0f0',
}}
>
{/* Deskripsi Form */}
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}>
<span style={{ fontWeight: 600 }}>Deskripsi Form:</span>
<br />
{PANDUAN_REKAM_DN.description.intro}
</Typography>
<Typography variant="body2">{PANDUAN_REKAM_DN.description.textList}</Typography>
<Typography variant="body2" sx={{}}>
{PANDUAN_REKAM_DN.description.textList}
</Typography>
<Box component="ol" sx={{ pl: 3, mb: 2 }}>
{PANDUAN_REKAM_DN.description.list.map((item, idx) => (
<Typography key={idx} variant="body2" component="li">
{item}
</Typography>
))}
</Box>
<Box component="ol" sx={{ pl: 3, mb: 2 }}>
{PANDUAN_REKAM_DN.description.list.map((item, idx) => (
<Typography key={`desc-${idx}`} variant="body2" component="li">
{item}
</Typography>
))}
</Box>
<Typography variant="body2" sx={{ mb: 2 }}>
{PANDUAN_REKAM_DN.description.closing}
</Typography>
<Typography variant="body2" sx={{ mb: 2 }}>
{PANDUAN_REKAM_DN.description.closing}
</Typography>
{/* Bagian-bagian */}
{PANDUAN_REKAM_DN.sections.map((section, i) => (
<Box key={i} sx={{ mb: 2 }}>
<Typography
variant="body2"
sx={{ fontWeight: 'bold', fontSize: '0.95rem', mb: 0.5 }}
>
{section.title}
</Typography>
{/* Bagian-bagian */}
{PANDUAN_REKAM_DN.sections.map((section, i) => (
<Box key={`section-${i}`} sx={{ mb: 2 }}>
<Typography
variant="body2"
sx={{ fontWeight: 'bold', fontSize: '0.95rem', mb: 0.5 }}
>
{section.title}
</Typography>
<Box component="ul" sx={{ pl: 2, listStyle: 'disc' }}>
{section.items.map((item, idx) => (
<Box key={idx} component="li" sx={{ mb: 0.5 }}>
<Box component="ul" sx={{ pl: 2, listStyle: 'disc' }}>
{section.items.map((item, idx) => (
<Fragment key={`item-${i}-${idx}`}>
<Box component="li" sx={{ mb: 0.5 }}>
<Typography variant="body2" component="span">
{item.text}
</Typography>
{item.subItems.length > 0 && (
{item.subItems?.length > 0 && (
<Box component="ol" sx={{ pl: 3, listStyle: 'decimal' }}>
{item.subItems.map((sub, subIdx) => (
<Typography key={subIdx} variant="body2" component="li">
<Typography
key={`sub-${i}-${idx}-${subIdx}`}
variant="body2"
component="li"
>
{sub}
</Typography>
))}
</Box>
)}
</Box>
))}
</Box>
</Fragment>
))}
</Box>
))}
</CardContent>
</Card>
</m.div>
)}
</Box>
);
};
</Box>
))}
</CardContent>
</Card>
</m.div>
)}
</Box>
);
export default PanduanDnRekam;
export default memo(PanduanDnRekam);
......@@ -528,6 +528,14 @@ export const JENIS_REKAP = {
};
// Start Dari Sini
export const FG_STATUS_DN = {
DRAFT: 'DRAFT',
NORMAL_DONE: 'NORMAL-Done',
AMENDED: 'AMENDED',
CANCELLED: 'CANCELLED',
};
export const FG_FASILITAS_DN = {
SKB_PPH_PASAL_22: '1',
SKB_PPH_PASAL_23: '2',
......
import { useMutation } from '@tanstack/react-query';
import { TCancelDnRequest, TCancelDnResponse } from '../types/types';
import dnApi from '../utils/api';
const useCancelDn = (props?: any) =>
useMutation<TCancelDnResponse, Error, TCancelDnRequest>({
mutationKey: ['cancel-dn'],
mutationFn: (payload) => dnApi.cancel(payload),
...props,
});
export default useCancelDn;
import { useMutation } from '@tanstack/react-query';
import dnApi from '../utils/api';
const useCetakPdfDn = (options?: any) =>
useMutation({
mutationKey: ['unifikasi', 'dn', 'cetak-pdf'],
mutationFn: async (params: any) => dnApi.cetakPdfDetail(params),
...options,
});
export default useCetakPdfDn;
import { useMutation } from '@tanstack/react-query';
import { TDeleteDnRequest, TBaseResponseAPI } from '../types/types';
import dnApi from '../utils/api';
const useDeleteDn = (props?: any) =>
useMutation<TBaseResponseAPI<null>, Error, TDeleteDnRequest>({
mutationKey: ['delete-dn'],
mutationFn: (payload) => dnApi.deleteDn(payload),
...props,
});
export default useDeleteDn;
......@@ -3,11 +3,10 @@ import { TBaseResponseAPI, TGetListDataKOPDnResult } from '../types/types';
import queryKey from '../constant/queryKey';
import dnApi from '../utils/api';
const useGetKodeObjekPajak = (params?: Record<string, any>) => {
return useQuery<TBaseResponseAPI<TGetListDataKOPDnResult>>({
const useGetKodeObjekPajak = (params?: Record<string, any>) =>
useQuery<TBaseResponseAPI<TGetListDataKOPDnResult>>({
queryKey: queryKey.getKodeObjekPajak(params),
queryFn: () => dnApi.getKodeObjekPajakDn(params),
});
};
export default useGetKodeObjekPajak;
......@@ -35,6 +35,7 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
name: ['thnPajak', 'fgFasilitas', 'fgIdDipotong', 'jmlBruto', 'tarif'],
});
// eslint-disable-next-line @typescript-eslint/no-shadow
const calculateAndSetPphDipotong = (
thnPajak: number,
fgFasilitas: string,
......
......@@ -2,8 +2,6 @@ import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import dnApi from '../utils/api';
import { TPostDnRequest } from '../types/types';
// import { dnApi } from '../utils';
// import { TPostDnRequest } from '../types';
const transformParams = ({ isPengganti = false, ...dnData }: any): TPostDnRequest => {
const {
......@@ -58,8 +56,8 @@ const transformParams = ({ isPengganti = false, ...dnData }: any): TPostDnReques
return {
id: !isPengganti ? (id ?? null) : null,
idBupot: idBupot ?? '',
noBupot: noBupot ?? '',
idBupot: idBupot ?? null,
noBupot: noBupot ?? null,
npwpPemotong: npwpLog,
idTku: idTku ?? '',
masaPajak: msPajak ? dayjs(msPajak).format('MM') : '',
......@@ -94,12 +92,11 @@ const transformParams = ({ isPengganti = false, ...dnData }: any): TPostDnReques
};
};
const useSaveDn = (props?: any) => {
return useMutation({
const useSaveDn = (props?: any) =>
useMutation({
mutationKey: ['Save-Dn'],
mutationFn: (params: any) => dnApi.saveDn(transformParams(params)),
...props,
});
};
export default useSaveDn;
// hooks/useUpload.ts
import { useMutation } from '@tanstack/react-query';
import dnApi from '../utils/api';
const useUpload = (props?: any) =>
useMutation({
mutationKey: ['upload-dn'],
mutationFn: (payload: { id: string | number }) => dnApi.upload(payload),
...props,
});
export default useUpload;
This diff is collapsed.
......@@ -142,3 +142,22 @@ export type TPostDnRequest = {
keterangan4: string;
keterangan5: string;
};
export type TPostUpload = {
id: string;
};
export type TDeleteDnRequest = {
id: string;
};
export type TCancelDnRequest = {
id: string | number;
tglPembatalan: string; // format: DDMMYYYY
};
export type TCancelDnResponse = TBaseResponseAPI<{
id: string | number;
statusBatal?: string;
message?: string;
}>;
import { useMutation } from '@tanstack/react-query';
import vswpApi from '../utils/api';
import { TVswpData } from '../types/types';
type TVswpType = 'npwp' | 'nik';
type TVswpPayload = { type: TVswpType; value: string };
export const useGetVswp = () =>
useMutation<TVswpData | null, Error, TVswpPayload>({
mutationKey: ['vswp'],
mutationFn: async ({ type, value }: TVswpPayload) => {
if (type === 'npwp') return vswpApi.getVswpByNpwp(value);
if (type === 'nik') return vswpApi.getVswpByNik(value);
throw new Error('Invalid type');
},
});
export type TVswpData = {
npwp: string;
tujuan: string;
nik: string;
nama: string;
alamat: string;
statusWp: string;
statusSpt: string;
};
export type TVswpResponse = {
status: number; // 1 = success, 0 = gagal
data: TVswpData | null;
};
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export * from './dashboard-view';
export const formatRupiah = (value: any) => {
if (!value) return '';
const num = parseFloat(value); // handle string angka
if (isNaN(num)) return value;
return new Intl.NumberFormat('id-ID').format(num);
};
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment