Commit 85c95a86 authored by Fachri's avatar Fachri

push

parent ec8a79c2
Pipeline #1016 failed with stages
import { forwardRef, useState } from 'react';
import { NumericFormat } from 'react-number-format';
const NumberFormatRupiah = forwardRef(function NumberFormatRupiah(props, ref) {
const { onChange, maxValue, minValue, maxLength, minLength, allowDecimalValue, ...other } = props;
const [newValue, setNewValue] = useState(props.value);
const key = (e2) => {
const kode = e2.charCode;
if (kode === 45 && (newValue?.toString().includes('(') || newValue?.toString().includes(')'))) {
setNewValue(`${props.value}`);
} else if (
kode === 45 &&
!(newValue?.toString().includes('(') || newValue?.toString().includes(')'))
) {
setNewValue(`(${props.value})`);
}
};
return (
<NumericFormat
{...other}
isNumericString
onKeyPress={key}
thousandSeparator="."
decimalSeparator=","
decimalScale={allowDecimalValue ? 2 : 0}
getInputRef={ref}
allowNegative={false}
isAllowed={(values) => {
const { floatValue, value } = values;
if (floatValue > 999999999999) return false;
if (value.includes(',') && value.split(',')[1] && value.split(',')[1].length > 2) {
return false;
}
const integerPart = value.split(',')[0].replace(/^0+/, '');
const integerDigits = integerPart.length;
if (maxLength !== undefined && integerDigits > maxLength) return false;
if (minLength !== undefined && integerDigits < minLength) return false;
if (floatValue !== undefined) {
if (maxValue !== undefined && floatValue > maxValue) return false;
if (minValue !== undefined && floatValue < minValue) return false;
}
return true;
}}
onValueChange={(values) => {
onChange({
target: {
name: props.name,
value: values.value,
},
});
}}
/>
);
});
NumberFormatRupiah.defaultProps = {
maxValue: undefined,
minValue: undefined,
};
export default NumberFormatRupiah;
export { default } from './NumberFormatRupiah'; // default export
export { default as NumberFormatRupiah } from './NumberFormatRupiah'; // named export juga kalau perlu
import { isNaN } from 'lodash';
import { forwardRef, useState, useEffect } from 'react';
// import { NumberFormat } from 'react-number-format';
import { NumericFormat } from 'react-number-format';
const formatNegativeValue = (value, negativeMask) => {
if (!value || value === '0') return value;
const numValue = parseFloat(value);
if (numValue >= 0) return value;
const formattedAbsValue = new Intl.NumberFormat('id-ID', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(Math.abs(numValue));
switch (negativeMask) {
case 'prefix':
return `-${formattedAbsValue}`;
case 'suffix':
return `${formattedAbsValue}-`;
case 'both':
return `(${formattedAbsValue})`;
default:
return `-${formattedAbsValue}`;
}
};
export const getIntegerDigitCount = (value) => {
if (!value) return 0;
// Convert to string and remove all non-digit characters
const cleanValue = value.toString().replace(/[^\d,.-]/g, '');
// Split by decimal separator and get the integer part
const integerPart = cleanValue.split(',')[0];
// Remove negative sign if present and count remaining digits
return integerPart.replace(/[-]/g, '').length;
};
const NumberFormatRupiahWithAllowedNegative = forwardRef(
function NumberFormatRupiahWithAllowedNegative(props, ref) {
const {
onChange,
allowNegativeValue = false,
allowDecimalValue = false,
negativeMask = 'prefix',
value: controlledValue,
maxValue,
minValue,
maxLength,
minLength,
...other
} = props;
const [isNegative, setIsNegative] = useState(false);
useEffect(() => {
if (allowNegativeValue && controlledValue) {
const numValue = parseFloat(controlledValue.toString().replace(/[^\d.-]/g, ''));
setIsNegative(numValue < 0);
}
}, [controlledValue, allowNegativeValue]);
const handleKeyPress = (e) => {
if (!allowNegativeValue) return;
const kode = e.charCode;
if (kode === 45) {
const newIsNegative = !isNegative;
setIsNegative(newIsNegative);
const currentValue = props.value || '0';
const numValue = parseFloat(currentValue.toString().replace(/[^\d.-]/g, ''));
if (numValue !== 0) {
const newValue = newIsNegative ? -Math.abs(numValue) : Math.abs(numValue);
if (
(maxValue === undefined || newValue <= maxValue) &&
(minValue === undefined || newValue >= minValue)
) {
onChange({
target: {
name: props.name,
value: newValue.toString(),
},
});
} else {
setIsNegative(!newIsNegative);
}
}
}
};
return (
<NumericFormat
{...other}
value={controlledValue}
isNumericString
onKeyPress={handleKeyPress}
thousandSeparator="."
decimalSeparator=","
decimalScale={allowDecimalValue ? 2 : 0}
getInputRef={ref}
allowNegative={allowNegativeValue}
isAllowed={(values) => {
const { floatValue, value } = values;
if (floatValue === undefined) return true;
// Check maximum value
if (Math.abs(floatValue) > 999999999999) {
return false;
}
// Specific handling for decimal input
if (value.includes(',') && value.split(',')[1] && value.split(',')[1].length > 2) {
return false;
}
// Get integer digit count (excluding leading zeros and decimal places)
const integerPart = value.split(',')[0].replace(/^0+/, '');
const integerDigits = integerPart.length;
// Length validations for non-zero integer part
if (maxLength !== undefined && integerDigits > maxLength) {
return false;
}
if (minLength !== undefined && integerDigits < minLength) {
return false;
}
// Min/Max validations (only when float value is complete)
if (floatValue !== undefined) {
if (maxValue !== undefined && floatValue > maxValue) {
return false;
}
if (minValue !== undefined && floatValue < minValue) {
return false;
}
}
return true;
}}
format={(value) => {
if (!value) return value;
const numValue = parseFloat(value.toString().replace(/[^\d.-]/g, ''));
if (isNaN(numValue)) return value;
const formatted = new Intl.NumberFormat('id-ID', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(Math.abs(numValue));
if (numValue < 0 || isNegative) {
switch (negativeMask) {
case 'prefix':
return `-${formatted}`;
case 'suffix':
return `${formatted}-`;
case 'both':
return `(${formatted})`;
default:
return `-${formatted}`;
}
}
return formatted;
}}
onValueChange={(values) => {
let finalValue = values.value;
if (allowNegativeValue && finalValue && finalValue !== '0') {
const numValue = parseFloat(finalValue.replace(/[^\d.-]/g, ''));
if (!isNaN(numValue)) {
const shouldBeNegative = numValue < 0 || isNegative;
const potentialValue = shouldBeNegative ? -Math.abs(numValue) : Math.abs(numValue);
if (
(maxValue === undefined || potentialValue <= maxValue) &&
(minValue === undefined || potentialValue >= minValue)
) {
setIsNegative(shouldBeNegative);
finalValue = shouldBeNegative
? `-${Math.abs(numValue)}`
: Math.abs(numValue).toString();
} else {
return;
}
}
}
onChange({
target: {
name: props.name,
value: finalValue,
},
});
}}
/>
);
}
);
export default NumberFormatRupiahWithAllowedNegative;
import { Controller, useFormContext } from 'react-hook-form';
import { TextField } from '@mui/material';
import NumberFormatRupiahWithAllowedNegative from '../NumberFormatRupiahWithAllowedNegative/NumberFormatRupiahWithAllowedNegative';
import NumberFormatRupiah from '../NumberFormatRupiah';
type RHFNumericProps = {
name: string;
label: string;
allowNegativeValue?: boolean;
allowDecimalValue?: boolean;
maxValue?: number;
minValue?: number;
readOnly?: boolean;
[key: string]: any;
};
export function RHFNumeric({
name,
label,
allowNegativeValue = false,
allowDecimalValue = false,
maxValue,
minValue,
readOnly,
...props
}: RHFNumericProps) {
const { control } = useFormContext();
const handleValueChange = (value: string) => {
const numericValue = Number(value.replace(/[^\d.-]/g, ''));
let finalValue = value;
if (!isNaN(numericValue)) {
if (maxValue !== undefined && numericValue > maxValue) {
finalValue = maxValue.toString();
}
if (minValue !== undefined && numericValue < minValue) {
finalValue = minValue.toString();
}
}
return finalValue;
};
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
label={label}
fullWidth
variant="outlined"
value={field.value ?? ''}
disabled={readOnly}
onChange={(e) => {
const constrainedValue = handleValueChange(e.target.value);
// kalau mau number -> field.onChange(Number(constrainedValue));
field.onChange(constrainedValue);
}}
InputProps={{
inputComponent: !allowNegativeValue
? NumberFormatRupiah
: NumberFormatRupiahWithAllowedNegative,
readOnly,
inputProps: {
allowNegativeValue,
allowDecimalValue,
maxValue,
minValue,
},
...props.InputProps,
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
sx={{
input: {
textAlign: 'right',
...(readOnly && {
backgroundColor: '#f6f6f6',
color: '#1C252E',
WebkitTextFillColor: '#1C252E',
}),
},
...(readOnly && {
'& .MuiInputLabel-root': {
color: '#1C252E',
},
'& .Mui-disabled': {
WebkitTextFillColor: '#1C252E',
color: '#1C252E',
opacity: 1,
backgroundColor: '#f6f6f6',
},
}),
}}
{...props}
/>
)}
/>
);
}
......@@ -5,9 +5,14 @@ import React, { useEffect, useMemo } from 'react';
import { Field } from 'src/components/hook-form';
import { TGetListDataKOPDn } from '../../types/types';
import { useFormContext } from 'react-hook-form';
import { FG_FASILITAS_DN, FG_FASILITAS_MASTER_KEY, FG_FASILITAS_TEXT } from '../../constant';
import FieldNumberText from 'src/shared/components/FieldNumberText ';
import usePphDipotong from '../../hooks/usePphDipotong';
import {
FG_FASILITAS_DN,
FG_FASILITAS_MASTER_KEY,
FG_FASILITAS_TEXT,
TARIF_0,
} from '../../constant';
import { NumericFormat } from 'react-number-format';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
type PPHDipotongProps = {
kodeObjectPajak: TGetListDataKOPDn[];
......@@ -18,47 +23,56 @@ const PphDipotong = ({ kodeObjectPajak }: PPHDipotongProps) => {
const selectedKode = watch('kdObjPjk');
const fgFasilitas = watch('fgFasilitas');
const jmlBruto = Number(watch('jmlBruto') || 0);
const tarif = Number(watch('tarif') || 0);
const kodeObjekPajakSelected = useMemo(
() => kodeObjectPajak.find((item) => item.kode === selectedKode),
[kodeObjectPajak, selectedKode]
);
// Hook otomatis hitung tarif & pphDipotong
usePphDipotong(kodeObjekPajakSelected);
// Fasilitas options
const fgFasilitasOptions = useMemo(
() =>
Object.entries(FG_FASILITAS_DN).map(([_, value]) => ({
value,
label: FG_FASILITAS_TEXT[value],
})),
[]
);
const fasilitasOptions = useMemo(
() =>
fgFasilitasOptions.filter(
(opt) =>
kodeObjekPajakSelected &&
kodeObjekPajakSelected[FG_FASILITAS_MASTER_KEY[opt.value] as keyof TGetListDataKOPDn] ===
1
),
[fgFasilitasOptions, kodeObjekPajakSelected]
);
// Hitung PPh dipotong
const pphDipotong = useMemo(() => {
if (!fgFasilitas) return 0;
if (TARIF_0.includes(fgFasilitas)) return 0;
return (jmlBruto * tarif) / 100;
}, [fgFasilitas, jmlBruto, tarif]);
// Reset fasilitas jika kode objek pajak berubah
// Reset tarif saat kode objek pajak berubah
useEffect(() => {
setValue('fgFasilitas', '', { shouldValidate: true });
}, [selectedKode, setValue]);
if (!selectedKode || !kodeObjekPajakSelected) return;
if (fgFasilitas !== FG_FASILITAS_DN.FASILITAS_LAINNYA) {
setValue('tarif', Number(kodeObjekPajakSelected.tarif) || 0);
}
}, [selectedKode, kodeObjekPajakSelected, fgFasilitas, setValue]);
console.log(selectedKode);
console.log(kodeObjekPajakSelected);
// console.log(kodeObjectPajak);
// Reset tarif saat fasilitas berubah
useEffect(() => {
if (!fgFasilitas) return;
if (fgFasilitas === FG_FASILITAS_DN.FASILITAS_LAINNYA) {
setValue('tarif', 0);
setValue('noDokLainnya', '');
} else if (kodeObjekPajakSelected) {
setValue('tarif', Number(kodeObjekPajakSelected.tarif) || 0);
setValue('noDokLainnya', '');
}
}, [fgFasilitas, kodeObjekPajakSelected, setValue]);
// Opsi fasilitas
const fasilitasOptions = useMemo(() => {
if (!kodeObjekPajakSelected) return [];
return Object.values(FG_FASILITAS_DN)
.map((v) => ({ value: v, label: FG_FASILITAS_TEXT[v] }))
.filter(
(opt) =>
kodeObjekPajakSelected[FG_FASILITAS_MASTER_KEY[opt.value] as keyof TGetListDataKOPDn] ===
1
);
}, [kodeObjekPajakSelected]);
return (
<Grid container rowSpacing={2} columnSpacing={2}>
{/* Kode objek pajak */}
<Grid sx={{ mt: 3 }} size={{ md: 6 }}>
<Field.Select name="kdObjPjk" label="Kode Objek Pajak">
{kodeObjectPajak.map((item) => (
......@@ -69,14 +83,22 @@ const PphDipotong = ({ kodeObjectPajak }: PPHDipotongProps) => {
</Field.Select>
</Grid>
{/* Divider */}
<Grid size={{ md: 12 }}>
<Divider sx={{ fontWeight: 'bold' }} textAlign="left">
Fasilitas Pajak Penghasilan
</Divider>
</Grid>
{/* Fasilitas */}
<Grid size={{ md: 6 }}>
<Field.Select name="fgFasilitas" label="Fasilitas">
<Field.Select
name="fgFasilitas"
label="Fasilitas"
// onChange={() => {
// setValue('noDokLainnya', '');
// }}
>
{fasilitasOptions.length === 0 ? (
<MenuItem disabled value="">
No options
......@@ -91,54 +113,54 @@ const PphDipotong = ({ kodeObjectPajak }: PPHDipotongProps) => {
</Field.Select>
</Grid>
{/* Dokumen lainnya */}
<Grid size={{ md: 6 }}>
<Field.Text
name="noDokLainnya"
label="Nomor Dokumen Lainnya"
disabled={['9', ''].includes(fgFasilitas)}
sx={{
'& .MuiInputBase-root.Mui-disabled': {
backgroundColor: '#f6f6f6',
},
}}
sx={{ '& .MuiInputBase-root.Mui-disabled': { backgroundColor: '#f6f6f6' } }}
/>
</Grid>
{/* Jumlah bruto */}
<Grid size={{ md: 4 }}>
<FieldNumberText name="jmlBruto" label="Jumlah Penghasilan Bruto (Rp)" />
<RHFNumeric
name="jmlBruto"
label="Jumlah Penghasilan Bruto (Rp)"
allowNegativeValue={false}
allowDecimalValue={false}
/>
</Grid>
{/* Tarif */}
<Grid size={{ md: 4 }}>
<Field.Text
<RHFNumeric
name="tarif"
label="Tarif (%)"
type="number"
value={kodeObjekPajakSelected?.tarif || ''}
slotProps={{
input: {
readOnly: ![FG_FASILITAS_DN.SKD_WPLN, FG_FASILITAS_DN.FASILITAS_LAINNYA].includes(
fgFasilitas
),
style: {
backgroundColor: ![
FG_FASILITAS_DN.SKD_WPLN,
FG_FASILITAS_DN.FASILITAS_LAINNYA,
].includes(fgFasilitas)
? '#f6f6f6'
: undefined,
},
},
}}
allowDecimalValue
maxValue={100}
readOnly={fgFasilitas !== FG_FASILITAS_DN.FASILITAS_LAINNYA}
disabled={fgFasilitas !== FG_FASILITAS_DN.FASILITAS_LAINNYA}
/>
</Grid>
{/* PPh dipotong */}
<Grid size={{ md: 4 }}>
<Field.Text
name="pphDipotong"
label="PPh Yang Dipotong/Dipungut"
type="number"
value={pphDipotong}
InputLabelProps={{ shrink: true }}
slotProps={{
input: {
inputComponent: NumericFormat as any,
inputProps: {
thousandSeparator: '.',
decimalSeparator: ',',
allowNegative: false,
valueIsNumericString: true,
},
readOnly: true,
style: { backgroundColor: '#f6f6f6' },
},
......
......@@ -32,6 +32,7 @@ const bpuSchema = z.object({
.nonempty('NPWP wajib diisi')
.regex(/^\d{16}$/, 'NPWP harus 16 digit'),
email: z.string().email({ message: 'Email tidak valid' }).optional(),
noDokLainnya: z.string().nonempty('No Dokumen Lainnya wajib diisi'),
// bisa tambah field lain sesuai kebutuhan
keterangan1: z.string().optional(),
keterangan2: z.string().optional(),
......
......@@ -7688,6 +7688,11 @@ react-markdown@^10.1.0:
unist-util-visit "^5.0.0"
vfile "^6.0.0"
react-number-format@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-5.4.4.tgz#d31f0e260609431500c8d3f81bbd3ae1fb7cacad"
integrity sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==
react-organizational-chart@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-organizational-chart/-/react-organizational-chart-2.2.1.tgz#876641081303349f611d3a24a9488bff38fba677"
......
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