Commit cf7f44f4 authored by Rais Aryaguna's avatar Rais Aryaguna

feat: Add hooks and views for managing Bulanan Bupot

- Implemented `useGetBulanan` hook for fetching Bulanan data.
- Created `usePphDipotong` hook for handling PPh calculations.
- Added `useSaveBulanan` hook for saving Bulanan data.
- Defined types in `types.ts` for API responses and requests.
- Developed API utility functions in `api.tsx` for fetching and saving data.
- Created `bulananClient.tsx` for Axios instance with interceptors.
- Added utility functions in `utils.tsx` for date and pagination handling.
- Built `BulananListView` for displaying Bulanan records in a data grid.
- Developed `BulananRekamView` for recording new Bulanan entries with form validation.
- Organized view exports in `index.ts` for easier imports.
parent 33a1b7e3
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import { useMemo } from 'react';
import { fetcher, endpoints } from 'src/lib/axios-ctas-box';
// ----------------------------------------------------------------------
const swrOptions: SWRConfiguration = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
// ----------------------------------------------------------------------
export function useKodeNegara() {
const url = endpoints.masterData.kodeNegara;
const { data, isLoading, error, isValidating } = useSWR<{
data: { kode: string; nama: string }[];
}>(url, fetcher, {
...swrOptions,
});
const memoizedValue = useMemo(
() => ({
kodeNegara: data?.data || [],
kodeNegaraLoading: isLoading,
kodeNegaraError: error,
kodeNegaraValidating: isValidating,
kodeNegaraEmpty: !isLoading && !data?.data.length,
}),
[data, error, isLoading, isValidating]
);
return memoizedValue;
}
// const getServices = async (params) => {
// const response = await axiosBupot.get('/mst_services', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getGoods = async (params) => {
// const response = await axiosBupot.get('/mst_goods', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getSatuan = async (params) => {
// const response = await axiosBupot.get('/mst_satuan', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getPenandatangan = async () => {
// const {
// data: { status: statusRes, message, data },
// } = await axiosBupot.get('/signer');
// if (statusRes === 'error') {
// throw new Error(message);
// }
// return data;
// };
// const getKodeObjekPajakBpnr = async (params) => {
// const response = await axiosBupot.get('/mst_kop_bpnr', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getKodeObjekPajakBpu = async (params) => {
// const response = await axiosBupot.get('/mst_kop_bpu', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getKodeObjekPajakBpsp = async (params) => {
// const response = await axiosBupot.get('/mst_kop_bpsp', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getAllKodeObjekPajak = async (params) => {
// const response = await axiosBupot.get('/mst_kop_all', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body;
// };
// const getKeteranganTambahan = async (params) => {
// const response = await axiosBupot.get('/mst_faktur_keterangan', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
// const getIdKeteranganTambahan = async (params) => {
// const response = await axiosBupot.get('/mst_faktur_idtambahan', {
// params,
// });
// const body = response.data;
// if (
// body.status === 'fail' ||
// body.status === 'error' ||
// body.status === '0' ||
// response.status !== 200
// ) {
// throw new Error(body.message || 'System tidak dapat memenuhi permintaan, coba beberapa saat lagi');
// }
// return body.data;
// };
...@@ -26,6 +26,8 @@ export type ConfigValue = { ...@@ -26,6 +26,8 @@ export type ConfigValue = {
amplify: { userPoolId: string; userPoolWebClientId: string; region: string }; amplify: { userPoolId: string; userPoolWebClientId: string; region: string };
auth0: { clientId: string; domain: string; callbackUrl: string }; auth0: { clientId: string; domain: string; callbackUrl: string };
supabase: { url: string; key: string }; supabase: { url: string; key: string };
nodesandbox: { api: string };
apiJava: { api: string };
}; };
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
...@@ -79,4 +81,17 @@ export const CONFIG: ConfigValue = { ...@@ -79,4 +81,17 @@ export const CONFIG: ConfigValue = {
url: import.meta.env.VITE_SUPABASE_URL ?? '', url: import.meta.env.VITE_SUPABASE_URL ?? '',
key: import.meta.env.VITE_SUPABASE_ANON_KEY ?? '', key: import.meta.env.VITE_SUPABASE_ANON_KEY ?? '',
}, },
/**
* nodesandbox
*/
nodesandbox: {
api: import.meta.env.VITE_HOST_API ?? '',
},
/**
* java
*/
apiJava: {
api: import.meta.env.VITE_HOST_API ?? '',
},
}; };
import type { AxiosRequestConfig, AxiosInstance } from 'axios';
import axios from 'axios';
import { JWT_STORAGE_KEY } from 'src/auth/context/jwt';
import { CONFIG } from 'src/global-config';
const API_CONFIGS = {
nodesandbox: {
baseURL: CONFIG.nodesandbox.api, // misal: https://api.yourdomain.com
name: 'nodesandbox API',
},
} as const;
const createAxiosInstance = (baseURL: string, name: string): AxiosInstance => {
const instance = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
});
// Request Interceptor - Tambahkan token jika diperlukan
instance.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem(JWT_STORAGE_KEY);
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response Interceptor - Handle errors
instance.interceptors.response.use(
(response) => response,
(error) => {
const message = error?.response?.data?.message || error?.message || 'Something went wrong!';
console.error(`[${name}] Axios error:`, message);
return Promise.reject(new Error(message));
}
);
return instance;
};
export const axiosnodesandbox = createAxiosInstance(
API_CONFIGS.nodesandbox.baseURL,
API_CONFIGS.nodesandbox.name
);
export default axiosnodesandbox;
type FetcherArgs = string | [string, AxiosRequestConfig & { method?: string }];
export const fetcher = async <T = unknown>(args: FetcherArgs): Promise<T> => {
try {
const [url, config = {}] = Array.isArray(args) ? args : [args, {}];
const { method = 'GET', ...restConfig } = config;
const res = await axiosnodesandbox.request<T>({
url,
method,
...restConfig,
});
return res.data;
} catch (error) {
console.error('[sandbox API] Fetcher failed:', error);
throw error;
}
};
export const endpoints = {
pph21: {
bulanan: {
list: '/IF_TXR_028/a0',
delete: '/IF_TXR_028/a0/delete',
upload: '/IF_TXR_028/a0/upload',
canceled: '/IF_TXR_028/a0/batal',
},
},
masterData: {
kodeNegara: '/sandbox/mst_negara',
services: '/sandbox/mst_services',
goods: '/sandbox/mst_goods',
satuan: '/sandbox/mst_satuan',
signer: '/sandbox/signer',
kop_bpnr: '/sandbox/mst_kop_bpnr',
kop_bpu: '/sandbox/mst_kop_bpu',
kop_bpsp: '/sandbox/mst_kop_bpsp',
kop_all: '/sandbox/mst_kop_all',
faktur_keterangan: '/sandbox/mst_faktur_kode',
faktur_idtambahan: '/sandbox/mst_faktur_keterangan',
},
} as const;
...@@ -62,7 +62,7 @@ export const endpoints = { ...@@ -62,7 +62,7 @@ export const endpoints = {
calendar: '/api/calendar', calendar: '/api/calendar',
auth: { auth: {
// me: '/api/auth/me', // me: '/api/auth/me',
me:'/sandbox/userinfo', me: '/sandbox/userinfo',
signIn: '/sandbox/login', signIn: '/sandbox/login',
signUp: '/api/auth/sign-up', signUp: '/api/auth/sign-up',
}, },
......
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { BulananListView } from 'src/sections/bupot-21-26/bupot-bulanan/view';
const metadata = { title: `E-Bupot 21/26- ${CONFIG.appName}` }; const metadata = { title: `E-Bupot 21/26- ${CONFIG.appName}` };
...@@ -7,7 +8,7 @@ export default function Page() { ...@@ -7,7 +8,7 @@ export default function Page() {
<> <>
<title>{metadata.title}</title> <title>{metadata.title}</title>
<p>Bupot Bulanan</p> <BulananListView />
</> </>
); );
} }
import { CONFIG } from 'src/global-config';
import { BulananRekamView } from 'src/sections/bupot-21-26/bupot-bulanan/view';
const metadata = { title: `E-Bupot Bulanan Rekam- ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<BulananRekamView />
</>
);
}
...@@ -105,10 +105,13 @@ export const paths = { ...@@ -105,10 +105,13 @@ export const paths = {
sspNew: `${ROOTS.UNIFIKASI}/ssp/new`, sspNew: `${ROOTS.UNIFIKASI}/ssp/new`,
dokumenDipersamakanNew: `${ROOTS.UNIFIKASI}/dokumen-dipersamakan/new`, dokumenDipersamakanNew: `${ROOTS.UNIFIKASI}/dokumen-dipersamakan/new`,
}, },
// Bupot 21/26
pph21: { pph21: {
root: ROOTS.PPH21, root: ROOTS.PPH21,
bulanan: `${ROOTS.PPH21}/bulanan`, bulanan: `${ROOTS.PPH21}/bulanan`,
detailsBulanan: (id: string) => `/bulanan/${id}`, bulananRekam: `${ROOTS.PPH21}/bulanan/rekam`,
bulananUbah: (id: string) => `/bulanan/${id}/ubah`,
bulananPengganti: (id: string) => `/bulanan/${id}/pengganti`,
bupotFinal: `${ROOTS.PPH21}/bupot-final`, bupotFinal: `${ROOTS.PPH21}/bupot-final`,
detailsBupotFinal: (id: string) => `/bupot-final/${id}`, detailsBupotFinal: (id: string) => `/bupot-final/${id}`,
tahuan: `${ROOTS.PPH21}/tahunan`, tahuan: `${ROOTS.PPH21}/tahunan`,
......
...@@ -57,6 +57,7 @@ const AccountChangePasswordPage = lazy( ...@@ -57,6 +57,7 @@ const AccountChangePasswordPage = lazy(
// Bupot 21/26 // Bupot 21/26
const OverviewBupotBulananPage = lazy(() => import('src/pages/pph21/bupotBulanan')); const OverviewBupotBulananPage = lazy(() => import('src/pages/pph21/bupotBulanan'));
const OverviewBupotBulananRekamPage = lazy(() => import('src/pages/pph21/bupotBulananRekam'));
const OverviewBupotFinalTdkFinalPage = lazy(() => import('src/pages/pph21/bupotFinaltidakFinal')); const OverviewBupotFinalTdkFinalPage = lazy(() => import('src/pages/pph21/bupotFinaltidakFinal'));
const OverviewBupotA1Page = lazy(() => import('src/pages/pph21/bupoTahunanA1')); const OverviewBupotA1Page = lazy(() => import('src/pages/pph21/bupoTahunanA1'));
const OverviewBupotPasal26Page = lazy(() => import('src/pages/pph21/bupotPasal26')); const OverviewBupotPasal26Page = lazy(() => import('src/pages/pph21/bupotPasal26'));
...@@ -137,6 +138,9 @@ export const dashboardRoutes: RouteObject[] = [ ...@@ -137,6 +138,9 @@ export const dashboardRoutes: RouteObject[] = [
children: [ children: [
{ index: true, element: <OverviewBupotBulananPage /> }, { index: true, element: <OverviewBupotBulananPage /> },
{ path: 'bulanan', element: <OverviewBupotBulananPage /> }, { path: 'bulanan', element: <OverviewBupotBulananPage /> },
{ path: 'bulanan/rekam', element: <OverviewBupotBulananRekamPage /> },
{ path: 'bulanan/:id/ubah', element: <OverviewBupotBulananRekamPage /> },
{ path: 'bulanan/:id/pengganti', element: <OverviewBupotBulananRekamPage /> },
{ path: 'bupot-final', element: <OverviewBupotFinalTdkFinalPage /> }, { path: 'bupot-final', element: <OverviewBupotFinalTdkFinalPage /> },
{ path: 'tahunan', element: <OverviewBupotA1Page /> }, { path: 'tahunan', element: <OverviewBupotA1Page /> },
{ path: 'bupot-26', element: <OverviewBupotPasal26Page /> }, { path: 'bupot-26', element: <OverviewBupotPasal26Page /> },
......
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import dayjs from 'dayjs';
import { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useParams } from 'react-router';
import { useKodeNegara } from 'src/actions/master-data';
import { Field } from 'src/components/hook-form';
type IdentitasProps = {
isPengganti?: boolean;
};
const Identitas = ({ isPengganti }: IdentitasProps) => {
const { dnId } = useParams();
const { setValue, watch } = useFormContext();
const tanggalPemotongan = watch('tglPemotongan');
const [jumlahKeterangan, setJumlahKeterangan] = useState<number>(0);
const maxKeterangan = 5;
const handleTambah = () => {
if (jumlahKeterangan < maxKeterangan) {
setJumlahKeterangan(jumlahKeterangan + 1);
}
};
const handleHapus = () => {
if (jumlahKeterangan > 0) {
const newCount = jumlahKeterangan - 1;
setJumlahKeterangan(newCount);
setValue(`keterangan${newCount + 1}`, null);
}
};
// auto set thnPajak dan msPajak berdasarkan tanggalPemotongan
useEffect(() => {
if (tanggalPemotongan) {
const date = dayjs(tanggalPemotongan);
setValue('thnPajak', date.format('YYYY'));
setValue('msPajak', date.format('MM'));
} else {
setValue('thnPajak', '');
setValue('msPajak', '');
}
}, [tanggalPemotongan, setValue]);
const { kodeNegara } = useKodeNegara();
return (
<>
<Grid container rowSpacing={2} alignItems="center" columnSpacing={2} sx={{ mb: 4 }}>
<Grid size={{ md: 6 }}>
<Field.DatePicker
name="tglPemotongan"
label="Tanggal Pemotongan"
format="DD/MM/YYYY"
maxDate={dayjs()}
/>
</Grid>
<Grid size={{ md: 3 }}>
<Field.DatePicker name="thnPajak" label="Tahun Pajak" view="year" format="YYYY" />
</Grid>
<Grid size={{ md: 3 }}>
<Field.DatePicker name="msPajak" label="Masa Pajak" view="month" format="MM" />
</Grid>
{/* NPWP dengan onChange langsung */}
<Grid size={{ md: 6 }}>
<Field.Text
name="idDipotong"
label="NPWP"
onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 16); // hanya angka, max 16
setValue('idDipotong', value, { shouldValidate: true, shouldDirty: true });
setValue('nitku', value.length === 16 ? value + '000000' : value, {
shouldValidate: true,
shouldDirty: true,
});
}}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="namaDipotong" label="Nama" />
</Grid>
<Grid size={{ md: 12 }}>
<Field.Text name="alamatDipotong" label="Alamat" />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="email" label="Email (optional)" />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="posisiJabatan" label="Jabatan" />
</Grid>
<Grid size={{ md: 3 }} alignSelf="center">
<Field.Checkbox name="fgKaryawanAsing" label="Status Karyawan Asing" />
</Grid>
<Grid size={{ md: 3 }}>
<Field.Autocomplete
name="kodeNegara"
label="Negara"
options={kodeNegara.map((val) => ({ label: val.nama, ...val }))}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="passport" label="Paspor" />
</Grid>
</Grid>
{/* Tambah / Hapus Keterangan */}
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Box
sx={{
borderRadius: '18px',
border: jumlahKeterangan >= maxKeterangan ? '1px solid #eee' : '1px solid #2e7d3280',
color: jumlahKeterangan >= maxKeterangan ? '#eee' : '#2e7d3280',
p: '0px 10px',
}}
>
<Button disabled={jumlahKeterangan >= maxKeterangan} onClick={handleTambah}>
Tambah Keterangan
</Button>
</Box>
<Box
sx={{
borderRadius: '18px',
border: jumlahKeterangan === 0 ? '1px solid #eee' : '1px solid #f44336',
color: jumlahKeterangan === 0 ? '#eee' : '#f44336',
p: '0px 10px',
}}
>
<Button disabled={jumlahKeterangan === 0} onClick={handleHapus}>
Hapus Keterangan
</Button>
</Box>
</Box>
<Box sx={{ mb: 3 }}>
{Array.from({ length: jumlahKeterangan }).map((_, i) => (
<Grid size={{ md: 12 }} key={i}>
<Field.Text
sx={{ mb: 2 }}
name={`keterangan${i + 1}`}
label={`Keterangan Tambahan ${i + 1}`}
/>
</Grid>
))}
</Box>
</>
);
};
export default Identitas;
import { Grid } from '@mui/material';
import { Field } from 'src/components/hook-form';
import {
FG_FASILITAS_PPH_21,
FG_PERHITUNGAN,
FG_PERHITUNGAN_TEXT,
KODE_OBJEK_PAJAK,
PTKP,
PTKP_TEXT,
PTKP_TITLE,
} from '../../constant';
import { useMemo } from 'react';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
import { useFormContext } from 'react-hook-form';
import { LoadingButton } from '@mui/lab';
import { CalculateRounded } from '@mui/icons-material';
const fgPerhitunganOptions = Object.values(FG_PERHITUNGAN).map((value) => ({
value,
label: FG_PERHITUNGAN_TEXT[value],
}));
function JumlahPerhitunganForm() {
const { watch } = useFormContext();
const fgFasilitas = watch('fgFasilitas');
const ptkpOptions = useMemo(
() =>
Object.entries(PTKP)
.map(([key, value]) => ({ value, label: PTKP_TEXT[value] }))
.filter((option) => !option.value.includes(PTKP_TITLE.HB)),
[]
);
return (
<Grid container rowSpacing={2} columnSpacing={2} sx={{ my: 3 }}>
<Grid size={{ md: 3 }}>
<Field.RadioGroup
row
name="fgPerhitungan"
label="Metode Pemotongan"
options={fgPerhitunganOptions.filter((a) => a.value !== FG_PERHITUNGAN.MIXED)}
/>
</Grid>
<Grid size={{ md: 9 }}>
<Field.Autocomplete name="ptkp" label="Status PTKP" options={ptkpOptions} />
</Grid>
<Grid size={{ md: 6 }}>
<RHFNumeric name="phBruto" label="Jumlah Penghasilan (Rp)" />
</Grid>
<Grid size={{ md: 6 }}>
<RHFNumeric name="tunjanganPPh" label="Tunjangan PPh 21 (Rp)" />
</Grid>
<Grid size={{ md: 5 }}>
<RHFNumeric
name="tarif"
label="Tarif (%)"
allowDecimalValue
maxValue={100}
readOnly={
![
KODE_OBJEK_PAJAK.FINAL_38,
KODE_OBJEK_PAJAK.FINAL_99,
KODE_OBJEK_PAJAK.TIDAK_FINAL_99,
].includes(watch('kodeObjekPajak')) &&
![FG_FASILITAS_PPH_21.FASILITAS_LAINNYA].includes(watch('fgFasilitas'))
}
/>
</Grid>
<Grid size={{ md: 5 }}>
<RHFNumeric
name="pph21"
label="PPh Pasal 21"
readOnly={![KODE_OBJEK_PAJAK.FINAL_38].includes(watch('kodeObjekPajak'))}
/>
</Grid>
<Grid size={{ md: 2 }} alignSelf="center">
<LoadingButton
variant="contained"
fullWidth
size="large"
color="primary"
// onClick={handleHitung}
// loading={hitung.isLoading}
startIcon={<CalculateRounded />}
>
Hitung
</LoadingButton>
</Grid>
</Grid>
);
}
export default JumlahPerhitunganForm;
import { ChevronRightRounded, CloseRounded } from '@mui/icons-material';
import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material';
import { m } from 'framer-motion';
import type { FC } from 'react';
import { PANDUAN_REKAM_DN } from '../../constant';
interface PanduanDnRekamProps {
handleOpen: () => void;
isOpen: boolean;
}
const PanduanDnRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => (
<Box position="sticky">
{/* Tombol toggle */}
<Box
height="100%"
display={isOpen ? 'none' : 'flex'}
justifyContent="center"
alignItems="center"
>
<Button
variant="contained"
sx={{
height: 'fit-content',
right: 0,
borderRadius: 0,
minWidth: 35,
pt: 3,
pb: 3,
fontWeight: 'bold',
fontSize: 16,
backgroundColor: '#143B88',
}}
size="small"
onClick={handleOpen}
>
<span
style={{
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
display: 'flex',
alignItems: 'center',
}}
>
Panduan Penggunaan
<ChevronRightRounded sx={{ fontSize: 30 }} />
</span>
</Button>
</Box>
{/* Konten panduan */}
{isOpen && (
<m.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1, transition: { delay: 0.2 } }}
>
<Card>
<CardHeader
avatar={
<img src="/assets/icon_panduan_penggunaan_1.svg" alt="Panduan" loading="lazy" />
}
sx={{
backgroundColor: '#123375',
color: '#FFFFFF',
padding: '16px',
'& .MuiCardHeader-title': {
fontSize: 18,
},
}}
action={
<IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}>
<CloseRounded />
</IconButton>
}
title="Panduan Penggunaan"
/>
<CardContent
sx={{
maxHeight: 300,
overflow: 'auto',
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': { backgroundColor: '#f0f0f0', borderRadius: 8 },
'&::-webkit-scrollbar-thumb': { backgroundColor: '#123375', borderRadius: 8 },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: '#0d2858' },
scrollbarWidth: 'thin',
scrollbarColor: '#123375 #f0f0f0',
}}
>
{/* Deskripsi Form */}
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}>
<span style={{ fontWeight: 600 }}>Deskripsi Form:</span>
<br />
{PANDUAN_REKAM_DN.description.intro}
</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>
<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>
<Box component="ul" sx={{ pl: 2, listStyle: 'disc' }}>
{section.items.map((item, idx) => (
<Box key={idx} component="li" sx={{ mb: 0.5 }}>
<Typography variant="body2" component="span">
{item.text}
</Typography>
{item.subItems.length > 0 && (
<Box component="ol" sx={{ pl: 3, listStyle: 'decimal' }}>
{item.subItems.map((sub, subIdx) => (
<Typography key={subIdx} variant="body2" component="li">
{sub}
</Typography>
))}
</Box>
)}
</Box>
))}
</Box>
</Box>
))}
</CardContent>
</Card>
</m.div>
)}
</Box>
);
export default PanduanDnRekam;
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import { FG_FASILITAS_PPH_21, FG_FASILITAS_PPH_21_TEXT } from '../../constant';
type PPHDipotongProps = {
kodeObjectPajak: {
value: string;
label: string;
}[];
};
const PerhitunganPPhPasal21 = ({ kodeObjectPajak }: PPHDipotongProps) => {
const { watch } = useFormContext();
const fgFasilitas = watch('fgFasilitas');
const fgFasilitasOptions = useMemo(
() =>
[
FG_FASILITAS_PPH_21.DTP,
FG_FASILITAS_PPH_21.FASILITAS_LAINNYA,
FG_FASILITAS_PPH_21.TANPA_FASILITAS,
].map((value) => ({
value,
label: FG_FASILITAS_PPH_21_TEXT[value],
})),
[]
);
return (
<Grid container rowSpacing={2} columnSpacing={2}>
{/* Divider */}
<Grid size={{ md: 12 }}>
<Divider sx={{ fontWeight: 'bold' }} textAlign="left">
Perhitungan PPh Pasal 21
</Divider>
</Grid>
{/* Kode objek pajak */}
<Grid size={{ md: 12 }}>
<Field.Autocomplete name="kdObjPjk" label="Kode Objek Pajak" options={kodeObjectPajak} />
</Grid>
{/* Fasilitas */}
<Grid size={{ md: 6 }}>
<Field.Autocomplete name="fgFasilitas" label="Fasilitas" options={fgFasilitasOptions} />
</Grid>
{/* Dokumen lainnya */}
<Grid size={{ md: 6 }}>
<Field.Text
name="noDokLainnya"
label="Nomor Dokumen Lainnya"
disabled={['9', ''].includes(fgFasilitas)}
sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
/>
</Grid>
</Grid>
);
};
export default PerhitunganPPhPasal21;
This diff is collapsed.
const appRootKey = 'unifikasi';
const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
bulanan: {
all: (params: any) => [appRootKey, 'bulanan', params],
detail: (params: any) => [appRootKey, 'bulanan', 'detail', params],
draft: [appRootKey, 'bulanan', 'draft'],
delete: [appRootKey, 'bulanan', 'delete'],
upload: [appRootKey, 'bulanan', 'upload'],
cancel: [appRootKey, 'bulanan', 'cancel'],
cetakPdf: (params: any) => [appRootKey, 'bulanan-cetak-pdf', params],
},
};
export default queryKey;
// /* eslint-disable consistent-return */
// /* eslint-disable no-useless-return */
// import useAdvancedSearch from '@pjap/shared/hooks/useAdvancedSearch';
// import {
// transformComparisonOperatorToPatternMatchingOperator,
// transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValueToQueryValueBasedOnModelOperator,
// } from '@pjap/shared/data-grid-premium/util';
// import {
// FG_PDF_STATUS,
// FG_SIGN_STATUS,
// FG_STATUS,
// } from '@pjap/pph21/app/bukti-potong/shared/constants/BupotConstant';
// import { addLeadingZero } from '@pjap/shared/utils/number';
// import { transformDateToCommonFormat } from '@pjap/pph21/utils/formatDate';
// const useBpuAdvancedSearch = () => {
// const fieldConfigs = {
// fgStatus: {
// fieldMapping: 'fgStatus',
// operator: transformComparisonOperatorToPatternMatchingOperator,
// transformValue: (value) => {
// switch (value) {
// case FG_STATUS.SUBMITTED:
// return "'%SUBMITTED%'".toLowerCase();
// case FG_STATUS.NORMAL:
// return "'%NORMAL%'".toLowerCase();
// case FG_STATUS.NORMAL_PENGGANTI:
// return "'%AMENDMENT%'".toLowerCase();
// case FG_STATUS.BATAL:
// return "'%CANCELLED%'".toLowerCase();
// case FG_STATUS.DRAFT:
// return "'%DRAFT%'".toLowerCase();
// case FG_STATUS.DIGANTI:
// return "'%AMENDED%'".toLowerCase();
// case FG_STATUS.PENDING:
// return "'%PENDING%'".toLowerCase();
// case FG_STATUS.ON_SCHEDULE:
// return "'%ON SCHEDULE%'".toLowerCase();
// default:
// return `'%${value}%'`.toLowerCase();
// }
// },
// },
// fgSignStatus: {
// fieldMapping: 'fgStatus',
// operator: (currentOperator, _, value) => {
// if (value === FG_SIGN_STATUS.ERROR) return 'IN';
// if (currentOperator === '!=') return 'NOT LIKE';
// return 'LIKE';
// },
// transformValue: (value) => {
// switch (value) {
// case FG_SIGN_STATUS.IN_PROGRESS:
// return "'%SIGNING_IN_PROGRESS%'".toLowerCase();
// case FG_SIGN_STATUS.FAILED:
// return "'%DJP-SIGN-MASTER%'".toLowerCase();
// case FG_SIGN_STATUS.SIGNED:
// return "'%document signed successfully%'".toLowerCase();
// case FG_SIGN_STATUS.NOT_MATCH_IDBUPOT:
// return "'%NOT_MATCH_IDBUPOT%'".toLowerCase();
// case FG_SIGN_STATUS.NOT_MATCH_STATUS:
// return "'%NOT_MATCH_STATUS%'".toLowerCase();
// case FG_SIGN_STATUS.NOT_MATCH_NILAI:
// return "'%NOT_MATCH_NILAI%'".toLowerCase();
// case FG_SIGN_STATUS.DUPLICATE:
// return "'%DUPLICATE%'".toLowerCase();
// case FG_SIGN_STATUS.ERROR:
// return "('draft','normal-done','amendment-done','amended-document signed successfully','cancelled-done','submitted-signing_in_progress')";
// default:
// return `'%${value}%'`.toLowerCase();
// }
// },
// additionalQuery: (originalValue) => {
// switch (originalValue) {
// case FG_SIGN_STATUS.ERROR:
// return `"errorMsg" IS NOT NULL AND "errorMsg" != ''`;
// default:
// return ``;
// }
// },
// },
// fgPdf: {
// fieldMapping: 'link',
// operator: transformComparisonOperatorToPatternMatchingOperator,
// transformValue: (value) => {
// switch (value) {
// case FG_PDF_STATUS.TERBENTUK:
// return "'%https://coretaxdjp.pajak.go.id%'";
// case FG_PDF_STATUS.BELUM_TERBENTUK:
// return `'%intranet.pajak.go.id%' OR ((link = '' OR link IS NULL) AND "fgStatus" IN ('NORMAL-document signed successfully', 'NORMAL-Done', 'AMENDED-document signed successfully', 'CANCELLED-document signed successfully', 'AMENDMENT-document signed successfully'))`;
// default:
// return "'' OR link IS NULL";
// }
// },
// },
// fgKirimEmail: {
// fieldMapping: 'fgkirimemail',
// },
// msPajak: {
// fieldMapping: 'masaPajak',
// transformValue: (value) => `'${addLeadingZero(value)}'`,
// },
// masaPajak: {
// fieldMapping: 'masaPajak',
// transformValue: (value) => `'${addLeadingZero(value)}'`,
// },
// thnPajak: {
// fieldMapping: 'tahunPajak',
// },
// tahunPajak: {
// fieldMapping: 'tahunPajak',
// },
// dpp: {
// fieldMapping: 'dpp',
// },
// noBupot: {
// fieldMapping: 'nomorBupot',
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// },
// npwp: {
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// idTku: {
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// errorMsg: {
// operator: transformComparisonOperatorToPatternMatchingOperator,
// transformValue: (value) => `'%${value}%'`.toLowerCase(),
// },
// jmlBruto: {
// fieldMapping: 'penghasilanBruto',
// },
// kdObjPjk: {
// fieldMapping: 'kodeObjekPajak',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// npwpPemotong: {
// fieldMapping: 'npwpPemotong',
// },
// namaPemotong: {
// fieldMapping: 'namaPemotong',
// },
// namaDipotong: {
// fieldMapping: 'nama',
// },
// pasalPPh: {
// fieldMapping: 'pasalPPh',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// tanggalApproval: {
// transformValue: transformDateToCommonFormat,
// },
// internal_id: {
// fieldMapping: 'internal_id',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// created: {
// fieldMapping: 'created_by',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// updated: {
// fieldMapping: 'updated_by',
// operator: transformOperatorToPatternMatchingOrContainsOperatorByModelOperator,
// transformValue: transformValueToQueryValueBasedOnModelOperator,
// },
// };
// const defaultFieldTypes = {
// created_at: 'date',
// updated_at: 'date',
// };
// return useAdvancedSearch({
// fieldConfigs,
// defaultFieldTypes,
// });
// };
// export default useBpuAdvancedSearch;
import { isEmpty } from 'lodash';
import { FG_PDF_STATUS, FG_SIGN_STATUS } from '../constant';
import { useQuery } from '@tanstack/react-query';
import queryKey from '../constant/queryKey';
import type {
TBaseResponseAPI,
TGetListDataTableDn,
TGetListDataTableDnResult,
} from '../types/types';
import bulananApi from '../utils/api';
export const transformFgStatusToFgSignStatus = (fgStatus: any) => {
console.log('🚀 ~ transformFgStatusToFgSignStatus ~ fgStatus:', fgStatus);
const splittedFgStatus = fgStatus?.split('-') || [];
if (splittedFgStatus.includes('SIGN') > 0) {
// failed
return FG_SIGN_STATUS.FAILED;
}
if (splittedFgStatus.includes('SIGNING IN PROGRESS')) {
return FG_SIGN_STATUS.IN_PROGRESS;
}
if (fgStatus === 'DUPLICATE') {
return FG_SIGN_STATUS.DUPLICATE;
}
if (fgStatus === 'NOT_MATCH_STATUS') {
return FG_SIGN_STATUS.NOT_MATCH_STATUS;
}
if (fgStatus === 'NOT_MATCH_NILAI') {
return FG_SIGN_STATUS.NOT_MATCH_NILAI;
}
if (fgStatus === 'NOT_MATCH_IDBUPOT') {
return FG_SIGN_STATUS.NOT_MATCH_IDBUPOT;
}
switch (splittedFgStatus[1]) {
case 'document signed successfully':
case 'Done':
return FG_SIGN_STATUS.SIGNED;
case 'SIGNING_IN_PROGRESS':
return FG_SIGN_STATUS.IN_PROGRESS;
case 'DUPLICATE':
return FG_SIGN_STATUS.DUPLICATE;
case 'NOT_MATCH_STATUS':
return FG_SIGN_STATUS.NOT_MATCH_STATUS;
case 'NOT_MATCH_IDBUPOT':
return FG_SIGN_STATUS.NOT_MATCH_IDBUPOT;
default:
return null;
}
};
export const getFgStatusPdf = (link: any, fgSignStatus: any) => {
if (isEmpty(link) || [FG_SIGN_STATUS.IN_PROGRESS].includes(fgSignStatus))
return FG_PDF_STATUS.TIDAK_TERSEDIA;
if (!link.includes('https://coretaxdjp.pajak.go.id/')) return FG_PDF_STATUS.BELUM_TERBENTUK;
return FG_PDF_STATUS.TERBENTUK;
};
export const transformSortModelToSortApiPayload = (transformedModel: any) => ({
sortingMode: transformedModel.map((item: any) => item.field).join(','),
sortingMethod: transformedModel.length > 0 ? transformedModel[0].sort : 'desc',
});
const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
...params,
nomorSP2D: params.dokumen_referensi?.[0]?.nomorSP2D || '',
metodePembayaranBendahara: params.dokumen_referensi?.[0]?.metodePembayaranBendahara || '',
dokReferensi: params.dokumen_referensi?.[0]?.dokReferensi || '',
nomorDokumen: params.dokumen_referensi?.[0]?.nomorDokumen || '',
id: params.id,
npwpPemotong: params.npwpPemotong,
idBupot: params.idBupot,
internal_id: params.internal_id,
// fgStatus: FG_STATUS[params.fgStatus],
fgStatus: params.fgStatus,
fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus),
fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)),
fgLapor: params.fgLapor,
revNo: params.revNo,
thnPajak: params.tahunPajak,
msPajak: params.masaPajak,
kdObjPjk: params.kodeObjekPajak,
noBupot: params.noBupot,
idDipotong: params.userId,
glAccount: params.glAccount,
namaDipotong: params.nama,
jmlBruto: params.dpp,
pphDipotong: params.pphDipotong,
created: params.created_by,
fgKirimEmail: params.fgkirimemail,
created_at: params.created_at,
updated: params.updated_by,
updated_at: params.updated_at,
});
const normalisPropsParmasGetDn = (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,
advanced: isEmpty(params.advanced) ? undefined : params.advanced,
...sorting,
};
};
const useGetBulanan = ({ params, ...props }: any) => {
const query = useQuery<TBaseResponseAPI<TGetListDataTableDnResult>>({
queryKey: queryKey.bulanan.all(params),
queryFn: async () => {
const response = await bulananApi.getBulanan({ params: normalisPropsParmasGetDn(params) });
return {
...response,
// data: response.data.map((data) => normalisePropsGetDn(data)),
};
},
initialData: {
data: [],
total: 0,
},
refetchOnWindowFocus: false,
...props,
});
return query;
};
export default useGetBulanan;
import { useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import type { TGetListDataKOPDn } from '../types/types';
const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
const { watch, setValue, control } = useFormContext();
// ambil value dari form
const fgFasilitas = watch('fgFasilitas');
const fgIdDipotong = watch('fgIdDipotong');
// mapping statusPPh ke isFinal
const isFinal = kodeObjekPajakSelected?.statuspph?.toLowerCase() === 'final' ? 1 : 0;
const updateTarifValues = () => {
if (kodeObjekPajakSelected) {
let valueTarif = Number(kodeObjekPajakSelected.tarif) || 0;
if (fgFasilitas === '6') {
valueTarif = 0.5;
} else if (fgFasilitas === '8') {
valueTarif = 0;
}
setValue('tarif', valueTarif, { shouldValidate: true });
setValue('tarifLt', fgIdDipotong === '1' && isFinal === 0 ? '100' : '0', {
shouldValidate: true,
});
}
};
// watch field yang mempengaruhi perhitungan
const handlerSetPphDipotong = useWatch({
control,
name: ['thnPajak', 'fgFasilitas', 'fgIdDipotong', 'jmlBruto', 'tarif'],
});
const calculateAndSetPphDipotong = (
thnPajak: number,
fgFasilitas: string,
fgIdDipotong: string,
jmlBruto: number,
tarif: number
) => {
if (kodeObjekPajakSelected) {
const valTarif = thnPajak < 2024 && fgIdDipotong === '1' && isFinal === 0 ? tarif * 2 : tarif;
const valPphDipotong =
fgFasilitas === '8' // contoh: fasilitas tertentu PPh 0
? 0
: (jmlBruto * valTarif) / 100;
setValue('pphDipotong', Math.round(valPphDipotong || 0), {
shouldValidate: true,
});
}
};
useEffect(() => {
if (handlerSetPphDipotong.filter((item) => !item).length < 2) {
calculateAndSetPphDipotong(
Number(handlerSetPphDipotong[0]),
handlerSetPphDipotong[1] as string,
handlerSetPphDipotong[2] as string,
Number(handlerSetPphDipotong[3]),
Number(handlerSetPphDipotong[4])
);
}
}, [handlerSetPphDipotong]);
return {
updateTarifValues,
};
};
export default usePphDipotong;
import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import dnApi from '../utils/api';
import type { TPostDnRequest } from '../types/types';
const transformParams = ({ isPengganti = false, ...dnData }: any): TPostDnRequest => {
const {
id,
idBupot,
noBupot,
msPajak,
thnPajak,
idDipotong,
nitku,
namaDipotong,
fgFasilitas,
noDokLainnya,
kdObjPjk,
kdJnsPjk,
statusPph,
jmlBruto,
tarif,
pphDipotong,
kap,
kjs,
revNo: initialRevNo,
tglPemotongan,
namaDok,
nomorDok,
tglDok,
metodePembayaranBendahara,
nomorSP2D,
idTku,
email,
glAccount,
keterangan1,
keterangan2,
keterangan3,
keterangan4,
keterangan5,
} = dnData;
const dokReferensi = [
{
dokReferensi: namaDok || '',
nomorDokumen: nomorDok || '',
tanggal_Dokumen: tglDok ? dayjs(tglDok).format('DDMMYYYY') : '',
metodePembayaranBendahara: metodePembayaranBendahara || '',
nomorSP2D: nomorSP2D || '',
},
];
const revNo = isPengganti ? parseInt(initialRevNo || 0, 10) + 1 : parseInt(initialRevNo || 0, 10);
const npwpLog = localStorage.getItem('npwp_log') ?? '';
return {
id: !isPengganti ? (id ?? null) : null,
idBupot: idBupot ?? '',
noBupot: noBupot ?? '',
npwpPemotong: npwpLog,
idTku: idTku ?? '',
masaPajak: msPajak ? dayjs(msPajak).format('MM') : '',
tahunPajak: thnPajak ? Number(dayjs(thnPajak).format('YYYY')) : 0,
npwp: idDipotong ?? '',
nik: nitku ?? (idDipotong ? `${idDipotong}000000` : ''),
nama: namaDipotong ?? '',
revNo,
fgNpwpNik: 'true', // static
fgJnsBupot: 'BPU', // static
dataDetilBpu: {
sertifikatInsentifDipotong: fgFasilitas ?? '9',
nomorSertifikatInsentif: noDokLainnya ?? '',
kodeObjekPajak: kdObjPjk ?? '',
pasalPPh: kdJnsPjk ?? '',
statusPPh: statusPph ?? '',
dpp: jmlBruto ?? '',
tarif: tarif ?? '',
pphDipotong: pphDipotong ?? '',
kap: kap ?? '',
kjs: kjs ?? '',
dokReferensi,
},
tglPemotongan: tglPemotongan ? dayjs(tglPemotongan).format('DDMMYYYY') : '',
email: email ?? '',
glAccount: glAccount ?? '',
keterangan1: keterangan1 ?? '',
keterangan2: keterangan2 ?? '',
keterangan3: keterangan3 ?? '',
keterangan4: keterangan4 ?? '',
keterangan5: keterangan5 ?? '',
};
};
const useSaveBulanan = (props?: any) =>
useMutation({
mutationKey: ['Save-Dn'],
mutationFn: (params: any) => dnApi.saveDn(transformParams(params)),
...props,
});
export default useSaveBulanan;
export type TBaseResponseAPI<T> = {
status: string;
message: string;
data: T;
time: string;
code: number;
metaPage: TBaseResponseMetaPage;
total?: number;
};
type TResponseData = {};
type TBaseResponseMetaPage = {
pageNum: number | null;
rowPerPage: number | null;
totalRow: number;
};
export type TGetListDataTableDn = {
id: number;
npwpPemotong: string;
idTku: string;
masaPajak: string;
tahunPajak: string;
fgNpwpNik: string;
npwp: string;
nik: string;
nama: string;
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
kodeObjekPajak: string;
pasalPPh: string;
statusPPh: string;
dpp: string;
tarif: string;
pphDipotong: string;
kap: string;
kjs: string;
tglpemotongan: string;
userId: string;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
fgStatus: string;
internal_id: string;
dokumen_referensi: {
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string;
metodePembayaranBendahara: string;
nomorSP2D: string;
}[];
revNo: number;
noBupot: string | null;
idBupot: string | null;
npwpNikPenandatangan: string;
namaPenandatangan: string;
link: string | null;
errorMsg: string | null;
email: string | null;
glAccount: string;
fgkirimemail: string;
glName: string | null;
keterangan1: string | null;
keterangan2: string | null;
keterangan3: string | null;
keterangan4: string | null;
keterangan5: string | null;
fgLapor: string;
};
export type TGetListDataTableDnResult = TGetListDataTableDn[];
export type TGetListDataKOPDn = {
dtp: number;
kap: string;
kjs: string;
kode: string;
nama: string;
noCertificate: number;
normanetto: string;
otherCert: number;
pasal: string;
skbBungaTabungan: number;
skbPHTB: number;
skbPasal22: number;
skbPasal23: number;
statuspph: string;
suket: number;
tarif: string;
};
export type TGetListDataKOPDnResult = TGetListDataKOPDn[];
export type ActionItem = {
title: string;
icon: React.ReactNode;
func?: () => void;
disabled?: boolean;
};
export type TPostDnRequest = {
id: string | null;
idBupot: string;
noBupot: string;
npwpPemotong: string;
idTku: string;
masaPajak: string;
tahunPajak: number;
npwp: string;
nik: string;
nama: string;
revNo: number;
fgNpwpNik: string; // static "true"
fgJnsBupot: string; // static "BPU"
dataDetilBpu: {
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
kodeObjekPajak: string;
pasalPPh: string;
statusPPh: string;
dpp: string;
tarif: string;
pphDipotong: string;
kap: string;
kjs: string;
dokReferensi: {
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string;
metodePembayaranBendahara: string;
nomorSP2D: string;
}[];
};
tglPemotongan: string;
email: string;
glAccount: string;
keterangan1: string;
keterangan2: string;
keterangan3: string;
keterangan4: string;
keterangan5: string;
};
import type {
TBaseResponseAPI,
TGetListDataKOPDnResult,
TGetListDataTableDnResult,
TPostDnRequest,
} from '../types/types';
import bulananClient from './bulananClient';
const bulananApi = () => {};
// API untuk get list table
bulananApi.getBulanan = async (config: any) => {
const {
data: { message, metaPage, data },
status: statusCode,
} = await bulananClient.get<TBaseResponseAPI<TGetListDataTableDnResult>>('IF_TXR_028/a0', {
...config,
});
if (statusCode !== 200) {
throw new Error(message);
}
return { total: metaPage ? Number(metaPage.totalRow) : 0, data };
};
// ✅ adjust biar bisa terima params
bulananApi.getKodeObjekPajakBulanan = async (params?: Record<string, any>) => {
const response = await bulananClient.get<TBaseResponseAPI<TGetListDataKOPDnResult>>(
'/sandbox/mst_kop_bpu',
{ params } // ⬅️ inject ke query string
);
const body = response.data;
if (response.status !== 200 || body.status !== 'success') {
throw new Error(body.message);
}
return body;
};
bulananApi.saveBulanan = async (config: TPostDnRequest) => {
const {
data: { message, data },
status: statusCode,
} = await bulananClient.post<TBaseResponseAPI<TPostDnRequest>>('/IF_TXR_028/a0', {
...config,
});
if (statusCode !== 200) {
throw new Error(message);
}
return data;
};
export default bulananApi;
import axios from 'axios';
import { CONFIG } from 'src/global-config';
const BASE_URL = CONFIG.nodesandbox.api;
const unifikasiClient = axios.create({
baseURL: BASE_URL,
validateStatus(status) {
return (status >= 200 && status < 300) || status === 500;
},
});
// Interceptor untuk selalu update token dari localStorage
unifikasiClient.interceptors.request.use((config) => {
const jwtAccessToken = localStorage.getItem('jwt_access_token');
const xToken = localStorage.getItem('x-token');
if (jwtAccessToken) {
config.headers.Authorization = `Bearer ${jwtAccessToken}`;
}
if (xToken) {
config.headers['x-token'] = xToken;
}
return config;
});
export default unifikasiClient;
import dayjs from 'dayjs';
import { ActionRekam, MIN_THN_PAJAK } from '../constant';
export const currentYear = dayjs().year();
export const getHighestStartingYear = (thnAwalUnifikasi: any) =>
Math.max(MIN_THN_PAJAK, thnAwalUnifikasi);
export const selectedInitialMonth = ({ thnAwalUnifikasi, masaAwalUnifikasi }: any) => {
const highestYear = getHighestStartingYear(thnAwalUnifikasi);
return highestYear > thnAwalUnifikasi ? '01' : masaAwalUnifikasi;
};
export const determineStartingMonth = ({ thnPajak, thnAwalUnifikasi, masaAwalUnifikasi }: any) => {
const highestYear = getHighestStartingYear(thnAwalUnifikasi);
const initialMonth = selectedInitialMonth({ thnAwalUnifikasi, masaAwalUnifikasi });
return thnPajak >= highestYear && thnPajak <= currentYear ? initialMonth : '';
};
export const checkCurrentPage = (pathname: string) => {
if (pathname.includes('rekam')) return ActionRekam.REKAM;
if (pathname.includes('pengganti')) return ActionRekam.PENGGANTI;
if (pathname.includes('pembetulan')) return ActionRekam.PEMBETULAN;
if (pathname.includes('ubah')) return ActionRekam.UBAH;
return ActionRekam.REKAM;
};
import Button from '@mui/material/Button';
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 TableHeaderLabel from 'src/shared/components/TableHeaderLabel';
import useGetBulanan from '../hooks/useGetBulanan';
// import CustomToolbarDn from '../components/customToolbarDn';
// import CustomToolbar, { CustomFilterButton } from '../components/customToolbarDn2';
export type IColumnGrid = GridColDef & {
field:
| 'fgStatus'
| 'noBupot'
| 'masaPajak'
| 'tahunPajak'
| 'kdObjPjk'
| 'pasalPPh'
| 'npwp'
| 'nama'
| 'dpp'
| 'pphDipotong'
| 'idTku'
| 'dokReferensi'
| 'nomorDokumen'
| 'created_by'
| 'created_at'
| 'updated_by'
| 'updated_at'
| 'internal_id'
| 'keterangan1'
| 'keterangan2'
| 'keterangan3'
| 'keterangan4'
| 'keterangan5';
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]);
console.log(data, '123');
// const handleChange = (event: React.SyntheticEvent, newValue: number) => {
// setTabs1(newValue);
// };
// const handleChange2 = (event: React.SyntheticEvent, newValue: number) => {
// setTabs2(newValue);
// };
// type aman
type Status = 'draft' | 'normal' | 'cancelled' | 'amendment';
type StatusOption = {
value: Status;
label: string;
};
const statusOptions: StatusOption[] = [
{ value: 'draft', label: 'Draft' },
{ value: 'normal', label: 'Normal' },
{ value: 'cancelled', label: 'Dibatalkan' },
{ value: 'amendment', label: 'Normal Pengganti' },
];
const columns: IColumnGrid[] = [
{
field: 'fgStatus',
headerName: 'Status',
width: 300,
type: 'singleSelect',
valueOptions: statusOptions.map((opt) => opt.value), // filter dropdown pakai value
valueFormatter: (params: any) => {
const option = statusOptions.find((opt) => opt.value === params.value);
return option ? option.label : (params.value as string);
},
},
{ field: 'noBupot', headerName: 'Nomor Bukti Pemotongan', width: 200 },
{ field: 'masaPajak', headerName: 'Masa Pajak', width: 150 },
{ field: 'tahunPajak', headerName: 'Tahun Pajak', width: 150 },
{ field: 'kdObjPjk', headerName: 'Kode Objek Pajak', width: 150 },
{ field: 'npwp', headerName: 'Identitas', width: 150 },
{ field: 'nama', headerName: 'Nama', width: 150 },
{ field: 'dpp', headerName: 'Jumlah Penghasilan Bruto (Rp)', width: 150 },
{ field: 'pphDipotong', headerName: 'Jumlah PPh Terutang (Rp)', width: 200 },
{ field: 'idTku', headerName: 'NITKU Pemotong', width: 150 },
{ field: 'dokReferensi', headerName: 'Nama dokumen', width: 150 },
{ field: 'nomorDokumen', headerName: 'Nomor dokumen', width: 150 },
{ field: 'created_by', headerName: 'Created', width: 150 },
{ field: 'created_at', headerName: 'Created At', width: 200 },
{ field: 'updated_by', headerName: 'Updated', width: 150 },
{ field: 'updated_at', headerName: 'Update At', width: 150 },
{ field: 'internal_id', headerName: 'Referensi', width: 150 },
{ field: 'keterangan1', headerName: 'Keterangan 1', width: 150 },
{ field: 'keterangan2', headerName: 'Keterangan 2', width: 200 },
{ field: 'keterangan3', headerName: 'Keterangan 3', width: 150 },
{ field: 'keterangan4', headerName: 'Keterangan 4', width: 150 },
{ field: 'keterangan5', headerName: 'Keterangan 5', width: 150 },
];
return (
<DashboardContent>
<CustomBreadcrumbs
heading="Bupot Bulanan"
links={[
{ name: 'Dashboard', href: paths.dashboard.root },
{ name: 'e-Bupot PPh Pasal 21 Bulanan' },
]}
action={
<Button component={RouterLink} href={paths.pph21.bulananRekam} variant="contained">
Rekam Data
</Button>
}
/>
<TableHeaderLabel label="Daftar Bupot Bulanan" />
<DataGridPremium
sx={{
border: 1,
borderColor: 'divider',
borderRadius: 2,
'& .MuiDataGrid-cell': {
borderColor: 'divider',
},
'& .MuiDataGrid-columnHeaders': {
borderColor: 'divider',
},
}}
checkboxSelection
rows={rows}
columns={columns}
loading={isLoading}
rowCount={totalRows}
initialState={{
pagination: { paginationModel: { pageSize: 10, page: 0 } },
}}
pagination
pageSizeOptions={[5, 10, 15, 25, 50, 100, 250, 500, 750, 100]}
paginationMode="server"
onPaginationModelChange={setPaginationModel}
filterMode="server"
onFilterModelChange={setFilterModel}
sortingMode="server"
onSortModelChange={setSortModel}
pinnedColumns={{
left: ['__check__', 'fgStatus', 'noBupot'],
}}
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 }))}
// />
// ),
// }}
/>
</DashboardContent>
);
}
import { zodResolver } from '@hookform/resolvers/zod';
import { LoadingButton } from '@mui/lab';
import Grid from '@mui/material/Grid';
import { Suspense, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { DashboardContent } from 'src/layouts/dashboard';
import { paths } from 'src/routes/paths';
import HeadingRekam from 'src/shared/components/HeadingRekam';
import z from 'zod';
import Divider from '@mui/material/Divider';
import FormSkeleton from 'src/shared/skeletons/FormSkeleton';
import Agreement from 'src/shared/components/agreement/Agreement';
import Stack from '@mui/material/Stack';
import PanduanDnRekam from '../components/rekam/PanduanDnRekam';
import { useParams, usePathname } from 'src/routes/hooks';
import Identitas from '../components/rekam/Identitas';
import { checkCurrentPage } from '../utils/utils';
import PerhitunganPPhPasal21 from '../components/rekam/PerhitunganPPhPasal21';
import { KODE_OBJEK_PAJAK, KODE_OBJEK_PAJAK_TEXT } from '../constant';
import JumlahPerhitunganForm from '../components/rekam/JumlahPerhitunganForm';
import { Field } from 'src/components/hook-form';
const bulananSchema = z.object({
tglPemotongan: z.string().nonempty('Tanggal Pemotongan wajib diisi'),
thnPajak: z.string().nonempty('Tahun Pajak wajib diisi'),
msPajak: z.string().nonempty('Masa Pajak wajib diisi'),
nitku: z
.string()
.nonempty('NITKU wajib diisi')
.regex(/^\d{22}$/, 'NITKU harus 22 digit'),
namaDipotong: z.string().nonempty('Nama wajib diisi'),
idDipotong: z
.string()
.nonempty('NPWP wajib diisi')
.regex(/^\d{16}$/, 'NPWP harus 16 digit'),
email: z.string().optional(),
noDokLainnya: z.string().nonempty('No Dokumen Lainnya wajib diisi'),
// bisa tambah field lain sesuai kebutuhan
keterangan1: z.string().optional(),
keterangan2: z.string().optional(),
keterangan3: z.string().optional(),
keterangan4: z.string().optional(),
keterangan5: z.string().optional(),
});
export const BulananRekamView = () => {
const { id } = useParams();
const pathname = usePathname();
const [isOpenPanduan, setIsOpenPanduan] = useState<boolean>(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const actionRekam = checkCurrentPage(pathname);
const dataListKOP = useMemo(
() =>
[KODE_OBJEK_PAJAK.BULANAN_01, KODE_OBJEK_PAJAK.BULANAN_02, KODE_OBJEK_PAJAK.BULANAN_03].map(
(value) => ({
value,
label: `${value} : ${KODE_OBJEK_PAJAK_TEXT[value]}`,
})
),
[]
);
type BpuFormData = z.infer<typeof bulananSchema>;
const handleOpenPanduan = () => setIsOpenPanduan(!isOpenPanduan);
const defaultValues = {
tglPemotongan: '',
thnPajak: '',
msPajak: '',
idDipotong: '',
nitku: '',
namaDipotong: '',
email: '',
keterangan1: '',
keterangan2: '',
keterangan3: '',
keterangan4: '',
keterangan5: '',
kdObjPjk: '',
fgFasilitas: '',
noDokLainnya: '',
jmlBruto: 0,
tarif: '',
PerhitunganPPhPasal21: '',
namaDok: '',
nomorDok: '',
tglDok: '',
idTku: '',
};
const methods = useForm<BpuFormData>({
mode: 'all',
resolver: zodResolver(bulananSchema),
defaultValues,
});
const SubmitRekam = () => {
console.log('Submit API');
};
const MockNitku = [
{
nama: '1091031210912281000000',
},
{
nama: '1091031210912281000001',
},
];
return (
<DashboardContent>
<CustomBreadcrumbs
heading="Add Bupot PPh Pasal 21 Bulanan"
links={[
{ name: 'Dashboard', href: paths.dashboard.root },
{
name: 'e-Bupot PPh Pasal 21 Bulanan',
href: paths.pph21.bulanan,
},
{ name: 'Add Bupot PPh Pasal 21 Bulanan' },
]}
/>
<HeadingRekam label="rekam Bupot PPh Pasal 21 Bulanan" />
<Grid container columnSpacing={2} /* container otomatis */>
<Grid size={{ xs: isOpenPanduan ? 8 : 11 }}>
<form onSubmit={methods.handleSubmit(SubmitRekam)}>
<FormProvider {...methods}>
<Suspense fallback={<FormSkeleton />}>
<Identitas />
<Suspense fallback={<FormSkeleton />}>
<PerhitunganPPhPasal21 kodeObjectPajak={dataListKOP} />
</Suspense>
<JumlahPerhitunganForm />
<Grid size={{ md: 12 }}>
<Field.Autocomplete
name="idTku"
label="NITKU Pemotong"
options={MockNitku.map((a) => ({ value: a, label: a }))}
/>
</Grid>
<Divider />
<Grid size={12}>
<Agreement
isCheckedAgreement={isCheckedAgreement}
setIsCheckedAgreement={setIsCheckedAgreement}
text="Dengan ini saya menyatakan bahwa Bukti Pemotongan/Pemungutan Unifikasi telah
saya isi dengan benar secara elektronik sesuai
dengan"
/>
</Grid>
<Stack direction="row" gap={2} justifyContent="end" marginTop={2}>
<LoadingButton
type="submit"
// loading={saveDn.isLoading}
disabled={!isCheckedAgreement}
variant="outlined"
sx={{ color: '#143B88' }}
>
Save as Draft
</LoadingButton>
<LoadingButton
type="button"
disabled={!isCheckedAgreement}
// onClick={handleClickUploadSsp}
// loading={uploadDn.isLoading}
variant="contained"
sx={{ background: '#143B88' }}
>
Save and Upload
</LoadingButton>
</Stack>
</Suspense>
</FormProvider>
</form>
</Grid>
<Grid size={{ xs: isOpenPanduan ? 4 : 1 }}>
<PanduanDnRekam handleOpen={handleOpenPanduan} isOpen={isOpenPanduan} />
</Grid>
</Grid>
</DashboardContent>
);
};
export * from './bulanan-list-view';
export * from './bulanan-rekam-view';
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