Commit d89056f3 authored by Fachri's avatar Fachri

bpu non residen

parent 9ffe9ff5
...@@ -6,6 +6,10 @@ import { LicenseInfo } from '@mui/x-license'; ...@@ -6,6 +6,10 @@ import { LicenseInfo } from '@mui/x-license';
import App from './app'; import App from './app';
import { routesSection } from './routes/sections'; import { routesSection } from './routes/sections';
import { ErrorBoundary } from './routes/components'; import { ErrorBoundary } from './routes/components';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(customParseFormat);
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
LicenseInfo.setLicenseKey( LicenseInfo.setLicenseKey(
......
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { DashboardView } from 'src/sections/dashboard/view'; import { DashboardView } from 'src/sections/dashboard/view';
// import { OverviewAppView } from 'src/sections/overview/app/view';
// ----------------------------------------------------------------------
const metadata = { title: `Dashboard - ${CONFIG.appName}` }; const metadata = { title: `Dashboard - ${CONFIG.appName}` };
export default function OverviewAppPage() { export default function OverviewAppPage() {
return ( return (
<> <>
<title>{metadata.title}</title> <title>{metadata.title}</title>
{/* <OverviewAppView /> */}
{/* aaa */}
<DashboardView /> <DashboardView />
</> </>
); );
......
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { NrListView } from 'src/sections/bupot-unifikasi/bupot-nr/view/nr-list-view';
// import { NrListView } from 'src/sections/bupot-unifikasi/bupot-nr/view'; import { NrListView } from 'src/sections/bupot-unifikasi/bupot-nr/view';
const metadata = { title: `E-Bupot Unifikasi- ${CONFIG.appName}` }; const metadata = { title: `E-Bupot Unifikasi- ${CONFIG.appName}` };
......
...@@ -143,30 +143,5 @@ export const paths = { ...@@ -143,30 +143,5 @@ export const paths = {
edit: (id: string) => `${ROOTS.DASHBOARD}/user/${id}/edit`, edit: (id: string) => `${ROOTS.DASHBOARD}/user/${id}/edit`,
demo: { edit: `${ROOTS.DASHBOARD}/user/${MOCK_ID}/edit` }, demo: { edit: `${ROOTS.DASHBOARD}/user/${MOCK_ID}/edit` },
}, },
product: {
root: `${ROOTS.DASHBOARD}/product`,
new: `${ROOTS.DASHBOARD}/product/new`,
details: (id: string) => `${ROOTS.DASHBOARD}/product/${id}`,
edit: (id: string) => `${ROOTS.DASHBOARD}/product/${id}/edit`,
demo: {
details: `${ROOTS.DASHBOARD}/product/${MOCK_ID}`,
edit: `${ROOTS.DASHBOARD}/product/${MOCK_ID}/edit`,
},
},
invoice: {
root: `${ROOTS.DASHBOARD}/invoice`,
new: `${ROOTS.DASHBOARD}/invoice/new`,
details: (id: string) => `${ROOTS.DASHBOARD}/invoice/${id}`,
edit: (id: string) => `${ROOTS.DASHBOARD}/invoice/${id}/edit`,
demo: {
details: `${ROOTS.DASHBOARD}/invoice/${MOCK_ID}`,
edit: `${ROOTS.DASHBOARD}/invoice/${MOCK_ID}/edit`,
},
},
order: {
root: `${ROOTS.DASHBOARD}/order`,
details: (id: string) => `${ROOTS.DASHBOARD}/order/${id}`,
demo: { details: `${ROOTS.DASHBOARD}/order/${MOCK_ID}` },
},
}, },
}; };
...@@ -122,6 +122,7 @@ export const dashboardRoutes: RouteObject[] = [ ...@@ -122,6 +122,7 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'dn/:id/:type', element: <OverviewUnifikasiRekamDnPage /> }, { path: 'dn/:id/:type', element: <OverviewUnifikasiRekamDnPage /> },
{ path: 'nr', element: <OverviewUnifikasiNrPage /> }, { path: 'nr', element: <OverviewUnifikasiNrPage /> },
{ path: 'nr/new', element: <OverviewUnifikasiRekamNrPage /> }, { path: 'nr/new', element: <OverviewUnifikasiRekamNrPage /> },
{ path: 'nr/:id/:type', element: <OverviewUnifikasiRekamNrPage /> },
{ path: 'ssp', element: <OverviewUnifikasiSspPage /> }, { path: 'ssp', element: <OverviewUnifikasiSspPage /> },
{ path: 'ssp/new', element: <OverviewUnifikasiRekamSspPage /> }, { path: 'ssp/new', element: <OverviewUnifikasiRekamSspPage /> },
{ path: 'digunggung', element: <OverviewUnifikasiDigunggungPage /> }, { path: 'digunggung', element: <OverviewUnifikasiDigunggungPage /> },
......
// import Box from '@mui/material/Box';
// import Button from '@mui/material/Button';
// import Grid from '@mui/material/Grid';
// import dayjs from 'dayjs';
// import React, { useEffect, useState } from 'react';
// import { useFormContext } from 'react-hook-form';
// import { Field } from 'src/components/hook-form';
// type IdentitasProps = {
// isPengganti: boolean;
// existingDn?: any; // data penuh dari API
// };
// const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => {
// const { setValue, watch, getValues } = useFormContext();
// const tanggalPemotongan = watch('tglPemotongan');
// const maxKeterangan = 5;
// const [jumlahKeterangan, setJumlahKeterangan] = useState<number>(0);
// // 🧩 Auto isi tahun & masa pajak 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]);
// useEffect(() => {
// // ambil nilai form saat ini (setelah reset di parent)
// const currentValues = getValues();
// const arr = [
// currentValues.keterangan1,
// currentValues.keterangan2,
// currentValues.keterangan3,
// currentValues.keterangan4,
// currentValues.keterangan5,
// ];
// const count = arr.filter((k) => !!k && k.trim() !== '').length;
// console.log('🧠 Detected keterangan count:', count, arr);
// // kalau ada field terisi, render sebanyak itu
// if (count > 0) {
// setJumlahKeterangan(count);
// }
// }, [existingDn, getValues]);
// // ➕ Tambah field
// const handleTambah = () => {
// if (jumlahKeterangan < maxKeterangan) {
// setJumlahKeterangan((prev) => prev + 1);
// }
// };
// // ➖ Hapus field terakhir
// const handleHapus = () => {
// if (jumlahKeterangan > 0) {
// setValue(`keterangan${jumlahKeterangan}`, '');
// setJumlahKeterangan((prev) => prev - 1);
// }
// };
// console.log(existingDn);
// return (
// <>
// <Grid container rowSpacing={2} alignItems="center" columnSpacing={2} sx={{ mb: 4 }}>
// {/* 📅 Tanggal & Masa Pajak */}
// <Grid size={{ md: 6 }}>
// <Field.DatePicker
// name="tglPemotongan"
// label="Tanggal Pemotongan"
// format="DD/MM/YYYY"
// maxDate={dayjs()}
// />
// </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 dan NITKU */}
// <Grid size={{ md: 6 }}>
// <Field.Text
// name="idDipotong"
// label="NPWP"
// onChange={(e) => {
// const value = e.target.value.replace(/\D/g, '').slice(0, 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="nitku"
// label="NITKU"
// onChange={(e) => {
// const value = e.target.value.replace(/\D/g, '').slice(0, 22);
// setValue('nitku', value, { shouldValidate: true, shouldDirty: true });
// }}
// />
// </Grid>
// {/* 👤 Nama dan Email */}
// <Grid size={{ md: 6 }}>
// <Field.Text name="namaDipotong" label="Nama" />
// </Grid>
// <Grid size={{ md: 6 }}>
// <Field.Text name="email" label="Email (optional)" />
// </Grid>
// </Grid>
// {/* ✏️ Tombol 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>
// {/* 🗒️ Input Keterangan Tambahan */}
// <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 Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
...@@ -360,7 +188,7 @@ const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => { ...@@ -360,7 +188,7 @@ const Identitas = ({ isPengganti, existingDn }: IdentitasProps) => {
{/* 🗒️ Input Keterangan Tambahan */} {/* 🗒️ Input Keterangan Tambahan */}
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
{Array.from({ length: jumlahKeterangan }).map((_, i) => ( {Array.from({ length: jumlahKeterangan }).map((_, i) => (
<Grid size={{ md: 12 }} key={i}> <Grid size={{ md: 12 }} key={`keterangan${i + 1}`}>
<Field.Text <Field.Text
sx={{ mb: 2 }} sx={{ mb: 2 }}
name={`keterangan${i + 1}`} name={`keterangan${i + 1}`}
......
// type FilterItem = {
// field: string;
// operator: string;
// value?: string | number | Array<string | number> | null;
// join?: 'AND' | 'OR'; // optional: join connector BEFORE this item (first item usually undefined)
// };
// type BaseParams = Record<string, any>;
// export function useAdvancedFilter() {
// const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
// const dateFields = new Set(['created_at', 'updated_at']);
// const fieldMap: Record<string, string> = {
// noBupot: 'nomorBupot',
// };
// const dbField = (field: string) => fieldMap[field] ?? field;
// const escape = (v: string) => String(v).replace(/'/g, "''");
// const toDbDate = (value: string | Date) => {
// if (value instanceof Date) {
// const y = value.getFullYear();
// const m = String(value.getMonth() + 1).padStart(2, '0');
// const d = String(value.getDate()).padStart(2, '0');
// return `${y}${m}${d}`;
// }
// const digits = String(value).replace(/[^0-9]/g, '');
// if (digits.length >= 8) return digits.slice(0, 8);
// return digits;
// };
// const normalizeOp = (op: string) => op?.toString().trim();
// function buildAdvancedFilter(filters?: FilterItem[] | null) {
// if (!filters || filters.length === 0) return '';
// const exprs: string[] = []; // each item's expression
// const joins: ('AND' | 'OR')[] = []; // join before each expr (for item 0, push nothing/AND by default)
// for (let i = 0; i < filters.length; i++) {
// const f = filters[i];
// if (!f || !f.field) continue;
// const op = normalizeOp(f.operator ?? '');
// const fieldName = dbField(f.field);
// // build expression for this item
// let expr: string | null = null;
// // DATE handling
// if (dateFields.has(fieldName)) {
// const rawVal = f.value;
// if (!rawVal && !/is empty|is not empty/i.test(op)) {
// continue;
// }
// if (/^is$/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd} 00:00:00' AND \"${fieldName}\" <= '${ymd} 23:59:59'`;
// } else if (/is on or after/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd}'`;
// } else if (/is on or before/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" <= '${ymd}'`;
// }
// }
// // EMPTY checks (user requested LOWER("col") IS NULL semantics)
// if (/is empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NULL`;
// } else if (/is not empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NOT NULL`;
// }
// // IS ANY OF handling
// if (!expr && /is any of/i.test(op)) {
// // collect values array
// let values: Array<string | number> = [];
// if (Array.isArray(f.value)) values = f.value as any;
// else if (typeof f.value === 'string')
// values = (f.value as string)
// .split(',')
// .map((s) => s.trim())
// .filter(Boolean);
// else if (f.value != null) values = [f.value as any];
// if ((values || []).length === 0) {
// expr = null;
// } else {
// // special-case fgStatus: need LIKE %val% OR LIKE %val2%
// if (fieldName === 'fgStatus' || fieldName === 'fg_status') {
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") LIKE LOWER('%${s}%')`;
// });
// expr = `(${ors.join(' OR ')})`;
// } else {
// // default: OR of equality (case-insensitive)
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") = '${s}'`;
// });
// expr = `(${ors.join(' OR ')})`;
// }
// }
// }
// // FGSTATUS special single-value is / is not / contains semantics
// if (!expr && (fieldName === 'fgStatus' || fieldName === 'fg_status')) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '' && !/is any of|is empty|is not empty/i.test(op)) {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// if (/^is$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/is not/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") NOT LIKE LOWER('%${valEscaped}%')`;
// }
// }
// }
// // GENERIC text/numeric handling when expr still not set
// if (!expr) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '') {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// // numeric fields: operators (=, >=, <=)
// if (numericFields.has(fieldName) && /^(=|>=|<=)$/.test(op)) {
// expr = `\"${fieldName}\" ${op} '${valEscaped}'`;
// } else if (/^contains$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/^equals$/i.test(op)) {
// // equals should produce IN (wrap single value as IN)
// // attempt to parse CSV if provided
// let values: string[] = [];
// if (Array.isArray(f.value))
// values = (f.value as any[]).map((v) => escape(String(v).toLowerCase()));
// else values = [escape(String(f.value).toLowerCase())];
// expr = `LOWER(\"${fieldName}\") IN (${values.map((v) => `'${v}'`).join(',')})`;
// } else if (/^(>=|<=|=)$/.test(op) && !numericFields.has(fieldName)) {
// expr = `LOWER(\"${fieldName}\") ${op} '${valEscaped}'`;
// } else if (/^(is)$/i.test(op)) {
// // fallback: treat as equals
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// } else {
// // fallback equality
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// }
// }
// }
// if (expr) {
// exprs.push(expr);
// // record join for this item (use provided join or default AND except for first item)
// const joinBefore = (f.join as 'AND' | 'OR') ?? (exprs.length > 1 ? 'AND' : 'AND');
// joins.push(joinBefore);
// }
// }
// // now combine exprs with joins; joins[i] is join BEFORE exprs[i]
// if (exprs.length === 0) return '';
// let out = exprs[0];
// for (let i = 1; i < exprs.length; i++) {
// const j = joins[i] ?? 'AND';
// out = `(${out}) ${j} (${exprs[i]})`;
// }
// return out;
// }
// function buildRequestParams(base: BaseParams = {}, advanced: string) {
// const out: BaseParams = { ...(base ?? {}) };
// if ('noBupot' in out) {
// out.nomorBupot = out.noBupot;
// delete out.noBupot;
// }
// out.advanced = advanced || '';
// return out;
// }
// return { buildAdvancedFilter, buildRequestParams } as const;
// }
// export default useAdvancedFilter;
type FilterItem = { type FilterItem = {
field: string; field: string;
operator: string; operator: string;
......
...@@ -92,24 +92,6 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({ ...@@ -92,24 +92,6 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
updated_at: formatDateToDDMMYYYY(params.updated_at), updated_at: formatDateToDDMMYYYY(params.updated_at),
}); });
// ---------- normalizer for params request ----------
// const normalisPropsParmasGetDn = (params: any) => {
// const sorting = !isEmpty(params.sortModel)
// ? transformSortModelToSortApiPayload(params.sortModel)
// : {};
// return {
// ...params,
// page: (typeof params.page === 'number' ? params.page : 0) + 1,
// limit: params.pageSize,
// masaPajak: params.msPajak || null,
// tahunPajak: params.thnPajak || null,
// npwp: params.idDipotong || null,
// advanced: isEmpty(params.advanced) ? undefined : params.advanced,
// ...sorting,
// };
// };
const normalizeParams = (params: any) => { const normalizeParams = (params: any) => {
const { const {
page = 0, page = 0,
......
...@@ -24,7 +24,8 @@ import useUpload from '../hooks/useUpload'; ...@@ -24,7 +24,8 @@ import useUpload from '../hooks/useUpload';
import { useGetDnById } from '../hooks/useGetDn'; import { useGetDnById } from '../hooks/useGetDn';
import ModalUploadDn from '../components/dialog/ModalUploadDn'; import ModalUploadDn from '../components/dialog/ModalUploadDn';
const bpuSchema = z.object({ const bpuSchema = z
.object({
tglPemotongan: z.string().nonempty('Tanggal Pemotongan harus diisi'), tglPemotongan: z.string().nonempty('Tanggal Pemotongan harus diisi'),
thnPajak: z.string().nonempty('Tahun Pajak harus diisi'), thnPajak: z.string().nonempty('Tahun Pajak harus diisi'),
msPajak: z.string().nonempty('Masa Pajak harus diisi'), msPajak: z.string().nonempty('Masa Pajak harus diisi'),
...@@ -45,7 +46,7 @@ const bpuSchema = z.object({ ...@@ -45,7 +46,7 @@ const bpuSchema = z.object({
keterangan5: z.string().optional(), keterangan5: z.string().optional(),
kdObjPjk: z.string().nonempty('Kode Objek Pajak harus diisi'), kdObjPjk: z.string().nonempty('Kode Objek Pajak harus diisi'),
fgFasilitas: z.string().nonempty('Fasilitas harus diisi'), fgFasilitas: z.string().nonempty('Fasilitas harus diisi'),
noDokLainnya: z.string().nonempty('No Dokumen Lainnya harus diisi'), noDokLainnya: z.string().optional(),
jmlBruto: z.string().nonempty('Jumlah Penghasilan Bruto harus diisi'), jmlBruto: z.string().nonempty('Jumlah Penghasilan Bruto harus diisi'),
tarif: z.union([z.string().nonempty('Tarif harus diisi'), z.number()]), tarif: z.union([z.string().nonempty('Tarif harus diisi'), z.number()]),
pphDipotong: z.string().nonempty('PPh Yang Dipotong/Dipungut harus diisi'), pphDipotong: z.string().nonempty('PPh Yang Dipotong/Dipungut harus diisi'),
...@@ -53,7 +54,20 @@ const bpuSchema = z.object({ ...@@ -53,7 +54,20 @@ const bpuSchema = z.object({
nomorDok: z.string().nonempty('Nomor Dokumen harus diisi'), nomorDok: z.string().nonempty('Nomor Dokumen harus diisi'),
tglDok: z.string().nonempty('Tanggal Dokumen harus diisi'), tglDok: z.string().nonempty('Tanggal Dokumen harus diisi'),
idTku: z.string().nonempty('Cabang harus diisi'), idTku: z.string().nonempty('Cabang harus diisi'),
}); })
.superRefine((data, ctx) => {
// Field dianggap DISABLED kalau fgFasilitas kosong ('') atau '9'
const isDisabled = ['', '9'].includes(data.fgFasilitas);
// Jika tidak disabled, berarti aktif → wajib isi
if (!isDisabled && (!data.noDokLainnya || data.noDokLainnya.trim() === '')) {
ctx.addIssue({
path: ['noDokLainnya'],
code: 'custom',
message: 'No Dokumen Lainnya harus diisi',
});
}
});
const DnRekamView = () => { const DnRekamView = () => {
const { id, type } = useParams<{ id?: string; type?: 'ubah' | 'pengganti' | 'new' }>(); const { id, type } = useParams<{ id?: string; type?: 'ubah' | 'pengganti' | 'new' }>();
......
import React from 'react'; import React from 'react';
import { GridPreferencePanelsValue, useGridApiContext } from '@mui/x-data-grid-premium'; import type { GridPreferencePanelsValue } from '@mui/x-data-grid-premium';
import { useGridApiContext } from '@mui/x-data-grid-premium';
import { IconButton, Tooltip } from '@mui/material'; import { IconButton, Tooltip } from '@mui/material';
import ViewColumnIcon from '@mui/icons-material/ViewColumn'; import ViewColumnIcon from '@mui/icons-material/ViewColumn';
......
import * as React from 'react'; import * as React from 'react';
import { GridToolbarContainer, GridToolbarProps } from '@mui/x-data-grid-premium'; import type { GridToolbarProps } from '@mui/x-data-grid-premium';
import { GridToolbarContainer } from '@mui/x-data-grid-premium';
import { Stack, Divider, IconButton, Tooltip } from '@mui/material'; import { Stack, Divider, IconButton, Tooltip } from '@mui/material';
import { ActionItem } from '../types/types'; import type { ActionItem } from '../types/types';
import { CustomFilterButton } from './CustomFilterButton'; import { CustomFilterButton } from './CustomFilterButton';
import CustomColumnsButton from './CustomColumnsButton'; import CustomColumnsButton from './CustomColumnsButton';
......
// import React, { useMemo, useState } from 'react';
// import { Stack, Button, Typography } from '@mui/material';
// import { useQueryClient } from '@tanstack/react-query';
// import { enqueueSnackbar } from 'notistack';
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
// import dayjs, { Dayjs } from 'dayjs';
// import minMax from 'dayjs/plugin/minMax';
// import DialogUmum from 'src/shared/components/dialog/DialogUmum';
// import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
// import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
// import useCancelDn from '../../hooks/useCancelDn';
// import { GridRowSelectionModel } from '@mui/x-data-grid-premium';
// dayjs.extend(minMax);
// // Helper format tanggal ke format API (DDMMYYYY)
// const formatDateDDMMYYYY = (d: Date) => {
// const dd = String(d.getDate()).padStart(2, '0');
// const mm = String(d.getMonth() + 1).padStart(2, '0');
// const yyyy = d.getFullYear();
// return `${dd}${mm}${yyyy}`;
// };
// interface ModalCancelDnProps {
// dataSelected?: any[]; // ✅ array of full row data (dari dataSelectedRef.current)
// setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
// tableApiRef?: React.MutableRefObject<any>;
// isOpenDialogCancel: boolean;
// setIsOpenDialogCancel: (v: boolean) => void;
// successMessage?: string;
// }
// const ModalCancelDn: React.FC<ModalCancelDnProps> = ({
// dataSelected = [],
// setSelectionModel,
// tableApiRef,
// isOpenDialogCancel,
// setIsOpenDialogCancel,
// successMessage = 'Data berhasil dibatalkan',
// }) => {
// const queryClient = useQueryClient();
// const [tglPembatalan, setTglPembatalan] = useState<Dayjs | null>(null);
// const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// const {
// numberOfData,
// numberOfDataFail,
// numberOfDataProcessed,
// numberOfDataSuccess,
// processSuccess,
// processFail,
// resetToDefault,
// status,
// } = useDialogProgressBar();
// const { mutateAsync } = useCancelDn({
// onSuccess: () => processSuccess(),
// onError: () => processFail(),
// });
// // ✅ Ambil tanggal pemotongan paling awal (minDate untuk DatePicker)
// const minPembatalanDate = useMemo(() => {
// if (!dataSelected.length) return null;
// const dates = dataSelected
// .map((d) => {
// const tgl = d.tglPemotongan || d.tglpemotongan;
// return tgl ? dayjs(tgl, ['YYYY-MM-DD', 'DD/MM/YYYY']) : null;
// })
// .filter((d): d is Dayjs => !!d && d.isValid());
// return dates.length > 0 ? dayjs.min(dates) : null;
// }, [dataSelected]);
// const handleCloseModal = () => {
// setIsOpenDialogCancel(false);
// resetToDefault();
// };
// const clearSelection = () => {
// tableApiRef?.current?.setRowSelectionModel?.([]);
// setSelectionModel?.(undefined);
// };
// const handleSubmit = async () => {
// if (!tglPembatalan) {
// enqueueSnackbar('Tanggal pembatalan harus diisi', { variant: 'warning' });
// return;
// }
// const formattedDate = formatDateDDMMYYYY(tglPembatalan.toDate());
// const ids = dataSelected.map((item) => String(item.id ?? item.internal_id));
// try {
// setIsOpenDialogProgressBar(true);
// const results = await Promise.allSettled(
// ids.map((id) => mutateAsync({ id, tglPembatalan: formattedDate }))
// );
// const rejected = results.filter((r) => r.status === 'rejected');
// const success = results.filter((r) => r.status === 'fulfilled');
// if (rejected.length > 0) {
// const errorMessages = rejected
// .map((r) => (r.status === 'rejected' ? r.reason?.message : ''))
// .filter(Boolean)
// .join('\n');
// enqueueSnackbar(
// <span style={{ whiteSpace: 'pre-line' }}>
// {errorMessages || `${rejected.length} dari ${ids.length} data gagal dibatalkan.`}
// </span>,
// { variant: 'error' }
// );
// processFail();
// } else {
// enqueueSnackbar(successMessage, { variant: 'success' });
// processSuccess();
// }
// // ✅ Langkah penting:
// // tunggu sampai React Query benar-benar refetch data baru
// await queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
// // ✅ lalu clear selection di DataGrid
// tableApiRef?.current?.setRowSelectionModel?.([]);
// setSelectionModel?.(undefined);
// handleCloseModal();
// } catch (error: any) {
// enqueueSnackbar(error?.message || 'Gagal membatalkan data', { variant: 'error' });
// } finally {
// setIsOpenDialogProgressBar(false);
// }
// };
// return (
// <>
// {/* ✅ Dialog reusable */}
// <DialogUmum
// isOpen={isOpenDialogCancel}
// onClose={handleCloseModal}
// title="Batal Bukti Pemotongan/Pemungutan PPh Unifikasi"
// >
// <Stack spacing={2}>
// <Typography>
// Silakan isi tanggal pembatalan. Tanggal tidak boleh sebelum tanggal pemotongan.
// </Typography>
// <DatePicker
// label="Tanggal Pembatalan"
// format="DD/MM/YYYY"
// value={tglPembatalan}
// maxDate={dayjs()} // tanggal maksimal = hari ini
// minDate={minPembatalanDate || undefined} // 🔹 minDate sesuai tglPemotongan
// onChange={(newValue) => setTglPembatalan(newValue)}
// slotProps={{
// textField: {
// size: 'medium',
// fullWidth: true,
// helperText:
// minPembatalanDate && `Tanggal minimal: ${minPembatalanDate.format('DD/MM/YYYY')}`,
// InputLabelProps: { shrink: true },
// sx: {
// '& .MuiOutlinedInput-root': {
// borderRadius: 1.5,
// backgroundColor: '#fff',
// '&:hover fieldset': {
// borderColor: '#123375 !important',
// },
// '&.Mui-focused fieldset': {
// borderColor: '#123375 !important',
// borderWidth: '1px',
// },
// },
// },
// },
// }}
// />
// <Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
// <Button variant="outlined" onClick={handleCloseModal}>
// Batal
// </Button>
// <Button
// variant="contained"
// color="error"
// onClick={handleSubmit}
// disabled={!tglPembatalan}
// >
// Batalkan
// </Button>
// </Stack>
// </Stack>
// </DialogUmum>
// {/* ✅ Dialog progress bar */}
// <DialogProgressBar
// isOpen={isOpenDialogProgressBar}
// handleClose={() => {
// handleCloseModal();
// setIsOpenDialogProgressBar(false);
// }}
// numberOfData={numberOfData}
// numberOfDataProcessed={numberOfDataProcessed}
// numberOfDataFail={numberOfDataFail}
// numberOfDataSuccess={numberOfDataSuccess}
// status={status}
// />
// </>
// );
// };
// export default ModalCancelDn;
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Stack, Button, Typography } from '@mui/material'; import { Stack, Button, Typography } from '@mui/material';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs, { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import minMax from 'dayjs/plugin/minMax'; import minMax from 'dayjs/plugin/minMax';
import DialogUmum from 'src/shared/components/dialog/DialogUmum'; import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar'; import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar'; import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import useCancelDn from '../../hooks/useCancelDn'; import useCancelDn from '../../hooks/useCancelNr';
import { GridRowSelectionModel } from '@mui/x-data-grid-premium'; import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
dayjs.extend(minMax); dayjs.extend(minMax);
...@@ -247,7 +31,7 @@ interface ModalCancelDnProps { ...@@ -247,7 +31,7 @@ interface ModalCancelDnProps {
successMessage?: string; successMessage?: string;
} }
const ModalCancelDn: React.FC<ModalCancelDnProps> = ({ const ModalCancelNr: React.FC<ModalCancelDnProps> = ({
dataSelected = [], dataSelected = [],
setSelectionModel, setSelectionModel,
tableApiRef, tableApiRef,
...@@ -441,4 +225,4 @@ const ModalCancelDn: React.FC<ModalCancelDnProps> = ({ ...@@ -441,4 +225,4 @@ const ModalCancelDn: React.FC<ModalCancelDnProps> = ({
); );
}; };
export default ModalCancelDn; export default ModalCancelNr;
...@@ -4,7 +4,7 @@ import DialogUmum from 'src/shared/components/dialog/DialogUmum'; ...@@ -4,7 +4,7 @@ import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import useCetakPdfDn from '../../hooks/useCetakPdfDn'; import useCetakPdfDn from '../../hooks/useCetakPdfNr';
import normalizePayloadCetakPdf from '../../utils/normalizePayloadCetakPdf'; import normalizePayloadCetakPdf from '../../utils/normalizePayloadCetakPdf';
interface ModalCetakPdfDnProps { interface ModalCetakPdfDnProps {
...@@ -13,18 +13,7 @@ interface ModalCetakPdfDnProps { ...@@ -13,18 +13,7 @@ interface ModalCetakPdfDnProps {
onClose: () => void; onClose: () => void;
} }
const formatTanggalIndo = (isoDate: string | undefined | null): string => { const ModalCetakPdfNr: React.FC<ModalCetakPdfDnProps> = ({ payload, isOpen, onClose }) => {
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 [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
...@@ -104,4 +93,4 @@ const ModalCetakPdfDn: React.FC<ModalCetakPdfDnProps> = ({ payload, isOpen, onCl ...@@ -104,4 +93,4 @@ const ModalCetakPdfDn: React.FC<ModalCetakPdfDnProps> = ({ payload, isOpen, onCl
); );
}; };
export default ModalCetakPdfDn; export default ModalCetakPdfNr;
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar'; import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar'; 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 DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import { GridRowSelectionModel } from '@mui/x-data-grid-premium'; import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useDeleteDn from '../../hooks/useDeleteDn'; import useDeleteDn from '../../hooks/useDeleteNr';
interface ModalDeleteDnProps { interface ModalDeleteDnProps {
dataSelected?: GridRowSelectionModel; dataSelected?: GridRowSelectionModel;
...@@ -46,7 +44,7 @@ const normalizeSelection = (sel?: any): (string | number)[] => { ...@@ -46,7 +44,7 @@ const normalizeSelection = (sel?: any): (string | number)[] => {
return []; return [];
}; };
const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({ const ModalDeleteNr: React.FC<ModalDeleteDnProps> = ({
dataSelected, dataSelected,
setSelectionModel, setSelectionModel,
tableApiRef, tableApiRef,
...@@ -145,4 +143,4 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({ ...@@ -145,4 +143,4 @@ const ModalDeleteDn: React.FC<ModalDeleteDnProps> = ({
); );
}; };
export default ModalDeleteDn; export default ModalDeleteNr;
...@@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,7 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar'; import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar'; import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import { GridRowSelectionModel } from '@mui/x-data-grid-premium'; import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useUpload from '../../hooks/useUpload'; import useUpload from '../../hooks/useUpload';
import DialogUmum from 'src/shared/components/dialog/DialogUmum'; import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
...@@ -11,12 +11,12 @@ import Grid from '@mui/material/Grid'; ...@@ -11,12 +11,12 @@ import Grid from '@mui/material/Grid';
import { Field } from 'src/components/hook-form'; import { Field } from 'src/components/hook-form';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from 'src/store'; import type { RootState } from 'src/store';
import Agreement from 'src/shared/components/agreement/Agreement'; import Agreement from 'src/shared/components/agreement/Agreement';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
interface ModalUploadDnProps { interface ModalUploadNrProps {
dataSelected?: GridRowSelectionModel; dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>; setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>; tableApiRef?: React.MutableRefObject<any>;
...@@ -55,7 +55,7 @@ const normalizeSelection = (sel?: any): (string | number)[] => { ...@@ -55,7 +55,7 @@ const normalizeSelection = (sel?: any): (string | number)[] => {
return []; return [];
}; };
const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ const ModalUploadNr: React.FC<ModalUploadNrProps> = ({
dataSelected, dataSelected,
setSelectionModel, setSelectionModel,
tableApiRef, tableApiRef,
...@@ -66,7 +66,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ ...@@ -66,7 +66,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
}) => { }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const uploadDn = useUpload(); const uploadNr = useUpload();
// custom hooks for progress state // custom hooks for progress state
const { const {
numberOfData, numberOfData,
...@@ -124,7 +124,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ ...@@ -124,7 +124,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' }); enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally { } finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi // sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] }); queryClient.invalidateQueries({ queryKey: ['unifikasi', 'nr'] });
} }
}; };
...@@ -158,17 +158,17 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ ...@@ -158,17 +158,17 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
<LoadingButton <LoadingButton
type="button" type="button"
disabled={!isCheckedAgreement} disabled={!isCheckedAgreement}
// onClick={onSubmit}
onClick={async () => { onClick={async () => {
if (onConfirmUpload) { if (onConfirmUpload) {
await onConfirmUpload(); await onConfirmUpload();
setIsOpenDialogUpload(false); setIsOpenDialogUpload(false);
return; return;
} }
await onSubmit(); await onSubmit();
}} }}
loading={uploadDn.isPending} loading={uploadNr.isPending}
variant="contained" variant="contained"
sx={{ background: '#143B88' }} sx={{ background: '#143B88' }}
> >
...@@ -195,4 +195,4 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({ ...@@ -195,4 +195,4 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
); );
}; };
export default ModalUploadDn; export default ModalUploadNr;
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import { 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 type { RootState } from 'src/store';
import { useFormContext } from 'react-hook-form';
const DokumenReferensi = () => {
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}>
<Grid sx={{ mt: 3 }} size={{ md: 12 }}>
<Divider sx={{ fontWeight: 'bold' }} textAlign="left">
Daftar Dokumen
</Divider>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Select name="namaDok" label="Nama Dokumen">
{JENIS_DOKUMEN.map((item, index) => (
<MenuItem key={index} value={item.value}>
{item.label}
</MenuItem>
))}
</Field.Select>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="nomorDok" label="Nomor Dokumen" />
</Grid>
<Grid size={{ md: 6 }}>
<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">
<MenuItem value={nitku}>{nitku}</MenuItem>
</Field.Select>
</Grid>
</Grid>
);
};
export default DokumenReferensi;
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 { Field } from 'src/components/hook-form';
import type { TCountryResult } from '../../types/types';
import MenuItem from '@mui/material/MenuItem';
type IdentitasProps = {
isPengganti: boolean;
existingNr?: any; // Data penuh dari API (opsional, untuk edit/pengganti)
country: TCountryResult;
};
const Identitas = ({ isPengganti, existingNr, country }: IdentitasProps) => {
const { setValue, watch, getValues } = useFormContext();
const tanggalPemotongan = watch('tglPemotongan');
const maxKeterangan = 5;
const [jumlahKeterangan, setJumlahKeterangan] = useState<number>(0);
// 🧩 Auto isi Tahun & Masa Pajak berdasarkan tanggalPemotongan
useEffect(() => {
if (tanggalPemotongan) {
const date = dayjs(tanggalPemotongan);
setValue('thnPajak', date.format('YYYY'));
setValue('masaPajak', date.format('MM'));
} else {
setValue('thnPajak', '');
setValue('masaPajak', '');
}
}, [tanggalPemotongan, setValue]);
// 🧠 Saat data API sudah masuk (edit/pengganti)
// Gunakan getValues() agar langsung membaca nilai dari form (bukan nunggu watch)
useEffect(() => {
if (existingNr) {
const currentValues = getValues();
const arr = [
currentValues.keterangan1,
currentValues.keterangan2,
currentValues.keterangan3,
currentValues.keterangan4,
currentValues.keterangan5,
];
const count = arr.filter((k) => !!k && k.trim() !== '').length;
console.log('🧠 Detected existing keterangan:', arr, 'count:', count);
if (count > 0) {
setJumlahKeterangan(count);
}
}
}, [existingNr, getValues]);
// 🧩 Pantau perubahan manual user (Tambah/Hapus)
useEffect(() => {
const subscription = watch((values) => {
const arr = [
values.keterangan1,
values.keterangan2,
values.keterangan3,
values.keterangan4,
values.keterangan5,
];
const count = arr.filter((k) => !!k && k.trim() !== '').length;
setJumlahKeterangan(count);
});
return () => subscription.unsubscribe();
}, [watch]);
// ➕ Tambah field baru
const handleTambah = () => {
if (jumlahKeterangan < maxKeterangan) {
setJumlahKeterangan((prev) => prev + 1);
}
};
// ➖ Hapus field terakhir
const handleHapus = () => {
if (jumlahKeterangan > 0) {
setValue(`keterangan${jumlahKeterangan}`, '');
setJumlahKeterangan((prev) => prev - 1);
}
};
return (
<>
{/* 📋 Identitas Dasar */}
<Grid container rowSpacing={2} alignItems="center" columnSpacing={2} sx={{ mb: 4 }}>
{/* 📅 Tanggal & Masa Pajak */}
<Grid size={{ md: 6 }}>
<Field.DatePicker
name="tglPemotongan"
label="Tanggal Pemotongan"
format="DD/MM/YYYY"
maxDate={dayjs()}
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 3 }}>
<Field.DatePicker
name="thnPajak"
label="Tahun Pajak"
view="year"
format="YYYY"
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 3 }}>
<Field.DatePicker
name="masaPajak"
label="Masa Pajak"
view="month"
format="MM"
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="idDipotong" label="Tax ID Number (TIN)" disabled={isPengganti} />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="namaDipotong" label="Nama" disabled={isPengganti} />
</Grid>
<Grid size={{ md: 12 }}>
<Field.Text
name="alamatDipotong"
label="Alamat"
multiline
minRows={2}
disabled={isPengganti}
sx={{
'& .MuiInputBase-inputMultiline': {
lineHeight: 1.6,
},
'& .MuiOutlinedInput-root': {
borderRadius: '8px',
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: '#bdbdbd',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#1976d2',
borderWidth: '2px',
},
}}
/>
</Grid>
<Grid size={{ md: 12 }}>
<Field.Select name="negaraDipotong" label="Negara" disabled={isPengganti}>
{country.map((item) => (
<MenuItem key={item.kode} value={item.kode}>
{`${item.nama}`}
</MenuItem>
))}
</Field.Select>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="tmptLahirDipotong" label="Tempat Lahir" disabled={isPengganti} />
</Grid>
<Grid size={{ md: 6 }}>
<Field.DatePicker
name="tglLahirDipotong"
label="Tanggal Lahir"
format="DD/MM/YYYY"
maxDate={dayjs()}
disabled={isPengganti}
/>
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="nomorPaspor" label="No. Paspor" disabled={isPengganti} />
</Grid>
<Grid size={{ md: 6 }}>
<Field.Text name="nomorKitasKitap" label="No.KITAS/KITAP" disabled={isPengganti} />
</Grid>
</Grid>
{/* ✏️ Tombol 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>
{/* 🗒️ Input Keterangan Tambahan */}
<Box sx={{ mb: 3 }}>
{Array.from({ length: jumlahKeterangan }).map((_, i) => (
<Grid size={{ md: 12 }} key={`keterangan${i + 1}`}>
<Field.Text
sx={{ mb: 2 }}
name={`keterangan${i + 1}`}
label={`Keterangan Tambahan ${i + 1}`}
/>
</Grid>
))}
</Box>
</>
);
};
export default Identitas;
import type { FC} from 'react';
import { Fragment, memo } from 'react';
import { Box, Button, Card, CardContent, CardHeader, IconButton, Typography } from '@mui/material';
import { ChevronRightRounded, CloseRounded } from '@mui/icons-material';
import { m } from 'framer-motion';
import { PANDUAN_REKAM_DN } from '../../constant';
interface PanduanDnRekamProps {
handleOpen: () => void;
isOpen: boolean;
}
const PanduanDnRekam: FC<PanduanDnRekamProps> = ({ handleOpen, isOpen }) => (
<Box position="sticky">
{/* Tombol toggle */}
{!isOpen && (
<Box height="100%" display="flex" justifyContent="center" alignItems="center">
<Button
variant="contained"
sx={{
height: 'fit-content',
right: 0,
borderRadius: 0,
minWidth: 35,
pt: 3,
pb: 3,
fontWeight: 'bold',
fontSize: 16,
backgroundColor: '#143B88',
}}
size="small"
onClick={handleOpen}
>
<span
style={{
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
display: 'flex',
alignItems: 'center',
}}
>
Panduan Penggunaan
<ChevronRightRounded sx={{ fontSize: 30 }} />
</span>
</Button>
</Box>
)}
{/* Konten panduan */}
{isOpen && (
<m.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1, transition: { delay: 0.2 } }}
>
<Card>
<CardHeader
avatar={
<img src="/assets/icon_panduan_penggunaan_1.svg" alt="Panduan" loading="lazy" />
}
sx={{
backgroundColor: '#123375',
color: '#FFFFFF',
p: 2,
'& .MuiCardHeader-title': { fontSize: 18 },
}}
action={
<IconButton aria-label="close" onClick={handleOpen} sx={{ color: 'white' }}>
<CloseRounded />
</IconButton>
}
title="Panduan Penggunaan"
/>
<CardContent
sx={{
maxHeight: 300,
overflow: 'auto',
'&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': {
backgroundColor: '#f0f0f0',
borderRadius: 8,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: '#123375',
borderRadius: 8,
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: '#0d2858',
},
scrollbarWidth: 'thin',
scrollbarColor: '#123375 #f0f0f0',
}}
>
{/* Deskripsi Form */}
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-line' }}>
<strong>Deskripsi Form:</strong>
<br />
{PANDUAN_REKAM_DN.description.intro}
</Typography>
<Typography variant="body2">{PANDUAN_REKAM_DN.description.textList}</Typography>
<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>
{/* 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) => (
<Fragment key={`item-${i}-${idx}`}>
<Box component="li" sx={{ mb: 0.5 }}>
<Typography variant="body2" component="span">
{item.text}
</Typography>
{item.subItems?.length > 0 && (
<Box component="ol" sx={{ pl: 3, listStyle: 'decimal' }}>
{item.subItems.map((sub, subIdx) => (
<Typography
key={`sub-${i}-${idx}-${subIdx}`}
variant="body2"
component="li"
>
{sub}
</Typography>
))}
</Box>
)}
</Box>
</Fragment>
))}
</Box>
</Box>
))}
</CardContent>
</Card>
</m.div>
)}
</Box>
);
export default memo(PanduanDnRekam);
...@@ -2,14 +2,14 @@ const appRootKey = 'unifikasi'; ...@@ -2,14 +2,14 @@ const appRootKey = 'unifikasi';
const queryKey = { const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params], getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
dn: { nr: {
all: (params: any) => [appRootKey, 'dn', params], all: (params: any) => [appRootKey, 'nr', params],
detail: (params: any) => [appRootKey, 'dn', 'detail', params], detail: (params: any) => [appRootKey, 'nr', 'detail', params],
draft: [appRootKey, 'dn', 'draft'], draft: [appRootKey, 'nr', 'draft'],
delete: [appRootKey, 'dn', 'delete'], delete: [appRootKey, 'nr', 'delete'],
upload: [appRootKey, 'dn', 'upload'], upload: [appRootKey, 'nr', 'upload'],
cancel: [appRootKey, 'dn', 'cancel'], cancel: [appRootKey, 'nr', 'cancel'],
cetakPdf: (params: any) => [appRootKey, 'dn-cetak-pdf', params], cetakPdf: (params: any) => [appRootKey, 'nr-cetak-pdf', params],
}, },
}; };
......
// type FilterItem = {
// field: string;
// operator: string;
// value?: string | number | Array<string | number> | null;
// join?: 'AND' | 'OR'; // optional: join connector BEFORE this item (first item usually undefined)
// };
// type BaseParams = Record<string, any>;
// export function useAdvancedFilter() {
// const numericFields = new Set(['masaPajak', 'tahunPajak', 'dpp', 'pphDipotong']);
// const dateFields = new Set(['created_at', 'updated_at']);
// const fieldMap: Record<string, string> = {
// noBupot: 'nomorBupot',
// };
// const dbField = (field: string) => fieldMap[field] ?? field;
// const escape = (v: string) => String(v).replace(/'/g, "''");
// const toDbDate = (value: string | Date) => {
// if (value instanceof Date) {
// const y = value.getFullYear();
// const m = String(value.getMonth() + 1).padStart(2, '0');
// const d = String(value.getDate()).padStart(2, '0');
// return `${y}${m}${d}`;
// }
// const digits = String(value).replace(/[^0-9]/g, '');
// if (digits.length >= 8) return digits.slice(0, 8);
// return digits;
// };
// const normalizeOp = (op: string) => op?.toString().trim();
// function buildAdvancedFilter(filters?: FilterItem[] | null) {
// if (!filters || filters.length === 0) return '';
// const exprs: string[] = []; // each item's expression
// const joins: ('AND' | 'OR')[] = []; // join before each expr (for item 0, push nothing/AND by default)
// for (let i = 0; i < filters.length; i++) {
// const f = filters[i];
// if (!f || !f.field) continue;
// const op = normalizeOp(f.operator ?? '');
// const fieldName = dbField(f.field);
// // build expression for this item
// let expr: string | null = null;
// // DATE handling
// if (dateFields.has(fieldName)) {
// const rawVal = f.value;
// if (!rawVal && !/is empty|is not empty/i.test(op)) {
// continue;
// }
// if (/^is$/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd} 00:00:00' AND \"${fieldName}\" <= '${ymd} 23:59:59'`;
// } else if (/is on or after/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" >= '${ymd}'`;
// } else if (/is on or before/i.test(op)) {
// const ymd = toDbDate(rawVal as string | Date);
// if (!ymd) continue;
// expr = `\"${fieldName}\" <= '${ymd}'`;
// }
// }
// // EMPTY checks (user requested LOWER("col") IS NULL semantics)
// if (/is empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NULL`;
// } else if (/is not empty/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") IS NOT NULL`;
// }
// // IS ANY OF handling
// if (!expr && /is any of/i.test(op)) {
// // collect values array
// let values: Array<string | number> = [];
// if (Array.isArray(f.value)) values = f.value as any;
// else if (typeof f.value === 'string')
// values = (f.value as string)
// .split(',')
// .map((s) => s.trim())
// .filter(Boolean);
// else if (f.value != null) values = [f.value as any];
// if ((values || []).length === 0) {
// expr = null;
// } else {
// // special-case fgStatus: need LIKE %val% OR LIKE %val2%
// if (fieldName === 'fgStatus' || fieldName === 'fg_status') {
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") LIKE LOWER('%${s}%')`;
// });
// expr = `(${ors.join(' OR ')})`;
// } else {
// // default: OR of equality (case-insensitive)
// const ors = (values as any[]).map((v) => {
// const s = escape(String(v).toLowerCase());
// return `LOWER(\"${fieldName}\") = '${s}'`;
// });
// expr = `(${ors.join(' OR ')})`;
// }
// }
// }
// // FGSTATUS special single-value is / is not / contains semantics
// if (!expr && (fieldName === 'fgStatus' || fieldName === 'fg_status')) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '' && !/is any of|is empty|is not empty/i.test(op)) {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// if (/^is$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/is not/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") NOT LIKE LOWER('%${valEscaped}%')`;
// }
// }
// }
// // GENERIC text/numeric handling when expr still not set
// if (!expr) {
// const valRaw = f.value == null ? '' : String(f.value);
// if (valRaw === '') {
// expr = null;
// } else {
// const valEscaped = escape(valRaw.toLowerCase());
// // numeric fields: operators (=, >=, <=)
// if (numericFields.has(fieldName) && /^(=|>=|<=)$/.test(op)) {
// expr = `\"${fieldName}\" ${op} '${valEscaped}'`;
// } else if (/^contains$/i.test(op)) {
// expr = `LOWER(\"${fieldName}\") LIKE LOWER('%${valEscaped}%')`;
// } else if (/^equals$/i.test(op)) {
// // equals should produce IN (wrap single value as IN)
// // attempt to parse CSV if provided
// let values: string[] = [];
// if (Array.isArray(f.value))
// values = (f.value as any[]).map((v) => escape(String(v).toLowerCase()));
// else values = [escape(String(f.value).toLowerCase())];
// expr = `LOWER(\"${fieldName}\") IN (${values.map((v) => `'${v}'`).join(',')})`;
// } else if (/^(>=|<=|=)$/.test(op) && !numericFields.has(fieldName)) {
// expr = `LOWER(\"${fieldName}\") ${op} '${valEscaped}'`;
// } else if (/^(is)$/i.test(op)) {
// // fallback: treat as equals
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// } else {
// // fallback equality
// expr = `LOWER(\"${fieldName}\") = '${valEscaped}'`;
// }
// }
// }
// if (expr) {
// exprs.push(expr);
// // record join for this item (use provided join or default AND except for first item)
// const joinBefore = (f.join as 'AND' | 'OR') ?? (exprs.length > 1 ? 'AND' : 'AND');
// joins.push(joinBefore);
// }
// }
// // now combine exprs with joins; joins[i] is join BEFORE exprs[i]
// if (exprs.length === 0) return '';
// let out = exprs[0];
// for (let i = 1; i < exprs.length; i++) {
// const j = joins[i] ?? 'AND';
// out = `(${out}) ${j} (${exprs[i]})`;
// }
// return out;
// }
// function buildRequestParams(base: BaseParams = {}, advanced: string) {
// const out: BaseParams = { ...(base ?? {}) };
// if ('noBupot' in out) {
// out.nomorBupot = out.noBupot;
// delete out.noBupot;
// }
// out.advanced = advanced || '';
// return out;
// }
// return { buildAdvancedFilter, buildRequestParams } as const;
// }
// export default useAdvancedFilter;
type FilterItem = { type FilterItem = {
field: string; field: string;
operator: string; operator: string;
......
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 type { TCancelNrRequest, TCancelNrResponse } from '../types/types';
import nrApi from '../utils/api';
const useCancelNr = (props?: any) =>
useMutation<TCancelNrResponse, Error, TCancelNrRequest>({
mutationKey: ['cancel-nr'],
mutationFn: (payload) => nrApi.cancel(payload),
...props,
});
export default useCancelNr;
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import dnApi from '../utils/api'; import nrApi from '../utils/api';
const useCetakPdfDn = (options?: any) => const useCetakPdfNr = (options?: any) =>
useMutation({ useMutation({
mutationKey: ['unifikasi', 'dn', 'cetak-pdf'], mutationKey: ['unifikasi', 'nr', 'cetak-pdf'],
mutationFn: async (params: any) => dnApi.cetakPdfDetail(params), mutationFn: async (params: any) => nrApi.cetakPdfDetail(params),
...options, ...options,
}); });
export default useCetakPdfDn; export default useCetakPdfNr;
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { TDeleteDnRequest, TBaseResponseAPI } from '../types/types'; import type { TBaseResponseAPI, TDeleteNrRequest } from '../types/types';
import dnApi from '../utils/api'; import nrApi from '../utils/api';
const useDeleteDn = (props?: any) => const useDeleteDn = (props?: any) =>
useMutation<TBaseResponseAPI<null>, Error, TDeleteDnRequest>({ useMutation<TBaseResponseAPI<null>, Error, TDeleteNrRequest>({
mutationKey: ['delete-dn'], mutationKey: ['delete-nr'],
mutationFn: (payload) => dnApi.deleteDn(payload), mutationFn: (payload) => nrApi.deleteNr(payload),
...props, ...props,
}); });
......
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { TBaseResponseAPI, TGetListDataKOPDnResult } from '../types/types'; import type { TBaseResponseAPI, TGetListDataKOPNrResult } from '../types/types';
import queryKey from '../constant/queryKey'; import queryKey from '../constant/queryKey';
import dnApi from '../utils/api'; import nrApi from '../utils/api';
const useGetKodeObjekPajak = (params?: Record<string, any>) => const useGetKodeObjekPajakNr = (params?: Record<string, any>) =>
useQuery<TBaseResponseAPI<TGetListDataKOPDnResult>>({ useQuery<TBaseResponseAPI<TGetListDataKOPNrResult>>({
queryKey: queryKey.getKodeObjekPajak(params), queryKey: queryKey.getKodeObjekPajak(params),
queryFn: () => dnApi.getKodeObjekPajakDn(params), queryFn: () => nrApi.getKodeObjekPajakNr(params),
}); });
export default useGetKodeObjekPajak; export default useGetKodeObjekPajakNr;
import { useQuery } from '@tanstack/react-query';
import type { TCountryResult } from '../types/types';
import nrApi from '../utils/api';
export const useGetNegara = (params?: Record<string, any>) =>
useQuery<TCountryResult>({
queryKey: ['negara-nr'],
queryFn: async () => {
const res = await nrApi.getCountryNr(params);
return res.data; // ✅ langsung array negara
},
});
export default useGetNegara;
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import dnApi from '../utils/api'; import type {
import { TGetListDataTableDn, TGetListDataTableDnResult } from '../types/types'; // TGetListDataTableDnResult,
TGetListDataTableNr,
TGetListDataTableNrResult,
} from '../types/types';
import { FG_PDF_STATUS, FG_SIGN_STATUS } from '../constant'; import { FG_PDF_STATUS, FG_SIGN_STATUS } from '../constant';
import queryKey from '../constant/queryKey'; import queryKey from '../constant/queryKey';
import nrApi from '../utils/api';
import dayjs from 'dayjs';
export type TGetDnApiWrapped = { export type TGetDnApiWrapped = {
data: TGetListDataTableDnResult[]; data: TGetListDataTableNrResult[];
total: number; total: number;
pageSize: number; pageSize: number;
page: number; // 1-based page: number; // 1-based
...@@ -61,7 +66,7 @@ export const formatDateToDDMMYYYY = (dateString: string | null | undefined) => { ...@@ -61,7 +66,7 @@ export const formatDateToDDMMYYYY = (dateString: string | null | undefined) => {
return `${day}/${month}/${year}`; return `${day}/${month}/${year}`;
}; };
const normalisePropsGetDn = (params: TGetListDataTableDn) => ({ const normalisePropsGetNr = (params: TGetListDataTableNr) => ({
...params, ...params,
nomorSP2D: params.dokumen_referensi?.[0]?.nomorSP2D || '', nomorSP2D: params.dokumen_referensi?.[0]?.nomorSP2D || '',
metodePembayaranBendahara: params.dokumen_referensi?.[0]?.metodePembayaranBendahara || '', metodePembayaranBendahara: params.dokumen_referensi?.[0]?.metodePembayaranBendahara || '',
...@@ -74,7 +79,7 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({ ...@@ -74,7 +79,7 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
fgStatus: params.fgStatus, fgStatus: params.fgStatus,
fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus), fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus),
fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)), fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)),
fgLapor: params.fgLapor, // fgLapor: params.fgLapor,
revNo: params.revNo, revNo: params.revNo,
thnPajak: params.tahunPajak, thnPajak: params.tahunPajak,
msPajak: params.masaPajak, msPajak: params.masaPajak,
...@@ -92,23 +97,55 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({ ...@@ -92,23 +97,55 @@ const normalisePropsGetDn = (params: TGetListDataTableDn) => ({
updated_at: formatDateToDDMMYYYY(params.updated_at), updated_at: formatDateToDDMMYYYY(params.updated_at),
}); });
// ---------- normalizer for params request ---------- export const normalizeExistingNr = (res: any) => ({
const normalisPropsParmasGetDn = (params: any) => { // 🧾 Data Pajak Utama
const sorting = !isEmpty(params.sortModel) tglPemotongan: res.tglpemotongan ?? '',
? transformSortModelToSortApiPayload(params.sortModel) thnPajak: res.tahunPajak ?? '',
: {}; msPajak: res.masaPajak ?? '',
return { // 👤 Identitas Dipotong
...params, idDipotong: res.npwpPemotong ?? '',
page: (typeof params.page === 'number' ? params.page : 0) + 1, namaDipotong: res.namaDipotong ?? '',
limit: params.pageSize, alamatDipotong: res.alamatDipotong ?? '',
masaPajak: params.msPajak || null, negaraDipotong: res.negaraDipotong ?? '',
tahunPajak: params.thnPajak || null, tmptLahirDipotong: res.tmptLahirDipotong ?? '',
npwp: params.idDipotong || null, tglLahirDipotong:
advanced: isEmpty(params.advanced) ? undefined : params.advanced, res.tglLahirDipotong && res.tglLahirDipotong.length === 8
...sorting, ? dayjs(res.tglLahirDipotong, 'DDMMYYYY').format('YYYY-MM-DD')
}; : '',
}; nomorPaspor: res.nomorPaspor ?? '',
nomorKitasKitap: res.nomorKitasKitap ?? '',
// 🧠 Informasi Tambahan
email: res.email ?? '',
keterangan1: res.keterangan1 ?? '',
keterangan2: res.keterangan2 ?? '',
keterangan3: res.keterangan3 ?? '',
keterangan4: res.keterangan4 ?? '',
keterangan5: res.keterangan5 ?? '',
// 💰 Pajak dan Penghasilan
kodeObjekPajak: res.kodeObjekPajak ?? '',
fgFasilitas: res.sertifikatInsentifDipotong ?? '',
noDokLainnya: res.nomorSertifikatInsentif ?? '',
penghasilanBruto: res.penghasilanBruto ?? '',
normaPenghasilanNeto: res.normaPenghasilanNeto ?? '',
tarif: String(res.tarif ?? ''),
pphDipotong: String(res.pphDipotong ?? ''),
// 📄 Dokumen Referensi
namaDok: res.dokumen_referensi?.[0]?.dokReferensi ?? '',
nomorDok: res.dokumen_referensi?.[0]?.nomorDokumen ?? '',
tglDok: res.dokumen_referensi?.[0]?.tanggal_Dokumen ?? '',
// 🏢 Cabang / Unit
idTku: res.idTku ?? '',
// 🆔 Metadata tambahan
idBupot: res.idBupot ?? '',
noBupot: res.noBupot ?? '',
revNo: res.revNo ?? 0,
});
const normalizeParams = (params: any) => { const normalizeParams = (params: any) => {
const { const {
...@@ -155,7 +192,7 @@ const normalizeParams = (params: any) => { ...@@ -155,7 +192,7 @@ const normalizeParams = (params: any) => {
}; };
}; };
export const useGetDn = ({ params }: { params: any }) => { export const useGetNr = ({ params }: { params: any }) => {
const { page, limit, advanced, sortingMode, sortingMethod } = params; const { page, limit, advanced, sortingMode, sortingMethod } = params;
const normalized = normalizeParams(params); const normalized = normalizeParams(params);
...@@ -163,19 +200,19 @@ export const useGetDn = ({ params }: { params: any }) => { ...@@ -163,19 +200,19 @@ export const useGetDn = ({ params }: { params: any }) => {
queryKey: ['dn', page, limit, advanced, sortingMode, sortingMethod], queryKey: ['dn', page, limit, advanced, sortingMode, sortingMethod],
queryFn: async () => { queryFn: async () => {
const res: any = await dnApi.getDn({ params: normalized }); const res: any = await nrApi.getNr({ params: normalized });
const rawData: any[] = Array.isArray(res?.data) ? res.data : res?.data ? [res.data] : []; const rawData: any[] = Array.isArray(res?.data) ? res.data : res?.data ? [res.data] : [];
const total = Number(res?.total ?? res?.totalRow ?? 0); const total = Number(res?.total ?? res?.totalRow ?? 0);
let dataArray: TGetListDataTableDnResult[] = []; let dataArray: TGetListDataTableNrResult[] = [];
const normalizeWithWorker = () => const normalizeWithWorker = () =>
new Promise<TGetListDataTableDnResult[]>((resolve, reject) => { new Promise<TGetListDataTableNrResult[]>((resolve, reject) => {
try { try {
const worker = new Worker( const worker = new Worker(
new URL('../workers/normalizeDn.worker.js', import.meta.url), new URL('../workers/normalizeNr.worker.js', import.meta.url),
{ type: 'module' } { type: 'module' }
); );
...@@ -186,7 +223,7 @@ export const useGetDn = ({ params }: { params: any }) => { ...@@ -186,7 +223,7 @@ export const useGetDn = ({ params }: { params: any }) => {
reject(new Error(error)); reject(new Error(error));
} else { } else {
worker.terminate(); worker.terminate();
resolve(data as TGetListDataTableDnResult[]); resolve(data as TGetListDataTableNrResult[]);
} }
}; };
...@@ -206,11 +243,11 @@ export const useGetDn = ({ params }: { params: any }) => { ...@@ -206,11 +243,11 @@ export const useGetDn = ({ params }: { params: any }) => {
dataArray = await normalizeWithWorker(); dataArray = await normalizeWithWorker();
} else { } else {
console.warn('⚠️ Worker not supported, using sync normalization'); console.warn('⚠️ Worker not supported, using sync normalization');
dataArray = rawData.map(normalisePropsGetDn) as unknown as TGetListDataTableDnResult[]; dataArray = rawData.map(normalisePropsGetNr) as unknown as TGetListDataTableNrResult[];
} }
} catch (err) { } catch (err) {
console.error('❌ Worker failed, fallback to sync normalize:', err); console.error('❌ Worker failed, fallback to sync normalize:', err);
dataArray = rawData.map(normalisePropsGetDn) as unknown as TGetListDataTableDnResult[]; dataArray = rawData.map(normalisePropsGetNr) as unknown as TGetListDataTableNrResult[];
} }
return { return {
...@@ -230,43 +267,14 @@ export const useGetDn = ({ params }: { params: any }) => { ...@@ -230,43 +267,14 @@ export const useGetDn = ({ params }: { params: any }) => {
}); });
}; };
export const useGetDnById = (id: string, options = {}) => export const useGetNrById = (id: string, options = {}) =>
useQuery({ useQuery({
queryKey: queryKey.dn.detail(id), queryKey: queryKey.nr.detail(id),
queryFn: async () => { queryFn: async () => {
console.log('🔍 Fetching getDnById with ID:', id); const res = await nrApi.getNrById(id);
const res = await dnApi.getDnById(id); console.log(res);
if (!res) throw new Error('Data tidak ditemukan'); if (!res) throw new Error('Data tidak ditemukan');
const normalized = normalizeExistingNr(res);
const normalized = {
id: res.id ?? '',
tglPemotongan: res.tglpemotongan ?? '',
thnPajak: res.tahunPajak ?? '',
msPajak: res.masaPajak ?? '',
idDipotong: res.npwpPemotong ?? '',
nitku: res.idTku ?? '',
namaDipotong: res.nama ?? '',
email: res.email ?? '',
keterangan1: res.keterangan1 ?? '',
keterangan2: res.keterangan2 ?? '',
keterangan3: res.keterangan3 ?? '',
keterangan4: res.keterangan4 ?? '',
keterangan5: res.keterangan5 ?? '',
kdObjPjk: res.kodeObjekPajak ?? '',
fgFasilitas: res.sertifikatInsentifDipotong ?? '',
noDokLainnya: res.nomorSertifikatInsentif ?? '',
jmlBruto: res.dpp ?? '',
tarif: String(res.tarif ?? ''),
pphDipotong: String(res.pphDipotong ?? ''),
namaDok: res.dokumen_referensi?.[0]?.dokReferensi ?? '',
nomorDok: res.dokumen_referensi?.[0]?.nomorDokumen ?? '',
tglDok: res.dokumen_referensi?.[0]?.tanggal_Dokumen ?? '',
idTku: res.idTku ?? '',
revNo: res.revNo ?? 0,
noBupot: res.noBupot ?? '',
idBupot: res.idBupot ?? '',
};
console.log('✅ Normalized data:', normalized); console.log('✅ Normalized data:', normalized);
return normalized; return normalized;
}, },
...@@ -275,4 +283,4 @@ export const useGetDnById = (id: string, options = {}) => ...@@ -275,4 +283,4 @@ export const useGetDnById = (id: string, options = {}) =>
...options, ...options,
}); });
export default useGetDn; export default useGetNr;
/* eslint-disable @typescript-eslint/no-shadow */
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch } from 'react-hook-form';
import { TGetListDataKOPDn } from '../types/types'; import type { TGetListDataKOPNr } from '../types/types';
const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => { const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPNr) => {
const { watch, setValue, control } = useFormContext(); const { watch, setValue, control } = useFormContext();
// ambil value dari form // ambil value dari form
...@@ -35,7 +36,6 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => { ...@@ -35,7 +36,6 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
name: ['thnPajak', 'fgFasilitas', 'fgIdDipotong', 'jmlBruto', 'tarif'], name: ['thnPajak', 'fgFasilitas', 'fgIdDipotong', 'jmlBruto', 'tarif'],
}); });
// eslint-disable-next-line @typescript-eslint/no-shadow
const calculateAndSetPphDipotong = ( const calculateAndSetPphDipotong = (
thnPajak: number, thnPajak: number,
fgFasilitas: string, fgFasilitas: string,
...@@ -67,6 +67,7 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => { ...@@ -67,6 +67,7 @@ const usePphDipotong = (kodeObjekPajakSelected?: TGetListDataKOPDn) => {
Number(handlerSetPphDipotong[4]) Number(handlerSetPphDipotong[4])
); );
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handlerSetPphDipotong]); }, [handlerSetPphDipotong]);
return { return {
......
import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import dnApi from '../utils/api';
import { 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 ?? null,
noBupot: noBupot ?? null,
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 useSaveDn = (props?: any) =>
useMutation({
mutationKey: ['Save-Dn'],
mutationFn: (params: any) => dnApi.saveDn(transformParams(params)),
...props,
});
export default useSaveDn;
import { useMutation } from '@tanstack/react-query';
import dayjs from 'dayjs';
import type { TPostNrRequest } from '../types/types';
import nrApi from '../utils/api';
const transformParams = ({ isPengganti = false, ...nrData }: any): TPostNrRequest => {
const {
id,
idBupot,
noBupot,
npwpPemotong,
idTku,
masaPajak,
tahunPajak,
tinDipotong,
namaDipotong,
alamatDipotong,
negaraDipotong,
tglLahirDipotong,
tmptLahirDipotong,
nomorPaspor,
nomorKitasKitap,
sertifikatInsentifDipotong,
nomorSertifikatInsentif,
kodeObjekPajak,
pasalPph,
statusPph,
penghasilanBruto,
normaPenghasilanNeto,
tarif,
pphDipotong,
kap,
kjs,
metodePembayaranBendahara,
nomorSP2D,
tglPemotongan,
userId,
kanal,
revNo: initialRevNo,
glAccount,
keterangan1,
keterangan2,
keterangan3,
keterangan4,
keterangan5,
} = nrData;
// Increment revNo kalau pengganti
const revNo = isPengganti
? parseInt(initialRevNo?.toString() || '0', 10) + 1
: parseInt(initialRevNo?.toString() || '0', 10);
// Ambil NPWP dari localStorage kalau mau fallback
const npwpLog = localStorage.getItem('npwp_log') ?? '';
return {
id: !isPengganti ? (id ?? null) : null,
idBupot: idBupot ?? null,
noBupot: noBupot ?? null,
// Header-level Identitas
npwpPemotong: npwpPemotong ?? npwpLog,
idTku: idTku ?? '',
masaPajak: masaPajak ? dayjs(masaPajak).format('MM') : '',
tahunPajak: tahunPajak ? Number(dayjs(tahunPajak).format('YYYY')) : new Date().getFullYear(),
// Data Wajib Pajak Dipotong
tinDipotong: tinDipotong ?? '',
namaDipotong: namaDipotong ?? '',
alamatDipotong: alamatDipotong ?? '',
negaraDipotong: negaraDipotong ?? '',
tglLahirDipotong: tglLahirDipotong ? dayjs(tglLahirDipotong).format('DDMMYYYY') : '',
tmptLahirDipotong: tmptLahirDipotong ?? '',
nomorPaspor: nomorPaspor ?? '',
nomorKitasKitap: nomorKitasKitap ?? '',
keterangan1: keterangan1 ?? '',
keterangan2: keterangan2 ?? '',
keterangan3: keterangan3 ?? '',
keterangan4: keterangan4 ?? '',
keterangan5: keterangan5 ?? '',
// Fasilitas
sertifikatInsentifDipotong: sertifikatInsentifDipotong ?? '9',
nomorSertifikatInsentif: nomorSertifikatInsentif ?? '',
// Objek Pajak
kodeObjekPajak: kodeObjekPajak ?? '',
pasalPph: pasalPph ?? '',
statusPph: statusPph ?? '',
penghasilanBruto: Number(penghasilanBruto ?? 0),
normaPenghasilanNeto: Number(normaPenghasilanNeto ?? 0),
tarif: Number(tarif ?? 0),
pphDipotong: Number(pphDipotong ?? 0),
kap: Number(kap ?? 0),
kjs: Number(kjs ?? 0),
dokReferensi: (() => {
const { namaDok, nomorDok, tglDok } = nrData;
// pastikan tidak undefined dan tanggal valid
if (!namaDok || !nomorDok || !tglDok) return [];
const parsedDate = dayjs(tglDok);
const tanggalFormatted = parsedDate.isValid() ? parsedDate.format('DDMMYYYY') : '';
if (!tanggalFormatted) return [];
return [
{
dokReferensi: namaDok,
nomorDokumen: nomorDok,
tanggal_Dokumen: tanggalFormatted,
},
];
})(),
metodePembayaranBendahara: metodePembayaranBendahara ?? '',
nomorSP2D: nomorSP2D ?? '',
tglPemotongan: tglPemotongan ? dayjs(tglPemotongan).format('DDMMYYYY') : '',
userId: userId ?? '',
kanal: kanal ?? '',
revNo,
glAccount: glAccount ?? '',
};
};
const useSaveNr = (props?: any) =>
useMutation({
mutationKey: ['Save-Nr'],
mutationFn: (params: any) => nrApi.saveNr(transformParams(params)),
...props,
});
export default useSaveNr;
// hooks/useUpload.ts // hooks/useUpload.ts
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import dnApi from '../utils/api'; import nrApi from '../utils/api';
const useUpload = (props?: any) => const useUpload = (props?: any) =>
useMutation({ useMutation({
mutationKey: ['upload-dn'], mutationKey: ['upload-nr'],
mutationFn: (payload: { id: string | number }) => dnApi.upload(payload), mutationFn: (payload: { id: string | number }) => nrApi.upload(payload),
...props, ...props,
}); });
......
export type TBaseResponseAPI<T> = {
status: string;
message: string;
data: T;
time: string;
code: number;
metaPage: TBaseResponseMetaPage;
total?: number;
};
type TBaseResponseMetaPage = {
pageNum: number | null;
rowPerPage: number | null;
totalRow: number;
};
export type TGetListDataTableNr = {
id: number;
npwpPemotong: string;
idTku: string;
masaPajak: string;
tahunPajak: string;
fgNpwpNik: string;
npwp: string;
nik: string;
nama: string;
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
kodeObjekPajak: string;
pasalPPh: string;
statusPPh: string;
dpp: string;
tarif: string;
pphDipotong: string;
kap: string;
kjs: string;
tglpemotongan: string;
userId: string;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
fgStatus: string;
internal_id: string;
dokumen_referensi: {
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string;
metodePembayaranBendahara: string;
nomorSP2D: string;
}[];
revNo: number;
noBupot: string;
idBupot: string;
npwpNikPenandatangan: string;
namaPenandatangan: string;
link: string | null;
errorMsg: string | null;
email: string | null;
glAccount: string;
fgkirimemail: string;
glName: string | null;
keterangan1: string | null;
keterangan2: string | null;
keterangan3: string | null;
keterangan4: string | null;
keterangan5: string | null;
};
export type TGetListDataTableNrResult = TGetListDataTableNr[];
export type TGetListDataKOPNr = {
kode: string;
nama: string;
pasal: string;
statuspph: string;
normanetto: string;
tarif: string;
kap: string;
kjs: string;
noCertificate: number;
certofDomicile: number;
otherCert: number;
};
export type TGetListDataKOPNrResult = TGetListDataKOPNr[];
export type ActionItem = {
title: string;
icon: React.ReactNode;
func?: () => void;
disabled?: boolean;
};
export type TDokReferensi = {
dokReferensi: string;
nomorDokumen: string;
tanggal_Dokumen: string; // format: DDMMYYYY
};
export type TPostNrRequest = {
id: string | null;
idBupot: string;
noBupot: string;
npwpPemotong: string;
idTku: string;
masaPajak: string;
tahunPajak: number;
tinDipotong: string;
namaDipotong: string;
alamatDipotong: string;
negaraDipotong: string;
tglLahirDipotong: string;
tmptLahirDipotong: string;
nomorPaspor: string;
nomorKitasKitap: string;
sertifikatInsentifDipotong: string;
nomorSertifikatInsentif: string;
kodeObjekPajak: string;
pasalPph: string;
statusPph: string;
penghasilanBruto: number;
normaPenghasilanNeto: number;
tarif: number;
pphDipotong: number;
kap: number;
kjs: number;
dokReferensi: TDokReferensi[];
metodePembayaranBendahara: string;
nomorSP2D: string;
tglPemotongan: string;
userId: string;
kanal: string;
revNo: number;
glAccount: string;
keterangan1: string | null;
keterangan2: string | null;
keterangan3: string | null;
keterangan4: string | null;
keterangan5: string | null;
};
export type TCountry = {
kode: string;
nama: string;
};
export type TCountryResult = TCountry[];
export type TPostUpload = {
id: string;
};
export type TDeleteNrRequest = {
id: string;
};
export type TCancelNrRequest = {
id: string | number;
tglPembatalan: string; // format: DDMMYYYY
};
export type TCancelNrResponse = TBaseResponseAPI<{
id: string | number;
statusBatal?: string;
message?: string;
}>;
import axios from 'axios'; import axios from 'axios';
import { import type {
TBaseResponseAPI, TBaseResponseAPI,
TCancelDnRequest, TCancelNrRequest,
TCancelDnResponse, TCancelNrResponse,
TDeleteDnRequest, TCountryResult,
TGetListDataKOPDnResult, TDeleteNrRequest,
TGetListDataTableDnResult, TGetListDataKOPNrResult,
TPostDnRequest, TGetListDataTableNrResult,
TPostUpload, TPostNrRequest,
} from '../types/types'; } from '../types/types';
import unifikasiClient from './unifikasiClient'; import unifikasiClient from './unifikasiClient';
const dnApi = () => {}; const nrApi = () => {};
const axiosCetakPdf = axios.create({ const axiosCetakPdf = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API_URL_CETAK, baseURL: import.meta.env.VITE_APP_BASE_API_URL_CETAK,
...@@ -22,11 +23,11 @@ const axiosCetakPdf = axios.create({ ...@@ -22,11 +23,11 @@ const axiosCetakPdf = axios.create({
}); });
// API untuk get list table // API untuk get list table
dnApi.getDn = async (config: any) => { nrApi.getNr = async (config: any) => {
const { const {
data: { status, message, metaPage, data }, data: { message, metaPage, data },
status: statusCode, status: statusCode,
} = await unifikasiClient.get<TBaseResponseAPI<TGetListDataTableDnResult>>('IF_TXR_028/bpu', { } = await unifikasiClient.get<TBaseResponseAPI<TGetListDataTableNrResult>>('IF_TXR_029/', {
...config, ...config,
}); });
...@@ -37,9 +38,24 @@ dnApi.getDn = async (config: any) => { ...@@ -37,9 +38,24 @@ dnApi.getDn = async (config: any) => {
return { total: metaPage ? Number(metaPage.totalRow) : 0, data }; return { total: metaPage ? Number(metaPage.totalRow) : 0, data };
}; };
dnApi.getKodeObjekPajakDn = async (params?: Record<string, any>) => { nrApi.getKodeObjekPajakNr = async (params?: Record<string, any>) => {
const response = await unifikasiClient.get<TBaseResponseAPI<TGetListDataKOPDnResult>>( const response = await unifikasiClient.get<TBaseResponseAPI<TGetListDataKOPNrResult>>(
'/sandbox/mst_kop_bpu', '/sandbox/mst_kop_bpnr',
{ params }
);
const body = response.data;
if (response.status !== 200 || body.status !== 'success') {
throw new Error(body.message);
}
return body;
};
nrApi.getCountryNr = async (params?: Record<string, any>) => {
const response = await unifikasiClient.get<TBaseResponseAPI<TCountryResult>>(
'/sandbox/mst_negara',
{ params } { params }
); );
...@@ -52,11 +68,10 @@ dnApi.getKodeObjekPajakDn = async (params?: Record<string, any>) => { ...@@ -52,11 +68,10 @@ dnApi.getKodeObjekPajakDn = async (params?: Record<string, any>) => {
return body; return body;
}; };
dnApi.saveDn = async (config: TPostDnRequest) => { nrApi.saveNr = async (config: TPostNrRequest) => {
const { const {
data: { status, message, data, code }, data: { message, data, code },
status: statusCode, } = await unifikasiClient.post<TBaseResponseAPI<TPostNrRequest>>('/IF_TXR_029/', {
} = await unifikasiClient.post<TBaseResponseAPI<TPostDnRequest>>('/IF_TXR_028/bpu', {
...config, ...config,
}); });
if (code === 0) { if (code === 0) {
...@@ -66,8 +81,8 @@ dnApi.saveDn = async (config: TPostDnRequest) => { ...@@ -66,8 +81,8 @@ dnApi.saveDn = async (config: TPostDnRequest) => {
return data; return data;
}; };
dnApi.getDnById = async (id: string) => { nrApi.getNrById = async (id: string) => {
const res = await unifikasiClient.get('/IF_TXR_028/bpu', { params: { id } }); const res = await unifikasiClient.get('/IF_TXR_029/', { params: { id } });
const { const {
data: { status, message, data }, data: { status, message, data },
...@@ -75,8 +90,8 @@ dnApi.getDnById = async (id: string) => { ...@@ -75,8 +90,8 @@ dnApi.getDnById = async (id: string) => {
} = res; } = res;
if (statusCode !== 200 || status?.toLowerCase() !== 'success') { if (statusCode !== 200 || status?.toLowerCase() !== 'success') {
console.error('getDnById failed:', { statusCode, status, message }); console.error('getNrId failed:', { statusCode, status, message });
throw new Error(message || 'Gagal mengambil data DN'); throw new Error(message || 'Gagal mengambil data NR');
} }
const dnData = Array.isArray(data) ? data[0] : data; const dnData = Array.isArray(data) ? data[0] : data;
...@@ -84,40 +99,39 @@ dnApi.getDnById = async (id: string) => { ...@@ -84,40 +99,39 @@ dnApi.getDnById = async (id: string) => {
return dnData; return dnData;
}; };
dnApi.upload = async ({ id }: { id: string | number }) => { nrApi.upload = async ({ id }: { id: string | number }) => {
const { const {
data: { status, message, data, code }, data: { status, message, data, code },
status: statusCode, status: statusCode,
} = await unifikasiClient.post('/IF_TXR_028/bpu/upload', { id }); } = await unifikasiClient.post('/IF_TXR_029/upload', { id });
return { status, message, data, code, statusCode }; return { status, message, data, code, statusCode };
}; };
dnApi.deleteDn = async (payload: TDeleteDnRequest, config?: Record<string, any>): Promise<any> => { nrApi.deleteNr = async (payload: TDeleteNrRequest, config?: Record<string, any>): Promise<any> => {
const { const {
data: { status, message, data }, data: { status, message, data },
status: statusCode, status: statusCode,
} = await unifikasiClient.post<TBaseResponseAPI<any>>('/IF_TXR_028/bpu/delete', payload, { } = await unifikasiClient.post<TBaseResponseAPI<any>>('/IF_TXR_029/delete', payload, {
...config, ...config,
}); });
if (statusCode !== 200 || status?.toLowerCase() === 'error') { if (statusCode !== 200 || status?.toLowerCase() === 'error') {
throw new Error(message || 'Gagal menghapus data DN'); throw new Error(message || 'Gagal menghapus data NR');
} }
return data; return data;
}; };
dnApi.cancel = async ({ id, tglPembatalan }: TCancelDnRequest): Promise<TCancelDnResponse> => { nrApi.cancel = async ({ id, tglPembatalan }: TCancelNrRequest): Promise<TCancelNrResponse> => {
const { const {
data: { status, message, data, code, time, metaPage, total }, data: { status, message, data, code, time, metaPage, total },
status: statusCode, } = await unifikasiClient.post('/IF_TXR_029/batal', {
} = await unifikasiClient.post('/IF_TXR_028/bpu/batal', {
id, id,
tglPembatalan, tglPembatalan,
}); });
console.log('Cancel DN response:', { code, message, status }); console.log('Cancel NR response:', { code, message, status });
if (code === 0) { if (code === 0) {
throw new Error(message || 'Gagal membatalkan data'); throw new Error(message || 'Gagal membatalkan data');
} }
...@@ -133,8 +147,8 @@ dnApi.cancel = async ({ id, tglPembatalan }: TCancelDnRequest): Promise<TCancelD ...@@ -133,8 +147,8 @@ dnApi.cancel = async ({ id, tglPembatalan }: TCancelDnRequest): Promise<TCancelD
}; };
}; };
dnApi.cetakPdfDetail = async (payload: Record<string, any>) => { nrApi.cetakPdfDetail = async (payload: Record<string, any>) => {
const response = await axiosCetakPdf.post('/report/ctas/bpu', payload); const response = await axiosCetakPdf.post('/report/ctas/bpnr', payload);
const body = response.data; const body = response.data;
...@@ -153,4 +167,4 @@ dnApi.cetakPdfDetail = async (payload: Record<string, any>) => { ...@@ -153,4 +167,4 @@ dnApi.cetakPdfDetail = async (payload: Record<string, any>) => {
return body; return body;
}; };
export default dnApi; export default nrApi;
...@@ -43,11 +43,11 @@ export const normalizePayloadCetakPdf = (payload: Record<string, any>) => { ...@@ -43,11 +43,11 @@ export const normalizePayloadCetakPdf = (payload: Record<string, any>) => {
adjusted.metodePembayaranBendahara = adjusted.metodePembayaranBendahara || '-'; adjusted.metodePembayaranBendahara = adjusted.metodePembayaranBendahara || '-';
adjusted.nomorSP2D = adjusted.nomorSP2D || '-'; adjusted.nomorSP2D = adjusted.nomorSP2D || '-';
adjusted.npwpDipotong = adjusted.npwp || ''; adjusted.npwpDipotong = adjusted.npwp || '';
adjusted.namaDipotong = adjusted.nama || ''; adjusted.namaDipotong = adjusted.namaDipotong || '';
adjusted.nitkuDipotong = adjusted.nik || ''; adjusted.nitkuDipotong = adjusted.nik || '';
adjusted.namaPemotong = adjusted.nama || ''; adjusted.namaPemotong = adjusted.namaDipotong || '';
adjusted.nitkuPemotong = adjusted.nik || ''; adjusted.nitkuPemotong = adjusted.idTku || '';
adjusted.penghasilanBruto = adjusted.dpp || ''; adjusted.penghasilanBruto = adjusted.penghasilanBruto || '';
adjusted.tanggal_Dokumen = adjusted.dokumen_referensi[0].tanggal_Dokumen; adjusted.tanggal_Dokumen = adjusted.dokumen_referensi[0].tanggal_Dokumen;
adjusted.status = 'Proforma'; adjusted.status = 'Proforma';
adjusted.msPajak = adjusted.masaPajak; adjusted.msPajak = adjusted.masaPajak;
......
...@@ -29,18 +29,20 @@ import { ...@@ -29,18 +29,20 @@ import {
// import { CustomToolbar } from '../components/CustomToolbar'; // import { CustomToolbar } from '../components/CustomToolbar';
import { formatRupiah } from 'src/shared/FormatRupiah/FormatRupiah'; import { formatRupiah } from 'src/shared/FormatRupiah/FormatRupiah';
import { FG_STATUS_DN } from '../constant'; import { FG_STATUS_DN } from '../constant';
import ModalDeleteDn from '../components/dialog/ModalDeleteDn'; import ModalDeleteNr from '../components/dialog/ModalDeleteNr';
import ModalUploadDn from '../components/dialog/ModalUploadDn'; import ModalUploadNr from '../components/dialog/ModalUploadNr';
import ModalCancelDn from '../components/dialog/ModalCancelDn'; import ModalCancelNr from '../components/dialog/ModalCancelNr';
import ModalCetakPdfDn from '../components/dialog/ModalCetakPdfDn'; import ModalCetakPdfNr from '../components/dialog/ModalCetakPdfNr';
import useGetDn from '../hooks/useGetDn'; import { useGetNr } from '../hooks/useGetNr';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { usePaginationStore } from '../store/paginationStore'; import { usePaginationStore } from '../store/paginationStore';
import StatusChip from '../components/StatusChip'; import StatusChip from '../components/StatusChip';
import { useDebounce, useThrottle } from 'src/shared/hooks/useDebounceThrottle'; import { useDebounce, useThrottle } from 'src/shared/hooks/useDebounceThrottle';
import useGetKodeObjekPajak from '../hooks/useGetKodeObjekPajak'; import useGetKodeObjekPajak from '../hooks/useGetKodeObjekPajakNr';
import useAdvancedFilter from '../hooks/useAdvancedFilterDn'; import useAdvancedFilter from '../hooks/useAdvancedFilterNr';
import { CustomToolbar } from '../components/CustomToolbar'; import { CustomToolbar } from '../components/CustomToolbar';
import { useSelector } from 'react-redux';
import type { RootState } from 'src/store';
export type IColumnGrid = GridColDef & { export type IColumnGrid = GridColDef & {
field: field:
...@@ -48,13 +50,14 @@ export type IColumnGrid = GridColDef & { ...@@ -48,13 +50,14 @@ export type IColumnGrid = GridColDef & {
| 'noBupot' | 'noBupot'
| 'masaPajak' | 'masaPajak'
| 'tahunPajak' | 'tahunPajak'
| 'kdObjPjk' | 'kodeObjekPajak'
| 'pasalPPh' | 'pasalPPh'
| 'npwp' | 'npwpPemotong'
| 'nama' | 'namaDipotong'
| 'dpp' | 'penghasilanBruto'
| 'pphDipotong' | 'pphDipotong'
| 'idTku' | 'idTku'
| 'negaraDipotong'
| 'dokReferensi' | 'dokReferensi'
| 'nomorDokumen' | 'nomorDokumen'
| 'created_by' | 'created_by'
...@@ -75,13 +78,14 @@ type TKodeObjekPajak = { ...@@ -75,13 +78,14 @@ type TKodeObjekPajak = {
nama: string; nama: string;
pasal: string; pasal: string;
statuspph: string; statuspph: string;
normanetto: string;
}; };
export function NrListView() { export function NrListView() {
const apiRef = useGridApiRef(); const apiRef = useGridApiRef();
const navigate = useNavigate(); const navigate = useNavigate();
const tableKey = 'dn'; const tableKey = 'nr';
const page = usePaginationStore((s) => s.tables[tableKey]?.page ?? 0); const page = usePaginationStore((s) => s.tables[tableKey]?.page ?? 0);
const pageSize = usePaginationStore((s) => s.tables[tableKey]?.pageSize ?? 10); const pageSize = usePaginationStore((s) => s.tables[tableKey]?.pageSize ?? 10);
...@@ -104,7 +108,8 @@ export function NrListView() { ...@@ -104,7 +108,8 @@ export function NrListView() {
const [selectionVersion, setSelectionVersion] = useState(0); const [selectionVersion, setSelectionVersion] = useState(0);
const [kodeObjekPajaks, setKodeObjekPajaks] = useState<TKodeObjekPajak[]>([]); const [kodeObjekPajaks, setKodeObjekPajaks] = useState<TKodeObjekPajak[]>([]);
const { data: kodeObjekPajak, isLoading: isLoadingKop } = useGetKodeObjekPajak(); const { data: kodeObjekPajak } = useGetKodeObjekPajak();
const signer = useSelector((state: RootState) => state.user.data.signer);
const { buildAdvancedFilter, buildRequestParams } = useAdvancedFilter(); const { buildAdvancedFilter, buildRequestParams } = useAdvancedFilter();
...@@ -127,7 +132,7 @@ export function NrListView() { ...@@ -127,7 +132,7 @@ export function NrListView() {
return buildRequestParams(baseParams, advanced); return buildRequestParams(baseParams, advanced);
}, [page, pageSize, sortModel, filterModel.items, buildAdvancedFilter, buildRequestParams]); }, [page, pageSize, sortModel, filterModel.items, buildAdvancedFilter, buildRequestParams]);
const { data, isFetching, refetch } = useGetDn({ const { data, isFetching, refetch } = useGetNr({
params, params,
}); });
const idStatusMapRef = useRef<Map<string | number, string>>(new Map()); const idStatusMapRef = useRef<Map<string | number, string>>(new Map());
...@@ -174,6 +179,7 @@ export function NrListView() { ...@@ -174,6 +179,7 @@ export function NrListView() {
// ---------- status options and columns (kept identical to your original) ---------- // ---------- status options and columns (kept identical to your original) ----------
type Status = 'draft' | 'normal' | 'cancelled' | 'amended'; type Status = 'draft' | 'normal' | 'cancelled' | 'amended';
type StatusOption = { value: Status; label: string }; type StatusOption = { value: Status; label: string };
// eslint-disable-next-line react-hooks/exhaustive-deps
const statusOptions: StatusOption[] = [ const statusOptions: StatusOption[] = [
{ value: 'draft', label: 'Draft' }, { value: 'draft', label: 'Draft' },
{ value: 'normal', label: 'Normal' }, { value: 'normal', label: 'Normal' },
...@@ -198,14 +204,24 @@ export function NrListView() { ...@@ -198,14 +204,24 @@ export function NrListView() {
{ field: 'noBupot', headerName: 'Nomor Bukti Pemotongan', width: 200 }, { field: 'noBupot', headerName: 'Nomor Bukti Pemotongan', width: 200 },
{ field: 'masaPajak', headerName: 'Masa Pajak', width: 150 }, { field: 'masaPajak', headerName: 'Masa Pajak', width: 150 },
{ field: 'tahunPajak', headerName: 'Tahun Pajak', width: 150 }, { field: 'tahunPajak', headerName: 'Tahun Pajak', width: 150 },
{ field: 'kdObjPjk', headerName: 'Kode Objek Pajak', width: 150 }, { field: 'kodeObjekPajak', headerName: 'Kode Objek Pajak', width: 150 },
{ field: 'npwp', headerName: 'Identitas', width: 150 }, { field: 'npwpPemotong', headerName: 'Identitas', width: 150 },
{ field: 'nama', headerName: 'Nama', width: 150 }, { field: 'namaDipotong', headerName: 'Nama', width: 150 },
{ {
field: 'dpp', field: 'negaraDipotong',
headerName: 'Negara',
width: 180,
renderCell: ({ row }) => {
const kode = row.negaraDipotong || '-';
const nama = row.namaNegara || '';
return `${kode}${nama ? ' - ' + nama : ''}`;
},
},
{
field: 'penghasilanBruto',
headerName: 'Jumlah Penghasilan Bruto (Rp)', headerName: 'Jumlah Penghasilan Bruto (Rp)',
width: 150, width: 150,
renderCell: ({ row }) => formatRupiah(row.dpp), renderCell: ({ row }) => formatRupiah(row.penghasilanBruto),
}, },
{ {
field: 'pphDipotong', field: 'pphDipotong',
...@@ -217,7 +233,7 @@ export function NrListView() { ...@@ -217,7 +233,7 @@ export function NrListView() {
{ field: 'dokReferensi', headerName: 'Nama dokumen', width: 150 }, { field: 'dokReferensi', headerName: 'Nama dokumen', width: 150 },
{ field: 'nomorDokumen', headerName: 'Nomor dokumen', width: 150 }, { field: 'nomorDokumen', headerName: 'Nomor dokumen', width: 150 },
{ field: 'created_by', headerName: 'Created', width: 150 }, { field: 'created_by', headerName: 'Created', width: 150 },
{ field: 'created_at', headerName: 'Created At', width: 200 }, { field: 'created_at', headerName: 'Created At', width: 150 },
{ field: 'updated_by', headerName: 'Updated', width: 150 }, { field: 'updated_by', headerName: 'Updated', width: 150 },
{ field: 'updated_at', headerName: 'Update At', width: 150 }, { field: 'updated_at', headerName: 'Update At', width: 150 },
{ field: 'internal_id', headerName: 'Referensi', width: 150 }, { field: 'internal_id', headerName: 'Referensi', width: 150 },
...@@ -243,7 +259,7 @@ export function NrListView() { ...@@ -243,7 +259,7 @@ export function NrListView() {
(type = 'ubah') => { (type = 'ubah') => {
const selectedRow = dataSelectedRef.current[0]; const selectedRow = dataSelectedRef.current[0];
if (!selectedRow) return; if (!selectedRow) return;
navigate(`/unifikasi/dn/${selectedRow.id}/${type}`); navigate(`/unifikasi/nr/${selectedRow.id}/${type}`);
}, },
[navigate] [navigate]
); );
...@@ -305,6 +321,7 @@ export function NrListView() { ...@@ -305,6 +321,7 @@ export function NrListView() {
canReplacement: count === 1 && dataSelected[0].fgStatus === FG_STATUS_DN.NORMAL_DONE, canReplacement: count === 1 && dataSelected[0].fgStatus === FG_STATUS_DN.NORMAL_DONE,
canCancel: hasSelection && allNormal, canCancel: hasSelection && allNormal,
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectionVersion]); }, [selectionVersion]);
useEffect(() => { useEffect(() => {
...@@ -321,9 +338,13 @@ export function NrListView() { ...@@ -321,9 +338,13 @@ export function NrListView() {
return; return;
} }
console.log('🧾 selectedRow:', selectedRow);
console.log('🧩 Keys:', Object.keys(selectedRow));
const kode = selectedRow.kodeObjekPajak || selectedRow.kdObjPjk; const kode = selectedRow.kodeObjekPajak || selectedRow.kdObjPjk;
const detailKop = kodeObjekPajaks.find((item) => item.kode === kode); const detailKop = kodeObjekPajaks.find((item) => item.kode === kode);
console.log(detailKop);
const mergedRow = { const mergedRow = {
...selectedRow, ...selectedRow,
...(detailKop ...(detailKop
...@@ -331,8 +352,11 @@ export function NrListView() { ...@@ -331,8 +352,11 @@ export function NrListView() {
namaObjekPajak: detailKop.nama, namaObjekPajak: detailKop.nama,
pasalPPh: detailKop.pasal, pasalPPh: detailKop.pasal,
statusPPh: detailKop.statuspph, statusPPh: detailKop.statuspph,
normaPenghasilanNeto: detailKop.normanetto,
} }
: {}), : {}),
tinDipotong: selectedRow.npwpPemotong ?? '',
namaPenandatangan: signer,
}; };
setPreviewPayload(mergedRow); setPreviewPayload(mergedRow);
...@@ -391,6 +415,7 @@ export function NrListView() { ...@@ -391,6 +415,7 @@ export function NrListView() {
}, },
], ],
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps
[validatedActions, refetch, handleEditData] [validatedActions, refetch, handleEditData]
); );
...@@ -416,20 +441,25 @@ export function NrListView() { ...@@ -416,20 +441,25 @@ export function NrListView() {
const api = apiRef.current; const api = apiRef.current;
if (!api) return; if (!api) return;
const id = window.setTimeout(() => { const id = window.setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const selected = getSelectedRowByKey('all'); const selected = getSelectedRowByKey('all');
}, 100); }, 100);
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
return () => clearTimeout(id); return () => clearTimeout(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiRef, selectionVersion]); }, [apiRef, selectionVersion]);
return ( return (
<> <>
<DashboardContent> <DashboardContent>
<CustomBreadcrumbs <CustomBreadcrumbs
heading="Bupot Unifikasi" heading="Bupot Unifikasi Non Residen"
links={[{ name: 'Dashboard', href: paths.dashboard.root }, { name: 'e-Bupot Unifikasi' }]} links={[
{ name: 'Dashboard', href: paths.dashboard.root },
{ name: 'e-Bupot Unifikasi Non Residen' },
]}
action={ action={
<Button component={RouterLink} href={paths.unifikasi.dnNew} variant="contained"> <Button component={RouterLink} href={paths.unifikasi.nrNew} variant="contained">
Rekam Data Rekam Data
</Button> </Button>
} }
...@@ -487,7 +517,7 @@ export function NrListView() { ...@@ -487,7 +517,7 @@ export function NrListView() {
</DashboardContent> </DashboardContent>
{isDeleteModalOpen && ( {isDeleteModalOpen && (
<ModalDeleteDn <ModalDeleteNr
dataSelected={rowSelectionModel} dataSelected={rowSelectionModel}
setSelectionModel={setRowSelectionModel} setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef} tableApiRef={apiRef}
...@@ -498,7 +528,7 @@ export function NrListView() { ...@@ -498,7 +528,7 @@ export function NrListView() {
)} )}
{isUploadModalOpen && ( {isUploadModalOpen && (
<ModalUploadDn <ModalUploadNr
dataSelected={rowSelectionModel} dataSelected={rowSelectionModel}
setSelectionModel={setRowSelectionModel} setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef} tableApiRef={apiRef}
...@@ -509,7 +539,7 @@ export function NrListView() { ...@@ -509,7 +539,7 @@ export function NrListView() {
)} )}
{isCancelModalOpen && ( {isCancelModalOpen && (
<ModalCancelDn <ModalCancelNr
dataSelected={dataSelectedRef.current} dataSelected={dataSelectedRef.current}
setSelectionModel={setRowSelectionModel} setSelectionModel={setRowSelectionModel}
tableApiRef={apiRef} tableApiRef={apiRef}
...@@ -520,7 +550,7 @@ export function NrListView() { ...@@ -520,7 +550,7 @@ export function NrListView() {
)} )}
{isPreviewOpen && ( {isPreviewOpen && (
<ModalCetakPdfDn <ModalCetakPdfNr
payload={previewPayload} payload={previewPayload}
isOpen={isPreviewOpen} isOpen={isPreviewOpen}
onClose={() => { onClose={() => {
......
// src/workers/normalizeDn.worker.js
// NOTE: keep this file plain JS - no TS imports - copy needed transform functions here.
function formatDateToDDMMYYYY(dateString) {
if (!dateString) return '';
const d = new Date(dateString);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
}
// minimal transform helpers used in normalize
function transformFgStatusToFgSignStatus(fgStatus) {
const splitted = (fgStatus || '').split('-') || [];
if (splitted.includes('SIGN') > 0) return 'FAILED';
if (splitted.includes('SIGNING IN PROGRESS')) return 'IN_PROGRESS';
switch (splitted[1]) {
case 'document signed successfully':
case 'Done':
return 'SIGNED';
default:
return null;
}
}
function getFgStatusPdf(link, fgSignStatus) {
if (!link || fgSignStatus === 'IN_PROGRESS') return 'TIDAK_TERSEDIA';
if (!link.includes('https://coretaxdjp.pajak.go.id/')) return 'BELUM_TERBENTUK';
return 'TERBENTUK';
}
function normalisePropsGetDn(params) {
if (!params) return params;
return {
...params,
nomorSP2D: params.dokumen_referensi?.[0]?.nomorSP2D || '',
metodePembayaranBendahara: params.dokumen_referensi?.[0]?.metodePembayaranBendahara || '',
dokReferensi: params.dokumen_referensi?.[0]?.dokReferensi || '',
nomorDokumen: params.dokumen_referensi?.[0]?.nomorDokumen || '',
id: params.id,
internal_id: params.internal_id,
fgStatus: params.fgStatus,
fgSignStatus: transformFgStatusToFgSignStatus(params.fgStatus),
fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)),
created_at: formatDateToDDMMYYYY(params.created_at),
updated_at: formatDateToDDMMYYYY(params.updated_at),
};
}
// eslint-disable-next-line func-names
onmessage = function (e) {
const { data } = e;
// data should be array of items
if (!Array.isArray(data)) {
postMessage({ error: 'expected array' });
return;
}
try {
const out = data.map(normalisePropsGetDn);
postMessage({ data: out });
} catch (err) {
postMessage({ error: (err && err.message) || String(err) });
}
};
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