Commit ae5a1461 authored by Fachri's avatar Fachri

update : add faktur PK

parent 0c51fca2
......@@ -6,7 +6,7 @@ module.exports = {
args: '--spa',
env: {
PM2_SERVE_PATH: 'dist',
PM2_SERVE_PORT: 9021,
PM2_SERVE_PORT: 9030,
PM2_SERVE_SPA: true,
},
},
......
<svg width="450" height="446" viewBox="0 0 450 446" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M230 45.5C329.41 45.5 410 126.09 410 225.5C410 324.91 329.41 405.5 230 405.5H65L110.45 360.05C73.37 327.08 50 279.02 50 225.5C50 126.09 130.59 45.5 230 45.5Z" fill="#9BC9FF"/>
<path d="M25 340.5C36.05 340.5 45 349.45 45 360.5C45 371.55 36.05 380.5 25 380.5C13.95 380.5 5 371.55 5 360.5C5 349.45 13.95 340.5 25 340.5Z" fill="#9BC9FF"/>
<path d="M415 75.5C431.569 75.5 445 62.0685 445 45.5C445 28.9315 431.569 15.5 415 15.5C398.431 15.5 385 28.9315 385 45.5C385 62.0685 398.431 75.5 415 75.5Z" fill="#9BC9FF"/>
<path d="M260 220.5V305.5C260 322.07 246.57 335.5 230 335.5C213.43 335.5 200 322.07 200 305.5V220.5C200 203.93 213.43 190.5 230 190.5C238.28 190.5 245.78 193.86 251.21 199.29C256.641 204.72 260 212.22 260 220.5Z" fill="white"/>
<path d="M251.21 114.29C256.641 119.72 260 127.22 260 135.5C260 143.78 256.641 151.28 251.21 156.71C245.78 162.14 238.28 165.5 230 165.5C213.43 165.5 200 152.07 200 135.5C200 127.22 203.36 119.72 208.79 114.29C214.22 108.86 221.72 105.5 230 105.5C238.28 105.5 245.78 108.86 251.21 114.29Z" fill="white"/>
<path d="M432.5 150.5C436.642 150.5 440 147.142 440 143C440 138.858 436.642 135.5 432.5 135.5C428.358 135.5 425 138.858 425 143C425 147.142 428.358 150.5 432.5 150.5Z" fill="#1E81CE"/>
<path d="M422.5 370.5C426.642 370.5 430 367.142 430 363C430 358.858 426.642 355.5 422.5 355.5C418.358 355.5 415 358.858 415 363C415 367.142 418.358 370.5 422.5 370.5Z" fill="#1E81CE"/>
<path d="M327.5 20.5C331.642 20.5 335 17.1421 335 13C335 8.85786 331.642 5.5 327.5 5.5C323.358 5.5 320 8.85786 320 13C320 17.1421 323.358 20.5 327.5 20.5Z" fill="#1E81CE"/>
<path d="M317.5 445.5C321.642 445.5 325 442.142 325 438C325 433.858 321.642 430.5 317.5 430.5C313.358 430.5 310 433.858 310 438C310 442.142 313.358 445.5 317.5 445.5Z" fill="#1E81CE"/>
<path d="M112.5 15.5C116.642 15.5 120 12.1421 120 8C120 3.85786 116.642 0.5 112.5 0.5C108.358 0.5 105 3.85786 105 8C105 12.1421 108.358 15.5 112.5 15.5Z" fill="#1E81CE"/>
<path d="M12.5 180.5C16.6421 180.5 20 177.142 20 173C20 168.858 16.6421 165.5 12.5 165.5C8.35786 165.5 5 168.858 5 173C5 177.142 8.35786 180.5 12.5 180.5Z" fill="#1E81CE"/>
<path d="M230 410.5H65C62.978 410.5 61.154 409.282 60.381 407.413C59.608 405.545 60.035 403.394 61.465 401.965L103.22 360.21C66.152 325.303 45 276.512 45 225.5C45 123.491 127.991 40.5 230 40.5C332.01 40.5 415 123.491 415 225.5C415 327.51 332.01 410.5 230 410.5ZM77.071 400.5H230C326.496 400.5 405 321.995 405 225.5C405 129.005 326.496 50.5 230 50.5C133.505 50.5 55 129.005 55 225.5C55 275.423 76.422 323.103 113.772 356.313C114.801 357.228 115.408 358.527 115.448 359.903C115.488 361.279 114.959 362.611 113.986 363.585L77.071 400.5Z" fill="#1E81CE"/>
<path d="M90 230.501C87.239 230.501 85 228.263 85 225.501C85 198.175 92.644 171.555 107.103 148.519C121.168 126.112 141.062 107.965 164.634 96.0398C167.098 94.7928 170.106 95.7798 171.352 98.2438C172.599 100.708 171.612 103.716 169.148 104.963C123.412 128.101 95 174.289 95 225.501C95 228.263 92.762 230.501 90 230.501Z" fill="#1E81CE"/>
<path d="M285.002 359.281C283.061 359.281 281.214 358.144 280.402 356.246C279.316 353.707 280.495 350.768 283.033 349.683C332.826 328.393 364.999 279.648 364.999 225.5C364.999 222.738 367.238 220.5 369.999 220.5C372.761 220.5 374.999 222.738 374.999 225.5C374.999 283.657 340.444 336.012 286.966 358.878C286.324 359.151 285.657 359.281 285.002 359.281Z" fill="#1E81CE"/>
<path d="M230 340.5C210.701 340.5 195 324.799 195 305.5V220.5C195 201.201 210.701 185.5 230 185.5C239.346 185.5 248.133 189.142 254.746 195.754C261.358 202.367 265 211.155 265 220.5V305.5C265 324.799 249.299 340.5 230 340.5ZM230 195.5C216.215 195.5 205 206.715 205 220.5V305.5C205 319.285 216.215 330.5 230 330.5C243.785 330.5 255 319.285 255 305.5V220.5C255 213.827 252.398 207.549 247.675 202.826C242.951 198.102 236.674 195.5 230 195.5Z" fill="#1E81CE"/>
<path d="M230 170.5C210.701 170.5 195 154.799 195 135.5C195 126.155 198.642 117.367 205.255 110.754C211.867 104.141 220.655 100.5 230 100.5C239.346 100.5 248.133 104.142 254.746 110.754C261.358 117.367 265 126.155 265 135.5C265 144.845 261.358 153.633 254.746 160.246C248.133 166.858 239.346 170.5 230 170.5ZM230 110.5C223.327 110.5 217.05 113.102 212.326 117.826C207.602 122.549 205 128.827 205 135.5C205 149.285 216.215 160.5 230 160.5C236.674 160.5 242.951 157.898 247.675 153.174C252.399 148.45 255 142.173 255 135.5C255 128.827 252.398 122.549 247.675 117.826C242.951 113.102 236.674 110.5 230 110.5Z" fill="#1E81CE"/>
<path d="M415 80.5C395.701 80.5 380 64.799 380 45.5C380 26.201 395.701 10.5 415 10.5C434.299 10.5 450 26.201 450 45.5C450 64.799 434.299 80.5 415 80.5ZM415 20.5C401.215 20.5 390 31.715 390 45.5C390 59.285 401.215 70.5 415 70.5C428.785 70.5 440 59.285 440 45.5C440 31.715 428.785 20.5 415 20.5Z" fill="#1E81CE"/>
<path d="M25 385.5C11.215 385.5 0 374.285 0 360.5C0 346.715 11.215 335.5 25 335.5C38.785 335.5 50 346.715 50 360.5C50 374.285 38.785 385.5 25 385.5ZM25 345.5C16.729 345.5 10 352.229 10 360.5C10 368.771 16.729 375.5 25 375.5C33.271 375.5 40 368.771 40 360.5C40 352.229 33.271 345.5 25 345.5Z" fill="#1E81CE"/>
</svg>
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M441.725 0H70.2745C31.463 0 0 31.463 0 70.2745V441.725C0 480.537 31.463 512 70.2745 512H441.725C480.537 512 512 480.537 512 441.725V70.2745C512 31.463 480.537 0 441.725 0Z" fill="url(#paint0_linear_93_249)"/>
<path d="M201.899 289.271L251.763 125.343H228.224H211.41L202.443 124.222L140.791 315.902H176.661L90.3487 469.281V470.591L237.912 289.271H201.899Z" fill="#F38C28"/>
<path d="M141.085 315.448L202.445 124.218L201.332 123.9C186.486 119.658 172.463 112.945 159.848 104.041L90.3508 351.51H126.94L90.5393 470.587L177.485 315.824L141.085 315.448Z" fill="#F18012"/>
<path d="M318.469 109.132L226.183 108.878C199.706 108.805 174.462 97.6799 156.546 78.1869H223.801H292.178H302.267L311.235 79.3078L320.202 80.2283L332.532 82.6706L341.814 84.9125L345.416 85.8279C351.818 87.4546 358.024 89.7789 363.919 92.7591L370.083 95.8411C371.948 96.7737 373.725 97.8722 375.393 99.1234C376.71 100.11 377.953 101.189 379.117 102.353L384.095 107.331L384.296 107.565C388.63 112.621 392.327 118.19 395.304 124.145L395.898 125.72C397.736 130.593 398.89 135.698 399.329 140.887C399.634 144.501 399.592 148.136 399.2 151.741L398.667 156.653L398.425 158.585C397.841 163.264 396.796 167.873 395.304 172.345L394.24 174.83C392.711 178.398 390.859 181.817 388.706 185.046C386.385 188.527 383.726 191.771 380.767 194.73L378.49 197.007L372.695 201.49L363.919 207.095L359.435 209.337C354.957 211.575 350.325 213.494 345.576 215.077L339.258 217.183L336.46 217.778C327.798 219.618 318.968 220.546 310.113 220.546H247.341L252.824 201.613C254.224 196.778 258.651 193.451 263.685 193.451H316.006C318.5 193.451 320.988 193.204 323.432 192.714L327.405 191.788C330.02 191.18 332.572 190.325 335.027 189.238C338.917 187.514 342.528 185.221 345.742 182.432L346.409 181.852L348.194 179.809C350.424 177.256 352.277 174.398 353.696 171.319C354.474 169.63 355.118 167.881 355.623 166.091L355.871 165.209C356.547 162.808 357.032 160.355 357.32 157.878L357.806 153.684V153.403C357.806 148.689 357.437 143.981 356.703 139.324L355.776 135.817C355.292 133.986 354.643 132.203 353.838 130.49C352.69 128.044 351.23 125.757 349.495 123.685L349.146 123.27C347.936 121.825 346.614 120.479 345.19 119.243L345.078 119.145C342.91 117.263 340.531 115.636 337.991 114.296L337.723 114.156C335.797 113.14 333.791 112.284 331.726 111.594L331.417 111.491C328.188 110.413 324.845 109.713 321.456 109.404L318.469 109.132Z" fill="#F4F4F4"/>
<path d="M325.401 50.1961H128.51L157.59 79.1258H204.636L304.656 79.3136L314.473 80.0651L319.628 80.8056C330.992 82.4384 342.212 84.9544 353.185 88.3306L356.298 89.5486C362.844 92.1094 369.072 95.4199 374.855 99.414L378.999 102.776C381.786 105.037 384.328 107.583 386.585 110.373C388.271 112.456 389.792 114.669 391.133 116.989L391.571 117.746C394.124 122.165 396.128 126.879 397.539 131.783L398.009 133.416L398.934 139.991L399.008 145.573C399.082 151.229 398.574 156.878 397.492 162.43L396.887 165.533C396.278 168.661 395.368 171.723 394.173 174.676C392.538 178.711 390.381 182.514 387.756 185.988L386.928 187.082C384.443 190.37 381.626 193.392 378.522 196.104L376.942 197.483C372.235 201.594 367.043 205.117 361.483 207.972C357.318 210.112 352.966 211.866 348.481 213.213L342.883 214.895C334.457 217.427 325.813 219.164 317.064 220.083L312.344 220.579H247.329L237.241 254.207H331.144L340.219 253.267L351.147 251.577L358.741 249.886L366.335 247.631L380.968 241.62L386.71 238.426C391.271 235.55 395.607 232.33 399.677 228.791L400.046 228.47L405.973 222.834L411.9 215.884L416.716 208.745L421.161 200.292L424.495 192.215L427.273 183.198L429.125 174.744L430.607 163.848L431.163 155.958V143.184L430.422 133.792L429.69 129.463C428.699 123.593 427.208 117.818 425.234 112.201L424.768 110.876C423.23 106.498 421.345 102.249 419.13 98.1715L418.4 96.8259C416.417 93.1735 414.113 89.7048 411.513 86.4611C409.305 83.7039 406.89 81.1177 404.291 78.7249L403.065 77.5967C398.472 73.3671 393.458 69.6163 388.104 66.4024L387.08 65.7881C380.79 62.536 374.278 59.7339 367.592 57.4029L365.78 56.771L353.925 53.5775L339.664 51.1354L325.401 50.1961Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_93_249" x1="256" y1="0" x2="256" y2="512" gradientUnits="userSpaceOnUse">
<stop stop-color="#34498F"/>
<stop offset="1" stop-color="#005AAA"/>
</linearGradient>
</defs>
</svg>
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#1E1E1E"/>
<path d="M441.725 0H70.2745C31.463 0 0 31.463 0 70.2745V441.725C0 480.537 31.463 512 70.2745 512H441.725C480.537 512 512 480.537 512 441.725V70.2745C512 31.463 480.537 0 441.725 0Z" fill="url(#paint0_linear_93_248)"/>
<path d="M201.899 289.271L251.763 125.343H228.224H211.41L202.443 124.222L140.791 315.902H176.661L90.3487 469.281V470.591L237.912 289.271H201.899Z" fill="#F38C28"/>
<path d="M141.085 315.448L202.445 124.218L201.332 123.9C186.486 119.658 172.463 112.945 159.848 104.041L90.3508 351.51H126.94L90.5393 470.587L177.485 315.824L141.085 315.448Z" fill="#F18012"/>
<path d="M318.469 109.132L226.183 108.878C199.706 108.805 174.462 97.6799 156.546 78.1869H223.801H292.178H302.267L311.235 79.3078L320.202 80.2283L332.532 82.6706L341.814 84.9125L345.416 85.8279C351.818 87.4546 358.024 89.7789 363.919 92.7591L370.083 95.8411C371.948 96.7737 373.725 97.8722 375.393 99.1234C376.71 100.11 377.953 101.189 379.117 102.353L384.095 107.331L384.296 107.565C388.63 112.621 392.327 118.19 395.304 124.145L395.898 125.72C397.736 130.593 398.89 135.698 399.329 140.887C399.634 144.501 399.592 148.136 399.2 151.741L398.667 156.653L398.425 158.585C397.841 163.264 396.796 167.873 395.304 172.345L394.24 174.83C392.711 178.398 390.859 181.817 388.706 185.046C386.385 188.527 383.726 191.771 380.767 194.73L378.49 197.007L372.695 201.49L363.919 207.095L359.435 209.337C354.957 211.575 350.325 213.494 345.576 215.077L339.258 217.183L336.46 217.778C327.798 219.618 318.968 220.546 310.113 220.546H247.341L252.824 201.613C254.224 196.778 258.651 193.451 263.685 193.451H316.006C318.5 193.451 320.988 193.204 323.432 192.714L327.405 191.788C330.02 191.18 332.572 190.325 335.027 189.238C338.917 187.514 342.528 185.221 345.742 182.432L346.409 181.852L348.194 179.809C350.424 177.256 352.277 174.398 353.696 171.319C354.474 169.63 355.118 167.881 355.623 166.091L355.871 165.209C356.547 162.808 357.032 160.355 357.32 157.878L357.806 153.684V153.403C357.806 148.689 357.437 143.981 356.703 139.324L355.776 135.817C355.292 133.986 354.643 132.203 353.838 130.49C352.69 128.044 351.23 125.757 349.495 123.685L349.146 123.27C347.936 121.825 346.614 120.479 345.19 119.243L345.078 119.145C342.91 117.263 340.531 115.636 337.991 114.296L337.723 114.156C335.797 113.14 333.791 112.284 331.726 111.594L331.417 111.491C328.188 110.413 324.845 109.713 321.456 109.404L318.469 109.132Z" fill="#F4F4F4"/>
<path d="M325.401 50.1961H128.51L157.59 79.1258H204.636L304.656 79.3136L314.473 80.0651L319.628 80.8056C330.992 82.4384 342.212 84.9544 353.185 88.3306L356.298 89.5486C362.844 92.1094 369.072 95.4199 374.855 99.414L378.999 102.776C381.786 105.037 384.328 107.583 386.585 110.373C388.271 112.456 389.792 114.669 391.133 116.989L391.571 117.746C394.124 122.165 396.128 126.879 397.539 131.783L398.009 133.416L398.934 139.991L399.008 145.573C399.082 151.229 398.574 156.878 397.492 162.43L396.887 165.533C396.278 168.661 395.368 171.723 394.173 174.676C392.538 178.711 390.381 182.514 387.756 185.988L386.928 187.082C384.443 190.37 381.626 193.392 378.522 196.104L376.942 197.483C372.235 201.594 367.043 205.117 361.483 207.972C357.318 210.112 352.966 211.866 348.481 213.213L342.883 214.895C334.457 217.427 325.813 219.164 317.064 220.083L312.344 220.579H247.329L237.241 254.207H331.144L340.219 253.267L351.147 251.577L358.741 249.886L366.335 247.631L380.968 241.62L386.71 238.426C391.271 235.55 395.607 232.33 399.677 228.791L400.046 228.47L405.973 222.834L411.9 215.884L416.716 208.745L421.161 200.292L424.495 192.215L427.273 183.198L429.125 174.744L430.607 163.848L431.163 155.958V143.184L430.422 133.792L429.69 129.463C428.699 123.593 427.208 117.818 425.234 112.201L424.768 110.876C423.23 106.498 421.345 102.249 419.13 98.1715L418.4 96.8259C416.417 93.1735 414.113 89.7048 411.513 86.4611C409.305 83.7039 406.89 81.1177 404.291 78.7249L403.065 77.5967C398.472 73.3671 393.458 69.6163 388.104 66.4024L387.08 65.7881C380.79 62.536 374.278 59.7339 367.592 57.4029L365.78 56.771L353.925 53.5775L339.664 51.1354L325.401 50.1961Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_93_248" x1="256" y1="0" x2="256" y2="512" gradientUnits="userSpaceOnUse">
<stop stop-color="#34498F"/>
<stop offset="1" stop-color="#005AAA"/>
</linearGradient>
</defs>
</svg>
......@@ -8,10 +8,13 @@ type RHFNumericProps = {
label: string;
allowNegativeValue?: boolean;
allowDecimalValue?: boolean;
disableFormat?: boolean;
decimalScale?: number;
maxValue?: number;
minValue?: number;
readOnly?: boolean;
[key: string]: any;
displayOnly?: boolean;
};
export function RHFNumeric({
......@@ -19,9 +22,12 @@ export function RHFNumeric({
label,
allowNegativeValue = false,
allowDecimalValue = false,
disableFormat = false,
decimalScale = 0,
maxValue,
minValue,
readOnly,
displayOnly = false,
...props
}: RHFNumericProps) {
const { control } = useFormContext();
......@@ -53,83 +59,69 @@ export function RHFNumeric({
fullWidth
variant="outlined"
value={field.value ?? ''}
disabled={readOnly}
// disabled={readOnly}
disabled={readOnly && !displayOnly}
onChange={(e) => {
if (readOnly || displayOnly) return;
const constrainedValue = handleValueChange(e.target.value);
// kalau mau number -> field.onChange(Number(constrainedValue));
field.onChange(constrainedValue);
}}
// InputProps={{
// inputComponent: disableFormat
// ? undefined // ⛔ tanpa format rupiah
// : !allowNegativeValue
// ? NumberFormatRupiah
// : NumberFormatRupiahWithAllowedNegative,
// readOnly,
// inputProps: {
// allowNegativeValue,
// allowDecimalValue,
// decimalScale,
// fixedDecimalScale: false,
// maxValue,
// minValue,
// },
// ...props.InputProps,
// }}
InputProps={{
inputComponent: !allowNegativeValue
? NumberFormatRupiah
: NumberFormatRupiahWithAllowedNegative,
readOnly,
...props.InputProps,
// kalau displayOnly → readonly beneran tapi tetep aktif format
readOnly: displayOnly ? true : props.InputProps?.readOnly,
inputComponent: disableFormat
? undefined
: !allowNegativeValue
? NumberFormatRupiah
: NumberFormatRupiahWithAllowedNegative,
inputProps: {
...props.InputProps?.inputProps,
allowNegativeValue,
allowDecimalValue,
decimalScale,
fixedDecimalScale: false,
maxValue,
minValue,
// format-component butuhkan ini agar tidak error
valueIsNumericString: true,
},
...props.InputProps,
style: displayOnly
? {
pointerEvents: 'none',
backgroundColor: '#f6f6f6',
...(props.InputProps?.style || {}),
}
: props.InputProps?.style,
}}
error={!!fieldState.error}
helperText={fieldState.error?.message}
// FormHelperTextProps={{ sx: { color: 'error.main' } }}
// sx={{
// input: {
// textAlign: 'right',
// ...(readOnly && {
// backgroundColor: '#f6f6f6',
// color: '#1C252E',
// WebkitTextFillColor: '#1C252E',
// }),
// },
// ...(readOnly && {
// '& .MuiInputLabel-root': {
// color: '#1C252E',
// },
// '& .Mui-disabled': {
// WebkitTextFillColor: '#1C252E',
// color: '#1C252E',
// opacity: 1,
// backgroundColor: '#f6f6f6',
// },
// }),
// }}
// sx={{
// input: {
// textAlign: 'right',
// ...(readOnly && {
// backgroundColor: '#f6f6f6',
// color: '#1C252E',
// WebkitTextFillColor: '#1C252E',
// }),
// },
// ...(readOnly && {
// '& .MuiInputLabel-root': {
// color: '#1C252E',
// },
// '& .Mui-disabled': {
// WebkitTextFillColor: '#1C252E',
// color: '#1C252E',
// opacity: 1,
// backgroundColor: '#f6f6f6',
// },
// }),
// // ✅ tambahan untuk kondisi error
// '& .Mui-error': {
// color: (theme) => theme.palette.error.main,
// },
// '& .MuiOutlinedInput-root.Mui-error .MuiOutlinedInput-notchedOutline': {
// borderColor: (theme) => theme.palette.error.main,
// },
// '& .MuiFormHelperText-root.Mui-error': {
// color: (theme) => theme.palette.error.main,
// },
// }}
sx={{
input: {
textAlign: 'right',
......
......@@ -53,6 +53,10 @@ export function RHFTextField({
type={isNumberType ? 'text' : type}
error={!!error}
helperText={error?.message ?? helperText}
InputLabelProps={{
shrink: !!field.value, // 🔥 otomatis naik kalau ada value
...other?.InputLabelProps,
}}
slotProps={{
...slotProps,
htmlInput: {
......
......@@ -58,65 +58,74 @@ export function Logo({
*/
const singleLogo = (
<svg
width="100%"
height="100%"
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id={`${uniqueId}-1`}
x1="152"
y1="167.79"
x2="65.523"
y2="259.624"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_DARKER} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient
id={`${uniqueId}-2`}
x1="86"
y1="128"
x2="86"
y2="384"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_LIGHT} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient
id={`${uniqueId}-3`}
x1="402"
y1="288"
x2="402"
y2="384"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_LIGHT} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
</defs>
<path
fill={`url(#${`${uniqueId}-1`})`}
d="M86.352 246.358C137.511 214.183 161.836 245.017 183.168 285.573C165.515 317.716 153.837 337.331 148.132 344.418C137.373 357.788 125.636 367.911 111.202 373.752C80.856 388.014 43.132 388.681 14 371.048L86.352 246.358Z"
/>
<path
fill={`url(#${`${uniqueId}-2`})`}
fillRule="evenodd"
clipRule="evenodd"
d="M444.31 229.726C398.04 148.77 350.21 72.498 295.267 184.382C287.751 198.766 282.272 226.719 270 226.719V226.577C257.728 226.577 252.251 198.624 244.735 184.24C189.79 72.356 141.96 148.628 95.689 229.584C92.207 235.69 88.862 241.516 86 246.58C192.038 179.453 183.11 382.247 270 383.858V384C356.891 382.389 347.962 179.595 454 246.72C451.139 241.658 447.794 235.832 444.31 229.726Z"
/>
<path
fill={`url(#${`${uniqueId}-3`})`}
fillRule="evenodd"
clipRule="evenodd"
d="M450 384C476.509 384 498 362.509 498 336C498 309.491 476.509 288 450 288C423.491 288 402 309.491 402 336C402 362.509 423.491 384 450 384Z"
/>
</svg>
<img
src="/assets/logo-pex.svg"
alt="PEX Logo"
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
}}
/>
// <svg
// width="100%"
// height="100%"
// viewBox="0 0 512 512"
// fill="none"
// xmlns="http://www.w3.org/2000/svg"
// >
// <defs>
// <linearGradient
// id={`${uniqueId}-1`}
// x1="152"
// y1="167.79"
// x2="65.523"
// y2="259.624"
// gradientUnits="userSpaceOnUse"
// >
// <stop stopColor={PRIMARY_DARKER} />
// <stop offset="1" stopColor={PRIMARY_MAIN} />
// </linearGradient>
// <linearGradient
// id={`${uniqueId}-2`}
// x1="86"
// y1="128"
// x2="86"
// y2="384"
// gradientUnits="userSpaceOnUse"
// >
// <stop stopColor={PRIMARY_LIGHT} />
// <stop offset="1" stopColor={PRIMARY_MAIN} />
// </linearGradient>
// <linearGradient
// id={`${uniqueId}-3`}
// x1="402"
// y1="288"
// x2="402"
// y2="384"
// gradientUnits="userSpaceOnUse"
// >
// <stop stopColor={PRIMARY_LIGHT} />
// <stop offset="1" stopColor={PRIMARY_MAIN} />
// </linearGradient>
// </defs>
// <path
// fill={`url(#${`${uniqueId}-1`})`}
// d="M86.352 246.358C137.511 214.183 161.836 245.017 183.168 285.573C165.515 317.716 153.837 337.331 148.132 344.418C137.373 357.788 125.636 367.911 111.202 373.752C80.856 388.014 43.132 388.681 14 371.048L86.352 246.358Z"
// />
// <path
// fill={`url(#${`${uniqueId}-2`})`}
// fillRule="evenodd"
// clipRule="evenodd"
// d="M444.31 229.726C398.04 148.77 350.21 72.498 295.267 184.382C287.751 198.766 282.272 226.719 270 226.719V226.577C257.728 226.577 252.251 198.624 244.735 184.24C189.79 72.356 141.96 148.628 95.689 229.584C92.207 235.69 88.862 241.516 86 246.58C192.038 179.453 183.11 382.247 270 383.858V384C356.891 382.389 347.962 179.595 454 246.72C451.139 241.658 447.794 235.832 444.31 229.726Z"
// />
// <path
// fill={`url(#${`${uniqueId}-3`})`}
// fillRule="evenodd"
// clipRule="evenodd"
// d="M450 384C476.509 384 498 362.509 498 336C498 309.491 476.509 288 450 288C423.491 288 402 309.491 402 336C402 362.509 423.491 384 450 384Z"
// />
// </svg>
);
const fullLogo = (
......
......@@ -105,6 +105,38 @@ export const navData: NavSectionProps['data'] = [
},
],
},
{
subheader: '',
items: [
{
title: 'e-Faktur',
path: paths.faktur.pk,
icon: ICONS.blank,
children: [
{
title: 'Faktur',
path: paths.faktur.pk,
children: [
{ title: 'Pajak Keluaran', path: paths.faktur.pk },
{ title: 'Pajak Masukan', path: paths.faktur.pm },
{ title: 'Retur Pajak Keluaran', path: paths.faktur.returPk },
{ title: 'Retur Pajak Masukan', path: paths.faktur.returPm },
],
},
{
title: 'Dokumen Lain',
path: paths.faktur.dlk,
children: [
{ title: 'Dokumen Lain Keluaran', path: paths.faktur.dlk },
{ title: 'Dokumen Lain Masukan', path: paths.faktur.dlm },
{ title: 'Retur Dokumen Lain Keluaran', path: paths.faktur.returDlk },
{ title: 'Retur Dokumen Lain Masukan', path: paths.faktur.returDlm },
],
},
],
},
],
},
];
/**
......
import { CONFIG } from 'src/global-config';
import { FakturPkListView } from 'src/sections/faktur/fakturPk/view/faktur-pk-list-view';
const metadata = { title: `Faktur - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FakturPkListView />
</>
);
}
import { CONFIG } from 'src/global-config';
import FakturPkRekamView from 'src/sections/faktur/fakturPk/view/fakturPkRekamView';
const metadata = { title: `Faktur - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FakturPkRekamView />
</>
);
}
......@@ -13,6 +13,7 @@ const ROOTS = {
DASHBOARD: '/dashboard',
UNIFIKASI: '/unifikasi',
PPH21: '/pph21',
FAKTUR: '/faktur',
};
// ----------------------------------------------------------------------
......@@ -122,6 +123,18 @@ export const paths = {
bupot26: `${ROOTS.PPH21}/bupot-26`,
detailsbupot26: (id: string) => `${ROOTS.PPH21}/bupot-26/${id}`,
},
faktur: {
root: ROOTS.FAKTUR,
pm: `${ROOTS.FAKTUR}/pm`,
pk: `${ROOTS.FAKTUR}/pk`,
rekamPk: `${ROOTS.FAKTUR}/pk/new`,
returPm: `${ROOTS.FAKTUR}/retur/pm`,
returPk: `${ROOTS.FAKTUR}/retur/pk`,
dlk: `${ROOTS.FAKTUR}/dokumen-lain/pajak-keluaran`,
dlm: `${ROOTS.FAKTUR}/dokumen-lain/pajak-masukan`,
returDlk: `${ROOTS.FAKTUR}/dokumen-lain/retur-pajak-keluaran`,
returDlm: `${ROOTS.FAKTUR}/dokumen-lain/retur-pajak-masukan`,
},
// DASHBOARD
dashboard: {
root: ROOTS.DASHBOARD,
......
......@@ -39,6 +39,11 @@ const OverviewUnifikasiDokumenDipersamakanPage = lazy(
const OverviewUnifikasiRekamDokumenDipersamakanPage = lazy(
() => import('src/pages/unifikasi/unifikasiRekamDokumenDipersamakan')
);
// Faktur
const OverviewFakturPkPage = lazy(() => import('src/pages/faktur/fakturPk'));
const OverviewFakturPkRekamPage = lazy(() => import('src/pages/faktur/fakturRekamPk'));
// Overview
const IndexPage = lazy(() => import('src/pages/dashboard'));
......@@ -149,4 +154,14 @@ export const dashboardRoutes: RouteObject[] = [
{ path: 'bupot-26', element: <OverviewBupotPasal26Page /> },
],
},
{
path: 'faktur',
element: CONFIG.auth.skip ? dashboardLayout() : <AuthGuard>{dashboardLayout()}</AuthGuard>,
children: [
{ index: true, element: <OverviewFakturPkPage /> },
{ path: 'pk', element: <OverviewFakturPkPage /> },
{ path: 'pk/new', element: <OverviewFakturPkRekamPage /> },
{ path: 'pk/:id/:type', element: <OverviewFakturPkRekamPage /> },
],
},
];
......@@ -25,6 +25,7 @@ interface ModalUploadDigunggungProps {
successMessage?: string;
// onConfirmUpload?: () => void;
onConfirmUpload?: () => Promise<void> | void;
singleUploadPayload?: any;
}
/**
......@@ -63,6 +64,7 @@ const ModalUploadDigunggung: React.FC<ModalUploadDigunggungProps> = ({
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
}) => {
const queryClient = useQueryClient();
......@@ -97,8 +99,14 @@ const ModalUploadDigunggung: React.FC<ModalUploadDigunggungProps> = ({
// fungsi multiple upload -- gunakan normalized array of ids
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([mutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
......
......@@ -13,8 +13,6 @@ const DokumenReferensi = () => {
const nitku = useAppSelector((state) => state.user.data.nitku_trial);
const nitkuValue = watch('idTku');
console.log(nitku);
useEffect(() => {
if (!nitkuValue && nitku) {
setValue('idTku', nitku);
......
......@@ -26,8 +26,6 @@ const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongPr
const dpp = watch('dpp');
const tarifWatched = watch('tarif');
console.log(selectedKode);
const kodeLoaded = Array.isArray(kodeObjectPajak) && kodeObjectPajak.length > 0;
// ---- state & refs to control timing & user-interaction
......@@ -203,8 +201,6 @@ const PphDipotong = ({ kodeObjectPajak, isFormPrefilled = false }: PPHDipotongPr
setValue('kjs', Number(kjs) || 0, { shouldValidate: true });
}, [kodeObjekPajakSelected, setValue]);
console.log(fasilitasOptions);
return (
<Grid container rowSpacing={2} columnSpacing={2}>
<Grid sx={{ mt: 3 }} size={{ md: 6 }}>
......
......@@ -338,6 +338,7 @@ const DigunggungRekamView = () => {
isOpenDialogUpload={isUploadModalOpen}
setIsOpenDialogUpload={setIsUploadModalOpen}
onConfirmUpload={() => handleSaveAndUpload(methods.getValues())}
singleUploadPayload={methods.getValues()}
/>
)}
</DashboardContent>
......
......@@ -25,6 +25,7 @@ interface ModalUploadDnProps {
successMessage?: string;
// onConfirmUpload?: () => void;
onConfirmUpload?: () => Promise<void> | void;
singleUploadPayload?: any;
}
/**
......@@ -63,6 +64,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
}) => {
const queryClient = useQueryClient();
......@@ -94,10 +96,17 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
signer: signer || '',
},
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([mutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
......@@ -116,7 +125,7 @@ const ModalUploadDn: React.FC<ModalUploadDnProps> = ({
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
// ✅ refetch langsung setelah sukses
......
......@@ -217,11 +217,6 @@ export function DnListView() {
resetPagination(tableKey);
}, 400);
// const paginationModel: GridPaginationModel = {
// page,
// pageSize,
// };
// ---------- status options and columns (kept identical to your original) ----------
type Status = 'draft' | 'normal' | 'cancelled' | 'amended';
type StatusOption = { value: Status; label: string };
......@@ -332,34 +327,6 @@ export function DnListView() {
return () => unsubscribe();
}, [apiRef]);
// ---------- memoized toolbar validation (avoid recompute heavy every click) ----------
// const validatedActions = useMemo(() => {
// const dataSelected = dataSelectedRef.current;
// const count = dataSelected.length;
// const hasSelection = count > 0;
// if (!hasSelection) {
// return {
// canDetail: false,
// canEdit: false,
// canDelete: false,
// canUpload: false,
// canReplacement: false,
// canCancel: false,
// };
// }
// const allDraft = dataSelected.every((d) => d.fgStatus === FG_STATUS_DN.DRAFT);
// const allNormal = dataSelected.every((d) => d.fgStatus === FG_STATUS_DN.NORMAL_DONE);
// return {
// canDetail: count === 1,
// canEdit: count === 1 && allDraft,
// canDelete: hasSelection && allDraft,
// canUpload: hasSelection && allDraft,
// canReplacement: count === 1 && dataSelected[0].fgStatus === FG_STATUS_DN.NORMAL_DONE,
// canCancel: hasSelection && allNormal,
// };
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [selectionVersion]);
const validatedActions = useMemo(() => {
const dataSelected = dataSelectedRef.current;
const count = dataSelected.length;
......
......@@ -340,6 +340,7 @@ const DnRekamView = () => {
isOpenDialogUpload={isUploadModalOpen}
setIsOpenDialogUpload={setIsUploadModalOpen}
onConfirmUpload={() => methods.handleSubmit(handleSaveAndUpload)()}
singleUploadPayload={methods.getValues()}
/>
)}
</DashboardContent>
......
......@@ -45,6 +45,7 @@ function normalisePropsGetDn(params) {
fgPdf: getFgStatusPdf(params.link, transformFgStatusToFgSignStatus(params.fgStatus)),
created_at: formatDateToDDMMYYYY(params.created_at),
updated_at: formatDateToDDMMYYYY(params.updated_at),
kdObjPjk: params.kodeObjekPajak,
};
}
......
......@@ -24,6 +24,7 @@ interface ModalUploadNrProps {
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
onConfirmUpload?: () => Promise<void> | void;
singleUploadPayload?: any;
}
/**
......@@ -62,6 +63,7 @@ const ModalUploadNr: React.FC<ModalUploadNrProps> = ({
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
}) => {
const queryClient = useQueryClient();
......@@ -94,9 +96,19 @@ const ModalUploadNr: React.FC<ModalUploadNrProps> = ({
},
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
// const handleMultipleDelete = async () => {
// const ids = normalizeSelection(dataSelected);
// return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
// };
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([mutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
......@@ -115,7 +127,7 @@ const ModalUploadNr: React.FC<ModalUploadNrProps> = ({
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
// ✅ refetch langsung setelah sukses
......
......@@ -376,6 +376,7 @@ const NrRekamView = () => {
isOpenDialogUpload={isUploadModalOpen}
setIsOpenDialogUpload={setIsUploadModalOpen}
onConfirmUpload={() => handleSaveAndUpload(methods.getValues())}
singleUploadPayload={methods.getValues()}
/>
)}
</DashboardContent>
......
......@@ -24,6 +24,8 @@ interface ModalUploadSspProps {
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
onConfirmUpload?: () => Promise<void> | void;
singleUploadPayload?: any;
}
/**
......@@ -62,6 +64,7 @@ const ModalUploadSsp: React.FC<ModalUploadSspProps> = ({
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
}) => {
const queryClient = useQueryClient();
......@@ -93,10 +96,17 @@ const ModalUploadSsp: React.FC<ModalUploadSspProps> = ({
signer: signer || '',
},
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([mutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
......@@ -115,7 +125,7 @@ const ModalUploadSsp: React.FC<ModalUploadSspProps> = ({
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
// ✅ refetch langsung setelah sukses
......
......@@ -341,6 +341,7 @@ const SspRekamView = () => {
isOpenDialogUpload={isUploadModalOpen}
setIsOpenDialogUpload={setIsUploadModalOpen}
onConfirmUpload={() => handleSaveAndUpload(methods.getValues())}
singleUploadPayload={methods.getValues()}
/>
)}
</DashboardContent>
......
import React from 'react';
import { Alert, Typography, Button, IconButton, Box } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
interface AlertInformationDppLainProps {
onClose: () => void;
}
const AlertInformationDppLain: React.FC<AlertInformationDppLainProps> = ({ onClose }) => (
<Alert
icon={
<Box
component="img"
src="/assets/icon-info-dpp-lain.svg"
alt="Info DPP Lain"
sx={{ width: 60, height: 60 }}
/>
}
severity="warning"
sx={{
position: 'relative',
border: '1px solid #FACC15', // yellow-400
alignItems: 'center',
bgcolor: '#FFFBEB', // yellow-50
}}
>
{/* Main Content */}
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Nilai DPP Nilai Lain/DPP diisi dengan memperhatikan ketentuan perpajakan yang berlaku.
</Typography>
<Typography variant="body2">Referensi:</Typography>
{/* List */}
<Box component="ul" sx={{ listStyle: 'none', pl: 0 }}>
<Box component="li" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#663C00' }} />
<Typography variant="body2" component="span" sx={{ pt: 1 }}>
Peraturan Menteri Keuangan Nomor 131 Tahun 2024.{' '}
<Button
component="a"
href="https://datacenter.ortax.org/ortax/aturan/show/26049"
target="_blank"
rel="noreferrer"
variant="text"
color="primary"
size="small"
sx={{
minWidth: 0,
p: 0,
textTransform: 'none',
'&:hover': { bgcolor: 'transparent' },
}}
>
Klik di sini
</Button>
</Typography>
</Box>
</Box>
</Box>
{/* Close Button */}
<IconButton
aria-label="Close Dialog"
onClick={onClose}
size="small"
sx={{ position: 'absolute', top: 8, right: 8 }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
);
export default AlertInformationDppLain;
import React from 'react';
import type { GridPreferencePanelsValue } from '@mui/x-data-grid-premium';
import { useGridApiContext } from '@mui/x-data-grid-premium';
import { IconButton, Tooltip } from '@mui/material';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
// ✅ React.memo: cegah render ulang tanpa alasan
const CustomColumnsButton: React.FC = React.memo(() => {
const apiRef = useGridApiContext();
// ✅ useCallback biar referensi handleClick stabil di setiap render
const handleClick = React.useCallback(() => {
if (!apiRef.current) return;
apiRef.current.showPreferences('columns' as GridPreferencePanelsValue);
}, [apiRef]);
return (
<Tooltip title="Kolom">
<IconButton
size="small"
onClick={handleClick}
sx={{
color: '#123375',
'&:hover': { backgroundColor: 'rgba(18, 51, 117, 0.08)' },
}}
>
<ViewColumnIcon fontSize="small" />
</IconButton>
</Tooltip>
);
});
export default CustomColumnsButton;
This diff is collapsed.
import * as React from 'react';
import type { GridToolbarProps } from '@mui/x-data-grid-premium';
import { GridToolbarContainer } from '@mui/x-data-grid-premium';
import { Stack, Divider, IconButton, Tooltip } from '@mui/material';
import type { ActionItem } from '../types/types';
import { CustomFilterButton } from './CustomFilterButton';
import CustomColumnsButton from './CustomColumnsButton';
interface CustomToolbarProps extends GridToolbarProps {
actions?: ActionItem[][];
columns: any[]; // GridColDef[]
filterModel: any;
setFilterModel: (m: any) => void;
statusOptions?: { value: string; label: string }[];
}
// ✅ React.memo mencegah render ulang kalau props sama
export const CustomToolbar = React.memo(function CustomToolbar({
actions = [],
columns,
filterModel,
setFilterModel,
statusOptions = [],
...gridToolbarProps
}: CustomToolbarProps) {
return (
<GridToolbarContainer
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 1.5,
}}
{...gridToolbarProps}
>
<Stack direction="row" alignItems="center" gap={1}>
{actions.map((group, groupIdx) => (
<Stack key={groupIdx} direction="row" gap={0.5} alignItems="center">
{group.map((action, idx) => (
<Tooltip key={idx} title={action.title}>
<span>
<IconButton
sx={{ color: action.disabled ? 'action.disabled' : '#123375' }}
size="small"
onClick={action.func}
disabled={action.disabled}
>
{action.icon}
</IconButton>
</span>
</Tooltip>
))}
{groupIdx < actions.length - 1 && <Divider orientation="vertical" flexItem />}
</Stack>
))}
</Stack>
<Stack direction="row" alignItems="center" gap={0.5}>
<CustomColumnsButton />
<CustomFilterButton
columns={columns}
filterModel={filterModel}
setFilterModel={setFilterModel}
statusOptions={statusOptions}
/>
</Stack>
</GridToolbarContainer>
);
});
import * as React from 'react';
import { Grid, Typography, Divider, Box } from '@mui/material';
interface ListDetailItem {
label: React.ReactNode;
value: React.ReactNode;
}
interface ListDetailBuilderProps {
rows?: ListDetailItem[];
labelWidth?: number; // optional, default 4
spacingY?: number; // optional, default 2
}
export default function ListDetailBuilder({
rows = [],
labelWidth = 4,
spacingY = 2,
}: ListDetailBuilderProps) {
if (rows.length === 0) return null;
return (
<Box
sx={{
width: '100%',
bgcolor: 'background.paper',
borderRadius: 2,
overflow: 'hidden',
}}
>
{rows.map((row, index) => (
<React.Fragment key={index}>
<Grid container alignItems="flex-start" spacing={2} sx={{ px: 2, py: spacingY }}>
<Grid size={{ xs: 12, md: labelWidth }}>
<Typography
component="div"
variant="body2"
sx={{
fontWeight: 600,
color: 'text.secondary',
whiteSpace: 'pre-wrap',
}}
>
{row.label}
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 12 - labelWidth }}>
<Typography
component="div"
variant="body2"
sx={{
color: 'text.primary',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{row.value ?? '-'}
</Typography>
</Grid>
</Grid>
{index < rows.length - 1 && <Divider sx={{ mx: 2 }} />}
</React.Fragment>
))}
</Box>
);
}
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_DIGUNGGUNG } from '../constant';
interface PanduanDnRekamProps {
handleOpen: () => void;
isOpen: boolean;
}
const PanduanSspRekam: 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_DIGUNGGUNG.description.intro}
</Typography>
<Typography variant="body2">{PANDUAN_REKAM_DIGUNGGUNG.description.textList}</Typography>
<Box component="ol" sx={{ pl: 3, mb: 2 }}>
{PANDUAN_REKAM_DIGUNGGUNG.description.list.map((item, idx) => (
<Typography key={`desc-${idx}`} variant="body2" component="li">
{item}
</Typography>
))}
</Box>
<Typography variant="body2" sx={{ mb: 2 }}>
{PANDUAN_REKAM_DIGUNGGUNG.description.closing}
</Typography>
{/* Bagian-bagian */}
{PANDUAN_REKAM_DIGUNGGUNG.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(PanduanSspRekam);
import React from 'react';
import Chip from '@mui/material/Chip';
import Box from '@mui/material/Box';
import { HourglassTopRounded, AddTaskOutlined, TaskAltOutlined } from '@mui/icons-material';
import { IconButton, Tooltip } from '@mui/material';
type Props = {
value?: string;
revNo?: number;
fgpelunasan?: string | boolean;
fguangmuka?: string | boolean;
};
const StatusChip: React.FC<Props> = ({ value, fgpelunasan, fguangmuka }) => {
if (!value) return <Chip label="" size="small" />;
const extraComponent = (() => {
if (fgpelunasan) {
return (
<Tooltip title="Pelunasan">
<IconButton
size="small"
sx={{
backgroundColor: 'blue',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'blue',
},
}}
>
<TaskAltOutlined style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
);
}
if (fguangmuka) {
return (
<Tooltip title="Uang Muka">
<IconButton
size="small"
sx={{
backgroundColor: 'green',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'green',
},
}}
>
<AddTaskOutlined style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
);
}
return null;
})();
let mainComponent: React.ReactNode = <Chip label={value} size="small" />;
if (value === 'WAITING FOR AMENDMENT') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label="Normal Pengganti"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
}}
/>
<Tooltip title="Menunggu Persetujuan">
<IconButton
size="small"
sx={{
backgroundColor: 'orange',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'orange',
},
}}
>
<HourglassTopRounded style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
</Box>
);
} else if (value === 'APPROVED') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label="Normal"
size="small"
variant="outlined"
sx={{
borderColor: '#1976d2',
color: '#1976d2',
borderRadius: '8px',
fontWeight: 500,
}}
/>
<Chip
label="Approved"
size="small"
variant="outlined"
sx={{
borderColor: '#2e7d32',
color: '#2e7d32',
borderRadius: '8px',
fontWeight: 500,
}}
/>
</Box>
);
} else if (value === 'AMENDED') {
mainComponent = (
<Chip
label="Diganti"
size="small"
variant="outlined"
sx={{
color: '#fff',
backgroundColor: '#f38c28',
borderRadius: '8px',
fontWeight: 500,
border: 'none',
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.15)',
}}
/>
);
} else if (value === 'CANCELLED') {
mainComponent = (
<Chip
label="Batal"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: 500,
}}
/>
);
} else if (value === 'DRAFT') {
mainComponent = (
<Chip
label="Draft"
size="small"
variant="outlined"
sx={{
borderColor: '#9e9e9e',
color: '#616161',
borderRadius: '8px',
}}
/>
);
} else if (value === 'WAITING FOR CANCELLATION') {
mainComponent = (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
<Chip
label="Batal"
size="small"
variant="outlined"
sx={{
borderColor: '#d32f2f',
color: '#d32f2f',
borderRadius: '8px',
fontWeight: 500,
}}
/>
<Tooltip title="Menunggu Persetujuan">
<IconButton
size="small"
sx={{
backgroundColor: 'orange',
color: 'white',
'&:hover': {
backgroundColor: 'white',
color: 'orange',
},
}}
>
<HourglassTopRounded style={{ height: '15px', width: '15px' }} />
</IconButton>
</Tooltip>
</Box>
);
}
// ✅ Gabungkan komponen utama + tambahan
return (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
}}
>
{mainComponent}
{extraComponent}
</Box>
);
};
export default React.memo(StatusChip);
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
} from '@mui/material';
interface CancelConfirmationDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
selectedCount: number;
}
const CancelConfirmationDialog: React.FC<CancelConfirmationDialogProps> = ({
open,
onClose,
onConfirm,
selectedCount,
}) => (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Konfirmasi Pembatalan</DialogTitle>
<DialogContent>
<Typography>
Apakah Anda yakin ingin membatalkan {selectedCount} data yang dipilih?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Batal</Button>
<Button onClick={onConfirm} color="error" variant="contained">
Ya, Batalkan
</Button>
</DialogActions>
</Dialog>
);
export default CancelConfirmationDialog;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useCancelFakturPk from '../../hooks/useCancelFakturPK';
interface ModalCancelFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogCancel: boolean;
setIsOpenDialogCancel: (v: boolean) => void;
successMessage?: string;
onConfirmCancel?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalCancelFakturPK: React.FC<ModalCancelFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogCancel,
setIsOpenDialogCancel,
successMessage = 'Data berhasil dibatalkan',
onConfirmCancel,
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useCancelFakturPk({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
// setSelectionModel?.(undefined);
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogCancel(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmCancel?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal membantalkan data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['faktur', 'pk'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogCancel, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin ingin melakukan pembatalan?"
description=""
actionTitle="Iya"
isOpen={isOpenDialogCancel}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={onSubmit}
/>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalCancelFakturPK;
import React, { useEffect, useState } from 'react';
import { enqueueSnackbar } from 'notistack';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import DialogContent from '@mui/material/DialogContent';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import useCetakPdfFakturPK from '../../hooks/useCetakFakturPK';
interface ModalCetakPdfFakturPKProps {
payload?: Record<string, any>;
isOpen: boolean;
onClose: () => void;
}
const ModalCetakFakturPK: React.FC<ModalCetakPdfFakturPKProps> = ({ payload, isOpen, onClose }) => {
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
console.log(payload, 'ini dari cetak');
const { mutateAsync } = useCetakPdfFakturPK({
onError: (error: any) => {
enqueueSnackbar(error?.message || 'Gagal memuat PDF', { variant: 'error' });
setLoading(false);
},
onSuccess: (res: any) => {
const fileUrl = res?.url || res?.data?.url;
if (!fileUrl) {
enqueueSnackbar('URL PDF tidak ditemukan di respons API', { variant: 'warning' });
setLoading(false);
return;
}
setPdfUrl(fileUrl);
setLoading(false);
enqueueSnackbar(res?.MsgStatus || 'PDF berhasil dibentuk', { variant: 'success' });
// console.log('Ok');
},
});
useEffect(() => {
const runCetak = async () => {
if (!isOpen || !payload) return;
setLoading(true);
setPdfUrl(null);
try {
// Payload sudah lengkap dari parent (sudah ada namaObjekPajak, pasalPPh, statusPPh)
// const normalized = normalizePayloadCetakPdf(payload);
// console.log('📤 Payload final cetak PDF:', normalized);
await mutateAsync({ id: payload.id });
} catch (err) {
console.error('❌ Error cetak PDF:', err);
enqueueSnackbar('Gagal generate PDF', { variant: 'error' });
setLoading(false);
}
};
runCetak();
}, [isOpen, payload, mutateAsync]);
return (
<DialogUmum maxWidth="lg" isOpen={isOpen} onClose={onClose} title="Detail Pajak Keluaran">
<DialogContent classes={{ root: 'p-16 sm:p-24' }}>
{loading && (
<Box display="flex" justifyContent="center" alignItems="center" height="60vh">
<CircularProgress />
</Box>
)}
{!loading && pdfUrl && (
<iframe
src={pdfUrl}
style={{
width: '100%',
height: '80vh',
border: 'none',
borderRadius: 8,
}}
title="Preview PDF Faktur PK"
/>
)}
{!loading && !pdfUrl && (
<Box textAlign="center" color="text.secondary" py={4}>
PDF tidak tersedia untuk ditampilkan.
</Box>
)}
</DialogContent>
</DialogUmum>
);
};
export default ModalCetakFakturPK;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useDeleteFakturPK from '../../hooks/useDeleteFakturPK';
interface ModalDeleteFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
successMessage?: string;
onConfirmDelete?: () => Promise<void> | void;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalDeleteFakturPK: React.FC<ModalDeleteFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogDelete,
setIsOpenDialogDelete,
successMessage = 'Data berhasil dihapus',
onConfirmDelete,
}) => {
const queryClient = useQueryClient();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
// React Query mutation for delete
const { mutateAsync } = useDeleteFakturPK({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
// fungsi multiple delete -- gunakan normalized array of ids
const handleMultipleDelete = async () => {
const ids = normalizeSelection(dataSelected);
return Promise.allSettled(ids.map(async (id) => mutateAsync({ id: String(id) })));
};
const clearSelection = () => {
// clear grid selection via apiRef if available
tableApiRef?.current?.setRowSelectionModel?.([]);
// also clear local state if setter provided
// set to undefined to follow the native hook type (or to empty array cast)
setSelectionModel?.(undefined);
};
const handleCloseModal = () => {
setIsOpenDialogDelete(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleDelete();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmDelete?.();
handleCloseModal();
clearSelection();
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
} finally {
// sesuaikan queryKey jika perlu; tetap panggil invalidasi
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'dn'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogDelete, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin akan menghapus data ini?"
description="Data yang sudah dihapus tidak dapat dikembalikan."
actionTitle="Hapus"
isOpen={isOpenDialogDelete}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={onSubmit}
/>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalDeleteFakturPK;
import React, { useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
import useUpload from '../../hooks/useUpload';
import DialogUmum from 'src/shared/components/dialog/DialogUmum';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Grid';
import { Field } from 'src/components/hook-form';
import MenuItem from '@mui/material/MenuItem';
import { useSelector } from 'react-redux';
import type { RootState } from 'src/store';
import Agreement from 'src/shared/components/agreement/Agreement';
import { FormProvider, useForm } from 'react-hook-form';
import Button from '@mui/material/Button';
interface ModalUploadFakturProps {
dataSelected?: GridRowSelectionModel;
setSelectionModel?: React.Dispatch<React.SetStateAction<GridRowSelectionModel | undefined>>;
tableApiRef?: React.MutableRefObject<any>;
isOpenDialogUpload: boolean;
setIsOpenDialogUpload: (v: boolean) => void;
successMessage?: string;
// onConfirmUpload?: () => void;
onConfirmUpload?: () => Promise<void> | void;
singleUploadPayload?: any;
}
/**
* Normalize various possible shapes of grid selection model into array of ids.
* Handles:
* - array of ids: [1, 2, 'a']
* - Set-of-ids: new Set([1,2])
* - object like { ids: Set(...), type: 'include' }
* - fallback: tries to extract keys if it's an object map
*/
const normalizeSelection = (sel?: any): (string | number)[] => {
if (!sel) return [];
if (Array.isArray(sel)) return sel as (string | number)[];
if (sel instanceof Set) return Array.from(sel) as (string | number)[];
if (typeof sel === 'object') {
// common shape from newer MUI: { ids: Set(...), type: 'include' }
if (sel.ids instanceof Set) return Array.from(sel.ids) as (string | number)[];
// maybe it's a map-like object { '1': true, '2': true }
const maybeIds = Object.keys(sel).filter((k) => k !== 'type' && k !== 'size');
if (maybeIds.length > 0) {
// try to convert numeric-like keys to number where applicable
return maybeIds.map((k) => {
const n = Number(k);
return Number.isNaN(n) ? k : n;
});
}
}
return [];
};
const ModalUploadFakturPK: React.FC<ModalUploadFakturProps> = ({
dataSelected,
setSelectionModel,
tableApiRef,
isOpenDialogUpload,
setIsOpenDialogUpload,
successMessage = 'Data berhasil diupload',
onConfirmUpload,
singleUploadPayload,
}) => {
const queryClient = useQueryClient();
// const uploadFakturPK = useUpload();
// custom hooks for progress state
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const [isCheckedAgreement, setIsCheckedAgreement] = useState<boolean>(false);
const signer = useSelector((state: RootState) => state.user.data.signer);
const { mutateAsync, isPending } = useUpload({
onSuccess: () => processSuccess(),
onError: () => processFail(),
});
const methods = useForm({
defaultValues: {
signer: signer || '',
},
});
const handleMultipleUpload = async () => {
if (singleUploadPayload) {
setNumberOfData(1);
return Promise.allSettled([mutateAsync(singleUploadPayload)]);
}
const ids = normalizeSelection(dataSelected);
setNumberOfData(ids.length);
return Promise.allSettled(ids.map((id) => mutateAsync({ id: String(id) })));
};
const handleCloseModal = () => {
setIsOpenDialogUpload(false);
resetToDefault();
};
const onSubmit = async () => {
try {
setIsOpenDialogProgressBar(true);
await handleMultipleUpload();
enqueueSnackbar(successMessage, { variant: 'success' });
await onConfirmUpload?.();
// ❌ jangan tutup modal dulu
// ❌ jangan reset progress dulu
// biarkan progress bar animasi naik ke 100%
} catch (error: any) {
enqueueSnackbar(error?.message || 'Gagal upload data', { variant: 'error' });
} finally {
queryClient.invalidateQueries({ queryKey: ['unifikasi', 'nr'] });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length);
}, [isOpenDialogUpload, dataSelected, setNumberOfData]);
return (
<>
<FormProvider {...methods}>
<DialogUmum
isOpen={isOpenDialogUpload}
onClose={handleCloseModal}
title="Upload Bukti Potong"
>
<Stack spacing={2} sx={{ mt: 2 }}>
<Grid size={{ md: 12 }}>
<Field.Select name="signer" label="NPWP/NIK Penandatangan">
<MenuItem value={signer}>{signer}</MenuItem>
</Field.Select>
</Grid>
<Grid size={12}>
<Agreement
isCheckedAgreement={isCheckedAgreement}
setIsCheckedAgreement={setIsCheckedAgreement}
text="Dengan ini saya menyatakan bahwa Bukti Pemotongan/Pemungutan Unifikasi telah saya isi dengan benar secara elektronik sesuai dengan"
/>
</Grid>
<Stack direction="row" justifyContent="flex-end" spacing={1} mt={1}>
<Button
type="button"
disabled={!isCheckedAgreement}
onClick={onSubmit}
loading={isPending}
variant="contained"
sx={{ background: '#143B88' }}
>
Save
</Button>
</Stack>
</Stack>
</DialogUmum>
</FormProvider>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalUploadFakturPK;
import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormLabel from '@mui/material/FormLabel';
import MenuItem from '@mui/material/MenuItem';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import React, { useEffect, useRef } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { Field } from 'src/components/hook-form';
import useGetNegara from '../../hooks/useGetNegara';
import { useParams } from 'react-router';
import Grid from '@mui/material/Grid';
interface InformasiPembeliProps {
fakturData?: any;
isLoading?: boolean;
}
const InformasiPembeli: React.FC<InformasiPembeliProps> = ({ fakturData, isLoading }) => {
const { type } = useParams<{ id?: string; type?: 'ubah' | 'pengganti' | 'new' }>();
const { control, setValue, resetField, getValues, trigger } = useFormContext();
const { data: country = [] } = useGetNegara();
const initialIdentitasRef = useRef<string | null>(null);
const isEdit = type === 'ubah';
// const isPengganti = type === 'pengganti';
// const canModify = isEdit || isPengganti;
const canModify = !fakturData || isEdit;
// watch fields
const identitas = useWatch({ control, name: 'idLainPembeli' });
const npwpPembeli = useWatch({ control, name: 'npwpPembeli' });
const detailTransaksi = useWatch({ control, name: 'detailTransaksi' });
const forceNpwpMode = detailTransaksi === 'TD.00302' || detailTransaksi === 'TD.00303';
useEffect(() => {
if (forceNpwpMode) {
const currentIdentitas = getValues('idLainPembeli');
if (currentIdentitas !== '0') {
setValue('idLainPembeli', '0', { shouldDirty: true, shouldValidate: true });
}
// pastikan negara IDN
setValue('kdNegaraPembeli', 'IDN', { shouldDirty: true, shouldValidate: true });
// trigger validation bila perlu
trigger('kdNegaraPembeli');
}
}, [forceNpwpMode, setValue, getValues, trigger]);
// flag to mark autofill
const hasFilledRef = useRef(false);
// AUTOFILL SAAT fakturData MUNCUL
useEffect(() => {
if (!fakturData) return;
// mark filled (do it before/after setValue — both ok)
hasFilledRef.current = true;
initialIdentitasRef.current = fakturData.idlainpembeli ?? '0';
const useNik = fakturData.idlainpembeli === '2' || fakturData.idlainpembeli === '3';
const mapped = {
idLainPembeli: fakturData.idlainpembeli ?? '0',
// npwpPembeli: fakturData.npwppembeli ?? '',
npwpPembeli: useNik ? (fakturData.nikpasppembeli ?? '') : (fakturData.npwppembeli ?? ''),
kdNegaraPembeli: fakturData.kdnegarapembeli ?? 'IDN',
namaPembeli: fakturData.namapembeli ?? '',
tkuPembeli: fakturData.tkupembeli ?? '',
emailPembeli: fakturData.emailpembeli ?? '',
alamatPembeli: fakturData.alamatpembeli ?? '',
};
// apply per-field to avoid clobbering entire form
// Object.entries(mapped).forEach(([key, val]) => {
// // don't overwrite if user already edited this field
// // NOTE: using setValue with shouldDirty: false to treat this as autofill
// setValue(key, val as any, { shouldDirty: false, shouldValidate: false });
// });
Object.entries(mapped).forEach(([key, val]) => {
const shouldValidate = key === 'npwpPembeli';
setValue(key, val as any, {
shouldDirty: false,
shouldValidate,
});
});
}, [fakturData, setValue]);
useEffect(() => {
// HANYA mode new yang boleh reset default
if (type !== 'new') return;
// masih loading → jangan reset
if (isLoading) return;
// sudah autofill (edit/pengganti) → jangan reset
// if (hasFilledRef.current) return;
if (hasFilledRef.current && fakturData) {
// kalau user ganti identitas → tetap reset
if (identitas === (fakturData?.idlainpembeli ?? '0')) return;
}
// kalau form sudah punya value (termasuk dari async source) → jangan reset
const current = getValues('idLainPembeli');
if (current !== undefined && current !== null && current !== '') return;
// apply default
const defaults = {
idLainPembeli: '0',
npwpPembeli: '',
namaPembeli: '',
tkuPembeli: '',
kdNegaraPembeli: 'IDN',
emailPembeli: '',
alamatPembeli: '',
};
Object.entries(defaults).forEach(([k, v]) =>
setValue(k, v as any, { shouldDirty: false, shouldValidate: false })
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type, fakturData, isLoading, setValue]);
useEffect(() => {
// if (hasFilledRef.current) return;
if (hasFilledRef.current && identitas === initialIdentitasRef.current) {
return;
}
if (!identitas) return;
const fields = [
'npwpPembeli',
'namaPembeli',
'kdNegaraPembeli',
'emailPembeli',
'alamatPembeli',
'tkuPembeli',
];
fields.forEach((f) => resetField(f));
if (identitas === '0' || identitas === '1') {
// NPWP & NIK → otomatis IDN
setValue('kdNegaraPembeli', 'IDN', { shouldDirty: true, shouldValidate: true });
trigger('kdNegaraPembeli'); // 🔥 baru
} else {
// Passport / ID Lain → kosong dan wajib diisi user
setValue('kdNegaraPembeli', '', { shouldDirty: true, shouldValidate: true });
trigger('kdNegaraPembeli'); // 🔥 baru
}
}, [identitas, resetField, setValue, trigger, hasFilledRef]);
// AUTO GENERATE TKU (ONLY WHEN MANUAL INPUT)
useEffect(() => {
if (hasFilledRef.current) return;
if (!npwpPembeli) {
setValue('tkuPembeli', '');
return;
}
if (identitas === '0' || identitas === '1') {
const base = npwpPembeli.replace(/\D/g, '').slice(0, 16);
setValue('tkuPembeli', `${base.padEnd(16, '0')}000000`.slice(0, 22));
} else {
setValue('tkuPembeli', '0'.repeat(22));
}
}, [npwpPembeli, identitas, setValue]);
const labelMap: Record<string, string> = {
'0': 'NPWP',
'1': 'NIK',
'2': 'Passport',
'3': 'ID Lain',
};
const labelPembeli = `${labelMap[identitas as keyof typeof labelMap] ?? 'NIK'} Pembeli`;
const disabledNegara = identitas === '0' || identitas === '1';
return (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid size={{ xs: 12 }} sx={{ mt: 3 }}>
<Divider sx={{ fontWeight: 'bold', fontSize: '1.2rem' }} textAlign="center">
Informasi Pembeli
</Divider>
</Grid>
{/* IDENTITAS */}
<Grid sx={{ mt: 3 }} size={{ md: 6 }}>
<Controller
name="idLainPembeli"
control={control}
render={({ field }) => (
<>
<FormLabel component="legend" sx={{ mb: 1, fontWeight: 600 }}>
Identitas
</FormLabel>
<RadioGroup row {...field}>
<FormControlLabel
value="0"
control={<Radio />}
label="NPWP"
disabled={!canModify}
/>
<FormControlLabel
value="1"
control={<Radio />}
label="NIK"
disabled={!canModify || forceNpwpMode}
/>
<FormControlLabel
value="2"
control={<Radio />}
label="Passport"
disabled={!canModify || forceNpwpMode}
/>
<FormControlLabel
value="3"
control={<Radio />}
label="ID Lain"
disabled={!canModify || forceNpwpMode}
/>
</RadioGroup>
</>
)}
/>
</Grid>
{/* NPWP / NIK / ID */}
<Grid size={{ md: 6 }} sx={{ display: 'flex', alignItems: 'end' }}>
<Field.Text name="npwpPembeli" label={labelPembeli} disabled={!canModify} />
</Grid>
{/* NEGARA */}
<Grid size={{ md: 6 }}>
<Field.Select
name="kdNegaraPembeli"
label="Kode Negara Pembeli"
disabled={disabledNegara || !canModify || forceNpwpMode}
>
{(country ?? []).map((item) => (
<MenuItem key={item.kode} value={item.kode}>
{item.nama}
</MenuItem>
))}
</Field.Select>
</Grid>
<Grid size={{ md: 6 }} sx={{ display: 'flex', alignItems: 'end' }}>
<Field.Text name="namaPembeli" label="Nama Pembeli" disabled={!canModify} />
</Grid>
<Grid size={{ md: 6 }} sx={{ display: 'flex', alignItems: 'end' }}>
<Field.Text name="tkuPembeli" label="NITKU Pembeli" disabled={!canModify} />
</Grid>
<Grid size={{ md: 6 }} sx={{ display: 'flex', alignItems: 'end' }}>
<Field.Text name="emailPembeli" label="Email" disabled={!canModify} />
</Grid>
<Grid size={{ md: 12 }}>
<Field.Text
name="alamatPembeli"
label="Alamat"
multiline
minRows={3}
disabled={!canModify}
/>
</Grid>
</Grid>
);
};
export default InformasiPembeli;
import React from 'react';
import Grid from '@mui/material/Grid';
import Divider from '@mui/material/Divider';
import { RHFNumeric } from 'src/components/hook-form/rhf-numeric';
// ----------------------------------------------------------------------
const TotalTransaksi: React.FC = () => (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid size={{ xs: 12 }} sx={{ mt: 3 }}>
<Divider sx={{ fontWeight: 'bold', fontSize: '1.2rem', mb: 2 }} textAlign="center">
Total Transaksi
</Divider>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="totalJumlahBarang"
label="Total Jumlah Barang"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="totalHarga"
label="Total Harga (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 4 }}>
<RHFNumeric
name="totalDiskon"
label="Total Diskon (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalDpp"
label="Jumlah DPP (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalDppLain"
label="Jumlah DPP Lain (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalPpn"
label="Jumlah PPN (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
<Grid size={{ md: 3 }}>
<RHFNumeric
name="totalPpnbm"
label="Jumlah PPnBM (Rp)"
allowDecimalValue
decimalScale={2}
displayOnly
/>
</Grid>
</Grid>
);
export default TotalTransaksi;
import React, { useEffect, useState } from 'react';
import { enqueueSnackbar } from 'notistack';
import DialogProgressBar from 'src/shared/components/dialog/DialogProgressBar';
import useDialogProgressBar from 'src/shared/hooks/useDialogProgressBar';
import DialogConfirm from 'src/shared/components/dialog/DialogConfirm';
import type { GridRowSelectionModel } from '@mui/x-data-grid-premium';
interface ModalDeleteBarangJasaProps {
/** data yang sedang dipilih untuk dihapus */
dataSelected?: GridRowSelectionModel;
/** buka / tutup modal delete */
isOpenDialogDelete: boolean;
setIsOpenDialogDelete: (v: boolean) => void;
/** callback setelah delete sukses */
onConfirmDelete?: () => Promise<void> | void;
/** pesan sukses */
successMessage?: string;
}
/**
* Normalisasi selection model agar selalu jadi array id
*/
const normalizeSelection = (sel?: GridRowSelectionModel): (string | number)[] => {
if (!sel) return [];
// ✅ v7 shape baru: { type: 'include', ids: Set(...) }
if (typeof sel === 'object' && 'ids' in sel && sel.ids instanceof Set) {
return Array.from(sel.ids);
}
// fallback untuk bentuk array lama
if (Array.isArray(sel)) return sel;
// fallback terakhir, kalau object key-value
if (typeof sel === 'object') {
return Object.keys(sel);
}
return [];
};
const ModalDeleteBarangJasa: React.FC<ModalDeleteBarangJasaProps> = ({
dataSelected,
isOpenDialogDelete,
setIsOpenDialogDelete,
onConfirmDelete,
successMessage = 'Data berhasil dihapus',
}) => {
const {
numberOfData,
setNumberOfData,
numberOfDataFail,
numberOfDataProcessed,
numberOfDataSuccess,
processSuccess,
processFail,
resetToDefault,
status,
} = useDialogProgressBar();
const [isOpenDialogProgressBar, setIsOpenDialogProgressBar] = useState(false);
const handleCloseModal = () => {
setIsOpenDialogDelete(false);
resetToDefault();
};
const onSubmit = async () => {
try {
// setIsOpenDialogProgressBar(true);
await onConfirmDelete?.();
processSuccess();
enqueueSnackbar(successMessage, { variant: 'success' });
handleCloseModal();
} catch (error: any) {
processFail();
enqueueSnackbar(error?.message || 'Gagal menghapus data', { variant: 'error' });
}
};
useEffect(() => {
setNumberOfData(normalizeSelection(dataSelected).length || 1);
}, [isOpenDialogDelete, dataSelected, setNumberOfData]);
return (
<>
<DialogConfirm
fullWidth
maxWidth="xs"
title="Apakah Anda yakin ingin menghapus data ini?"
description="Data yang sudah dihapus tidak dapat dikembalikan."
actionTitle="Hapus"
isOpen={isOpenDialogDelete}
isLoadingBtnSubmit={false}
handleClose={handleCloseModal}
handleSubmit={onSubmit}
/>
<DialogProgressBar
isOpen={isOpenDialogProgressBar}
handleClose={() => {
handleCloseModal();
setIsOpenDialogProgressBar(false);
}}
numberOfData={numberOfData}
numberOfDataProcessed={numberOfDataProcessed}
numberOfDataFail={numberOfDataFail}
numberOfDataSuccess={numberOfDataSuccess}
status={status}
/>
</>
);
};
export default ModalDeleteBarangJasa;
import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import HighlightOffTwoToneIcon from '@mui/icons-material/HighlightOffTwoTone';
interface ToolbarCancelProps {
selectedRows: any[];
selectedRowsData: any[];
onCancel: (ids: number[]) => void;
}
const ToolbarCancel: React.FC<ToolbarCancelProps> = ({
selectedRows,
selectedRowsData,
onCancel,
}) => {
// Logic sederhana
const isEnabled =
selectedRows.length > 0 &&
selectedRowsData.every((row: any) => row.fgStatus === 'normal' || row.fgStatus === 'amendment');
const handleClick = () => {
if (!isEnabled) return;
const ids = selectedRowsData.map((row: any) => row.id).filter((id: any) => id !== undefined);
onCancel(ids);
};
return (
<Tooltip title={isEnabled ? `Batalkan ${selectedRows.length} data` : 'Pilih data yang valid'}>
<IconButton
onClick={handleClick}
disabled={!isEnabled}
color={isEnabled ? 'error' : 'default'}
>
<HighlightOffTwoToneIcon />
</IconButton>
</Tooltip>
);
};
export default ToolbarCancel;
This diff is collapsed.
const appRootKey = 'unifikasi';
const queryKey = {
getKodeObjekPajak: (params: any) => [appRootKey, 'kode-objek-pajak', params],
faktuPK: {
all: (params: any) => [appRootKey, 'fakturPk', params],
detail: (params: any) => [appRootKey, 'fakturPk', 'detail', params],
uangMukaDetail: (nomor: string) => ['fakturPK', 'uangMukaDetail', nomor],
draft: [appRootKey, 'fakturPk', 'draft'],
delete: [appRootKey, 'fakturPk', 'delete'],
upload: [appRootKey, 'fakturPk', 'upload'],
cancel: [appRootKey, 'fakturPk', 'cancel'],
cetakPdf: (params: any) => [appRootKey, 'fakturPk-cetak-pdf', params],
},
};
export default queryKey;
import { useMutation } from '@tanstack/react-query';
import type { TCancelRequest, TCancelResponse } from '../types/types';
import fakturApi from '../utils/api';
const useCancelFakturPk = (props?: any) =>
useMutation<TCancelResponse, Error, TCancelRequest>({
mutationKey: ['cancel-faktur-pk'],
mutationFn: (payload) => fakturApi.cancel(payload),
...props,
});
export default useCancelFakturPk;
// import { useMutation } from '@tanstack/react-query';
// import fakturApi from '../utils/api';
// const useCetakPdfFakturPK = (options?: any) =>
// useMutation({
// mutationKey: ['faktur', 'pk', 'cetak-pdf'],
// mutationFn: async (params: any) => fakturApi.cetakPdfDetail(params),
// ...options,
// });
// export default useCetakPdfFakturPK;
import { useMutation } from '@tanstack/react-query';
import fakturApi from '../utils/api';
const useCetakPdfFakturPK = (options?: any) =>
useMutation({
mutationFn: async (params: any) => fakturApi.cetakPdfDetail(params),
...options,
});
export default useCetakPdfFakturPK;
import { useMutation } from '@tanstack/react-query';
import type { TBaseResponseAPI, TDeleteRequest } from '../types/types';
import fakturApi from '../utils/api';
const useDeleteFakturPk = (props?: any) =>
useMutation<TBaseResponseAPI<null>, Error, TDeleteRequest>({
mutationKey: ['delete-faktur-pk'],
mutationFn: (payload) => fakturApi.deleteFakturPK(payload),
...props,
});
export default useDeleteFakturPk;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export * from './faktur-pk-list-view';
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment