mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Compare commits
493 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 584b495e4b | |||
| c8e76a8558 | |||
| a468a5948b | |||
| e05e1a4121 | |||
| e3b86e3033 | |||
| d467c56ea6 | |||
| 784d9f26ab | |||
| 978ef764ea | |||
| 928136ff18 | |||
| 79567e4f1b | |||
| 633deece21 | |||
| 46483af4c2 | |||
| c2653e5068 | |||
| 8569bda7d6 | |||
| 7da63dc542 | |||
| aeceef4361 | |||
| ff6955be54 | |||
| 9389fa0354 | |||
| eccab314b3 | |||
| 37bc7a85e5 | |||
| 667eb41eb2 | |||
| e64b55e527 | |||
| 9710998dc6 | |||
| ca0b216ba0 | |||
| 9ff6f3a35d | |||
| 333dd01f92 | |||
| 75f765ee69 | |||
| 945bdb8b27 | |||
| 77eae32a3d | |||
| a5ebc6d1ae | |||
| 15c7452d7b | |||
| 5dac900a1a | |||
| 4b49cd18f5 | |||
| a4cb4e202b | |||
| 4a69eef294 | |||
| 2de6636bbf | |||
| a7951b6c28 | |||
| 03baba40a6 | |||
| 96ef6f8496 | |||
| 94ab48d3f6 | |||
| ea88c3ce8e | |||
| 267ef9d812 | |||
| b5fc1d4310 | |||
| c98e7d8cb3 | |||
| 1acbc91cfe | |||
| 807041834b | |||
| 4de21561b3 | |||
| 1938f6cbda | |||
| f4a522fc0c | |||
| 7ae04b3f3e | |||
| b786acf71a | |||
| bb28ae8613 | |||
| 080592ff01 | |||
| 6d41203a5e | |||
| 89d7d3ef91 | |||
| 18ebf75aa7 | |||
| 6e7dfebacc | |||
| 4b93cb4589 | |||
| 58f1ab82c7 | |||
| 1fb9687142 | |||
| 4f6d71f1f4 | |||
| d502ec707c | |||
| fa86f488e1 | |||
| 5e5400f56b | |||
| 741884ac29 | |||
| 4e278c5687 | |||
| 3d2e40518b | |||
| cbab7f52f2 | |||
| ceb316d3da | |||
| 9ea152aef9 | |||
| 910ea85b62 | |||
| 968243c370 | |||
| f1ba577a97 | |||
| c6b906a28e | |||
| eafcfd2f28 | |||
| 0e5d38f75c | |||
| f2b59ded3c | |||
| 1341b1ff53 | |||
| 9c4c750664 | |||
| d3501e5f3d | |||
| d96388e5f4 | |||
| 749b7d6f1a | |||
| aadf10b8b9 | |||
| 7db6ae4077 | |||
| 7eaf6b7a3a | |||
| 8397d76171 | |||
| 56d4b8a5c9 | |||
| ef9d820c0d | |||
| 4e3c6736ab | |||
| aa1ef7a559 | |||
| 04b5a7bd4d | |||
| 3aec412599 | |||
| 80a94c48c3 | |||
| 9bd4a73a90 | |||
| d1c6fe8fb4 | |||
| a4378ebd04 | |||
| 16f2f2bc06 | |||
| 64843a36ab | |||
| 396a5ab5ba | |||
| 9f4c041ec8 | |||
| 5e3d2273d1 | |||
| 0b01eefe20 | |||
| 43045b3f8b | |||
| d3ce60d3ba | |||
| 8c7577dcc5 | |||
| da3cadc37d | |||
| 2e6aa1b83a | |||
| 8811976f53 | |||
| 9fb65bdacd | |||
| 6f6f54571f | |||
| 3099588141 | |||
| 47ee911852 | |||
| b4d0ed1537 | |||
| 36f2368e95 | |||
| a4d5cbb117 | |||
| c926a81756 | |||
| 2476b6a4b4 | |||
| 01c1843fd5 | |||
| f86498e350 | |||
| 5cd24f2c46 | |||
| da0be9cb52 | |||
| da1cdfb59e | |||
| a991150262 | |||
| ee49c91fba | |||
| 75463326e3 | |||
| e98c49ac5a | |||
| aeeb0b721c | |||
| b14bc00af4 | |||
| e2a4088e77 | |||
| ebe80358ee | |||
| 9d4e9f6318 | |||
| e5007a285a | |||
| da82c704d5 | |||
| 88b9c890e5 | |||
| 5a67901722 | |||
| 0031a65f97 | |||
| a89e83af29 | |||
| a75d84556a | |||
| 0af7b172a0 | |||
| 8be33b230b | |||
| 4fda2f661a | |||
| 22b1102454 | |||
| e2e64f093f | |||
| 90942b41b9 | |||
| 93a2d99b7f | |||
| dae9a24a7c | |||
| f701ab0d91 | |||
| 47a2439777 | |||
| cce84a3a6f | |||
| f92622cc22 | |||
| f0041ca938 | |||
| 6566b881b2 | |||
| 1a2e38568b | |||
| ced3970aae | |||
| 9de31c991d | |||
| bdca10e0ac | |||
| 9a5e2987d5 | |||
| a5b4deaac4 | |||
| 82953f4c9c | |||
| 509fc5476d | |||
| 6a10849a84 | |||
| 50424a25fc | |||
| 35b09b514f | |||
| 81e4e1fc6a | |||
| 62c9ab014d | |||
| ba28d64562 | |||
| 75ee058818 | |||
| 755bddc74c | |||
| 08aa1900a8 | |||
| 7e1166b5e8 | |||
| 75e9b06a83 | |||
| ac2d83a666 | |||
| 03e0cebe35 | |||
| 1cc0e16c01 | |||
| 1f2f3acebb | |||
| de0f9ae985 | |||
| a0e79168b2 | |||
| 797f88fe15 | |||
| 4c3e7c615f | |||
| b35b6c2ab8 | |||
| bbbd767cf2 | |||
| 3e30dcb04e | |||
| 1a137e7500 | |||
| 3be6d5bb26 | |||
| e22f95cc58 | |||
| 6ac903313c | |||
| a4ff92520a | |||
| 608cf4cbe7 | |||
| 60e360537e | |||
| e9784bd5ed | |||
| 4f018eb2b1 | |||
| 40b3d779bc | |||
| 1bdf413650 | |||
| 495b1b2869 | |||
| a231140bc0 | |||
| a0af934002 | |||
| 82975219a8 | |||
| 60ae670f24 | |||
| 7d79b6b957 | |||
| 8a1e0f080f | |||
| c3a69bc66a | |||
| 4c1f11d859 | |||
| 350ff0fbbe | |||
| 4c70ec7cab | |||
| 944db8dba7 | |||
| befc1c1217 | |||
| 8fe19feaac | |||
| 9c953ca382 | |||
| c53430fa1f | |||
| 1fe722cb81 | |||
| d9bd73d8c1 | |||
| 0235494d46 | |||
| 32354e3c2d | |||
| 14e1c59a69 | |||
| 42cc0f2661 | |||
| 2f5d518e15 | |||
| d085b18788 | |||
| d68bedf5ce | |||
| 2169c0ea62 | |||
| 02165df89c | |||
| 15289951e6 | |||
| 62674044e7 | |||
| e94967ea4c | |||
| ed576fc8eb | |||
| d4c6a05c0c | |||
| da27f4c581 | |||
| 9d6cc90162 | |||
| 512ccddfc7 | |||
| f5b16b68e9 | |||
| e8e4f7b877 | |||
| b6edd8f10c | |||
| ec3a0367dd | |||
| e9da5210ad | |||
| 67f2a80f23 | |||
| ceb594a4cc | |||
| d312da4c66 | |||
| 3a676723e4 | |||
| 684f67593f | |||
| d5962f94a1 | |||
| 5c00893ea3 | |||
| 211622c7b0 | |||
| dbb523c710 | |||
| 6aae18df54 | |||
| cb171118ee | |||
| 45ac8348fe | |||
| 5d92e6774e | |||
| 6595ff7a6e | |||
| dc4e945a35 | |||
| b154b478bc | |||
| 510573e66f | |||
| dbcf469123 | |||
| 325fb373a8 | |||
| 4b6a8b2773 | |||
| 5e4619fac7 | |||
| 43d26b4833 | |||
| 6d2855d117 | |||
| ee53ea61cc | |||
| 322b519def | |||
| e23b53d797 | |||
| fd78ca6ac1 | |||
| 28dabcbeb6 | |||
| 62dd1de150 | |||
| 166e95930b | |||
| 52d58d0921 | |||
| 14d0dc590f | |||
| ed781da372 | |||
| 4e5745d237 | |||
| b03ef4923e | |||
| d7486e8b8a | |||
| 498602a2c9 | |||
| 1b4d373fea | |||
| 4215b0ea7d | |||
| c3dee6b292 | |||
| 3834982fca | |||
| 539de03a5b | |||
| 0f1d2ce477 | |||
| 70b63f7773 | |||
| 02d13efc25 | |||
| 1227b7639f | |||
| 5593463eab | |||
| be7b2a0f93 | |||
| 4c6ac6e8e1 | |||
| 5cc51c52d9 | |||
| 59eb781a22 | |||
| 2af83bed8a | |||
| 4775c1e115 | |||
| d0dea834c1 | |||
| def894e5f4 | |||
| 4f9401ed34 | |||
| 80763acc53 | |||
| 5fb065de3e | |||
| d6b9161500 | |||
| bcc2070ed2 | |||
| e4e6e563c9 | |||
| efec9b6265 | |||
| 4cf2f77265 | |||
| c86f0379b5 | |||
| 606380460e | |||
| a3bcabe5c2 | |||
| 89ffad398f | |||
| 35986aab56 | |||
| 4717330bc8 | |||
| 291eee3bce | |||
| e6a572ac17 | |||
| bd5b614bf8 | |||
| ba0753428d | |||
| 862cf38f92 | |||
| 1dc6ffca5c | |||
| b7fd5d3569 | |||
| 911136981a | |||
| 6cbe14b36e | |||
| 80c79cc14b | |||
| cb498b01d9 | |||
| cd95b1f8ff | |||
| 60ace68dae | |||
| a8dce9da46 | |||
| b85e47f601 | |||
| fc4a0a58e2 | |||
| 039dfd529e | |||
| 3b42709577 | |||
| 3dee5c1828 | |||
| 5ac958231a | |||
| 54a6e7e247 | |||
| 4e80c1a703 | |||
| 9ee5e95d0b | |||
| 3bacc59dc6 | |||
| 4d23929924 | |||
| c9a5a91970 | |||
| 08d1447d11 | |||
| 304be4f432 | |||
| 5e9ce70320 | |||
| 42088e51a8 | |||
| 9dc8f05534 | |||
| e16fa9a167 | |||
| 869110ad2e | |||
| d415bbba82 | |||
| 1ecca83339 | |||
| a6c827bb40 | |||
| 968d9e1f2a | |||
| 7b9ba48204 | |||
| 6e2e9da1be | |||
| 980a5674e2 | |||
| f5b9c52e71 | |||
| ade8fefe0d | |||
| 6b54b49443 | |||
| 8fb16903f8 | |||
| f0637e2ce9 | |||
| f6cf4a29ad | |||
| 66f017549c | |||
| db7219e261 | |||
| ac6c77bb92 | |||
| d5eeadc9a7 | |||
| 70a9fa15ec | |||
| 4fd4374e64 | |||
| b4353cf834 | |||
| eb95afe9a0 | |||
| 92886fe5e2 | |||
| fb1b310d1d | |||
| 3b221795ba | |||
| d41600d8e2 | |||
| 856674de75 | |||
| 1af2b72bea | |||
| e66f30e703 | |||
| ca32af592f | |||
| 372b439ff0 | |||
| 4aa9d54b1e | |||
| b45c7c8ea6 | |||
| c164977bb9 | |||
| 3153423f14 | |||
| ac3fbedccd | |||
| 9004de06fa | |||
| 4d7bd5213e | |||
| 2f1c4e3c87 | |||
| 43dcbf73ee | |||
| cb22fd1037 | |||
| dfd86a04e0 | |||
| 09cd6395e6 | |||
| a8c9b697e3 | |||
| 25a89b8987 | |||
| 760e9ccd89 | |||
| c019162390 | |||
| 428d9f33d9 | |||
| c3f4c9b1ec | |||
| 2918500585 | |||
| b48bf06f7d | |||
| 2b343b893e | |||
| 0c25751401 | |||
| 9be5bb35c4 | |||
| c24cf7ed1a | |||
| 7b398bc2ee | |||
| d9ee2bc7be | |||
| f31eb8db59 | |||
| 40411b0417 | |||
| fbbe5532a0 | |||
| c102a4043c | |||
| ab694270c4 | |||
| 0febc9d8f3 | |||
| 1ee92f1064 | |||
| 15e53d2c2d | |||
| 0093410d5b | |||
| 7f4eb2ad35 | |||
| f9e00a3f4f | |||
| 2d13564538 | |||
| 4fef966428 | |||
| e29097499a | |||
| d4c8f9bbbc | |||
| 43afd35e54 | |||
| 489e8a31f3 | |||
| 12c7e56604 | |||
| eb6071aaf8 | |||
| f01765d2f8 | |||
| e5d6be446a | |||
| bb83acbe81 | |||
| 758a0cd9a7 | |||
| 1a938b4218 | |||
| dc5bd6b329 | |||
| 68f3c95b81 | |||
| d826746f29 | |||
| 39b18f7efc | |||
| e123ca9b13 | |||
| 17589cb2b4 | |||
| 0c0ad04c20 | |||
| d327af814f | |||
| 8637d1c2c2 | |||
| 68feef77fc | |||
| 4dec97b57c | |||
| d776c73a03 | |||
| dc26da7404 | |||
| 2425316fea | |||
| ff2e0ed114 | |||
| 0efae3089d | |||
| 1a9f5424a5 | |||
| c19a7cba68 | |||
| ec7427b948 | |||
| e169068e1c | |||
| 85556c0db0 | |||
| 7ac92ff451 | |||
| 448cf5ceae | |||
| 995d20bdf3 | |||
| 3ca4b324d3 | |||
| 8fa2a444f0 | |||
| 4a1464185b | |||
| 448fb51c81 | |||
| a57f65a420 | |||
| 70bb40d4f2 | |||
| 66c8f67245 | |||
| 5b167db6c3 | |||
| c6b9ed4f12 | |||
| 9928997dd8 | |||
| d92a63db41 | |||
| 73319bbdfa | |||
| d12374e5d9 | |||
| 507cf710e1 | |||
| 8ce72b21e1 | |||
| 92cc082c54 | |||
| 24f2b94bb8 | |||
| 67cf459216 | |||
| e3a2b41342 | |||
| fcea485286 | |||
| 0b7ff6f3f1 | |||
| 190792affe | |||
| 0a81790049 | |||
| b9b02f285f | |||
| d3be683b69 | |||
| 2c27b2e41d | |||
| 8b0dd6deea | |||
| a5613980c0 | |||
| 12a02cd15d | |||
| aecf470173 | |||
| 64bd57cad4 | |||
| ae0d03ddc0 | |||
| 2a12bc4ba4 | |||
| ace3018539 | |||
| fbf7cb2d21 | |||
| c0b1da89f1 | |||
| 4b0397903d | |||
| 48a7cd168a | |||
| 93b2496f55 | |||
| 15c167d24d | |||
| 8947d48a43 | |||
| 3de19d495e | |||
| 8bf0f067bb | |||
| 970278f684 | |||
| 99f8e5dcf3 | |||
| d40d352418 | |||
| 7956ce5b6f | |||
| 8babb4e3d7 | |||
| de8fda9360 | |||
| b2670f76bf | |||
| 89f711241d | |||
| 4e801bf744 | |||
| c5269c4fc5 | |||
| 4e00ded843 |
@@ -3,11 +3,10 @@
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import ClosingDetail from '@/components/pages/closing/ClosingDetail';
|
import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs';
|
||||||
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { FlockApi } from '@/services/api/master-data';
|
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
|
||||||
@@ -34,33 +33,6 @@ const ClosingDetailPage = () => {
|
|||||||
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
() => ProjectFlockKandangApi.getSingle(Number(kandangId))
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: salesData, isLoading: isLoadingSales } = useSWR(
|
|
||||||
kandangId
|
|
||||||
? `sales-${closingId}-${kandangId}`
|
|
||||||
: closingId
|
|
||||||
? `sales-${closingId}`
|
|
||||||
: null,
|
|
||||||
() =>
|
|
||||||
kandangId
|
|
||||||
? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId))
|
|
||||||
: ClosingApi.getPenjualan(Number(closingId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
|
|
||||||
kandangId
|
|
||||||
? `hpp-ekspedisi-${closingId}-${kandangId}`
|
|
||||||
: closingId
|
|
||||||
? `hpp-ekspedisi-${closingId}`
|
|
||||||
: null,
|
|
||||||
() =>
|
|
||||||
kandangId
|
|
||||||
? ClosingApi.getHppEkspedisiByKandang(
|
|
||||||
Number(closingId),
|
|
||||||
Number(kandangId)
|
|
||||||
)
|
|
||||||
: ClosingApi.getHppEkspedisi(Number(closingId))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!closingId) {
|
if (!closingId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
@@ -76,12 +48,7 @@ const ClosingDetailPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading =
|
const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang;
|
||||||
isLoadingClosing ||
|
|
||||||
isLoadingSales ||
|
|
||||||
isLoadingHppEkspedisi ||
|
|
||||||
isLoadingProject ||
|
|
||||||
isLoadingKandang;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
@@ -91,12 +58,6 @@ const ClosingDetailPage = () => {
|
|||||||
<ClosingDetail
|
<ClosingDetail
|
||||||
id={Number(closingId)}
|
id={Number(closingId)}
|
||||||
initialValue={closing.data}
|
initialValue={closing.data}
|
||||||
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
|
||||||
hppExpeditionData={
|
|
||||||
isResponseSuccess(hppEkspedisiData)
|
|
||||||
? hppEkspedisiData.data
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
projectData={
|
projectData={
|
||||||
isResponseSuccess(projectData) ? projectData.data : undefined
|
isResponseSuccess(projectData) ? projectData.data : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable';
|
|||||||
|
|
||||||
const Closing = () => {
|
const Closing = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='w-full p-3'>
|
||||||
<ClosingsTable />
|
<ClosingsTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import useSWR from 'swr';
|
|||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||||
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
|
||||||
|
|
||||||
const EditFinanceTransactionPage = () => {
|
const EditFinanceTransactionPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import FinanceDetail from '@/components/pages/finance/FinanceDetail';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
const FinanceDetailPage = () => {
|
const FinanceDetailPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
||||||
|
|
||||||
const Finance = () => {
|
const Finance = () => {
|
||||||
return (
|
return <FinanceTable />;
|
||||||
<section className='size-full p-6'>
|
|
||||||
<div className='flex flex-row gap-4'></div>
|
|
||||||
<FinanceTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Finance;
|
export default Finance;
|
||||||
|
|||||||
@@ -68,6 +68,8 @@
|
|||||||
|
|
||||||
--shadow-button-soft:
|
--shadow-button-soft:
|
||||||
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
|
0 3px 2px -2px var(--color-base-200), 0 4px 3px -2px var(--color-base-200);
|
||||||
|
|
||||||
|
--shadow-bg: 0px -2px 4px 0px #00000014;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import InventoryAdjustmentTable from '@/components/pages/inventory/adjustment/In
|
|||||||
|
|
||||||
const InventoryAdjustment = () => {
|
const InventoryAdjustment = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='w-full'>
|
||||||
<InventoryAdjustmentTable />
|
<InventoryAdjustmentTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const EditMarketingDelivery = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const soId = searchParams.get('marketingId');
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: marketing,
|
|
||||||
isLoading: isLoading,
|
|
||||||
mutate: refreshMarketing,
|
|
||||||
} = useSWR(`get-so-${soId}`, () =>
|
|
||||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!soId) {
|
|
||||||
router.back();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4'>
|
|
||||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoading && isResponseSuccess(marketing) && (
|
|
||||||
<MarketingForm
|
|
||||||
formType='add_deliver'
|
|
||||||
initialValues={marketing.data}
|
|
||||||
afterSubmit={() => {
|
|
||||||
refreshMarketing();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default EditMarketingDelivery;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
|
||||||
|
|
||||||
const AddSalesOrder = () => {
|
|
||||||
return (
|
|
||||||
<div className='size-full p-4'>
|
|
||||||
<MarketingForm formType='add' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddSalesOrder;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const EditMarketingDelivery = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const soId = searchParams.get('marketingId');
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: marketing,
|
|
||||||
isLoading: isLoading,
|
|
||||||
mutate: refreshMarketing,
|
|
||||||
} = useSWR(`get-so-${soId}`, () =>
|
|
||||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!soId) {
|
|
||||||
router.back();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isResponseSuccess(marketing) &&
|
|
||||||
marketing.data.latest_approval.step_number != 3
|
|
||||||
) {
|
|
||||||
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
|
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4'>
|
|
||||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoading && isResponseSuccess(marketing) && (
|
|
||||||
<MarketingForm
|
|
||||||
formType='edit_deliver'
|
|
||||||
initialValues={marketing.data}
|
|
||||||
afterSubmit={() => {
|
|
||||||
refreshMarketing();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default EditMarketingDelivery;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const DetailMarketing = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const soId = searchParams.get('marketingId');
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: marketing,
|
|
||||||
isLoading: isLoading,
|
|
||||||
mutate: refreshMarketing,
|
|
||||||
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
|
|
||||||
|
|
||||||
if (!soId) {
|
|
||||||
router.back();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4'>
|
|
||||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoading && isResponseSuccess(marketing) && (
|
|
||||||
<MarketingDetail
|
|
||||||
initialValues={marketing.data}
|
|
||||||
refresh={refreshMarketing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DetailMarketing;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const EditSalesOrder = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const soId = searchParams.get('marketingId');
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: marketing,
|
|
||||||
isLoading: isLoading,
|
|
||||||
mutate: refreshMarketing,
|
|
||||||
} = useSWR(`get-so-${soId}`, () =>
|
|
||||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!soId) {
|
|
||||||
router.back();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4'>
|
|
||||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoading && isResponseSuccess(marketing) && (
|
|
||||||
<MarketingForm
|
|
||||||
formType='edit'
|
|
||||||
initialValues={marketing.data}
|
|
||||||
afterSubmit={() => {
|
|
||||||
refreshMarketing();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default EditSalesOrder;
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import DeliveryOrderFormModal from '@/components/pages/marketing/DeliveryOrderFormModal';
|
||||||
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
||||||
|
import SalesOrderFormModal from '@/components/pages/marketing/SalesOrderFormModal';
|
||||||
|
|
||||||
const Marketing = () => {
|
const Marketing = () => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4'>
|
<div className='w-full'>
|
||||||
<MarketingTable />
|
<MarketingTable />
|
||||||
|
|
||||||
|
<SalesOrderFormModal />
|
||||||
|
<DeliveryOrderFormModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import AreasTable from '@/components/pages/master-data/area/AreasTable';
|
import AreasTable from '@/components/pages/master-data/area/AreasTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return (
|
return <AreasTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<AreasTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import BanksTable from '@/components/pages/master-data/bank/BanksTable';
|
import BanksTable from '@/components/pages/master-data/bank/BanksTable';
|
||||||
|
|
||||||
const Bank = () => {
|
const Bank = () => {
|
||||||
return (
|
return <BanksTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<BanksTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Bank;
|
export default Bank;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
|
import CustomersTable from '@/components/pages/master-data/customer/CustomersTable';
|
||||||
|
|
||||||
const Customer = () => {
|
const Customer = () => {
|
||||||
return (
|
return <CustomersTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<CustomersTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Customer;
|
export default Customer;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
|
||||||
|
|
||||||
const AddFcr = () => {
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
|
||||||
<FcrForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddFcr;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
|
||||||
|
|
||||||
import { FcrApi } from '@/services/api/master-data';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
|
||||||
|
|
||||||
const FcrEdit = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const fcrId = searchParams.get('fcrId');
|
|
||||||
|
|
||||||
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
|
||||||
fcrId,
|
|
||||||
(id: number) =>
|
|
||||||
FcrApi.getSingle(id) as Promise<
|
|
||||||
BaseApiResponse<FcrWithStandards> | undefined
|
|
||||||
>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fcrId) {
|
|
||||||
router.back();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
|
||||||
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
|
||||||
<FcrForm type='edit' initialValues={fcr.data} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FcrEdit;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) => {
|
|
||||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
|
||||||
|
|
||||||
import { FcrApi } from '@/services/api/master-data';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
|
|
||||||
const FcrDetail = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const fcrId = searchParams.get('fcrId');
|
|
||||||
|
|
||||||
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
|
||||||
fcrId,
|
|
||||||
(id: number) =>
|
|
||||||
FcrApi.getSingle(id) as Promise<
|
|
||||||
BaseApiResponse<FcrWithStandards> | undefined
|
|
||||||
>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fcrId) {
|
|
||||||
router.back();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
||||||
<span className='loading loading-spinner loading-xl' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
|
||||||
router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
|
||||||
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
|
||||||
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
|
||||||
<FcrForm type='detail' initialValues={fcr.data} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FcrDetail;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable';
|
|
||||||
|
|
||||||
const Fcr = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full p-4'>
|
|
||||||
<FcrsTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Fcr;
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
|
import FlockTable from '@/components/pages/master-data/flock/FlocksTable';
|
||||||
|
|
||||||
const Flock = () => {
|
const Flock = () => {
|
||||||
return (
|
return <FlockTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<FlockTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Flock;
|
export default Flock;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable';
|
import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return (
|
return <KandangsTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<KandangsTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import LocationsTable from '@/components/pages/master-data/location/LocationsTable';
|
import LocationsTable from '@/components/pages/master-data/location/LocationsTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return (
|
return <LocationsTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<LocationsTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable';
|
import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return (
|
return <NonstocksTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<NonstocksTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
|
import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable';
|
||||||
|
|
||||||
const ProductCategory = () => {
|
const ProductCategory = () => {
|
||||||
return (
|
return <ProductCategoryTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<ProductCategoryTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductCategory;
|
export default ProductCategory;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
|
import ProductsTable from '@/components/pages/master-data/product/ProductTable';
|
||||||
|
|
||||||
const Product = () => {
|
const Product = () => {
|
||||||
return (
|
return <ProductsTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<ProductsTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Product;
|
export default Product;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
|
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
|
||||||
|
|
||||||
const ProductionStandardPage = () => {
|
const ProductionStandardPage = () => {
|
||||||
return (
|
return <ProductionStandardTable />;
|
||||||
<div className='w-full'>
|
|
||||||
<ProductionStandardTable />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductionStandardPage;
|
export default ProductionStandardPage;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
|
import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable';
|
||||||
|
|
||||||
const Supplier = () => {
|
const Supplier = () => {
|
||||||
return (
|
return <SuppliersTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<SuppliersTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Supplier;
|
export default Supplier;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import UomsTable from '@/components/pages/master-data/uom/UomsTable';
|
import UomsTable from '@/components/pages/master-data/uom/UomsTable';
|
||||||
|
|
||||||
const Nonstock = () => {
|
const Nonstock = () => {
|
||||||
return (
|
return <UomsTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<UomsTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Nonstock;
|
export default Nonstock;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable';
|
import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable';
|
||||||
|
|
||||||
const Warehouse = () => {
|
const Warehouse = () => {
|
||||||
return (
|
return <WarehousesTable />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<WarehousesTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Warehouse;
|
export default Warehouse;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import PageNotFound from '@/components/helper/NotFoundPage';
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return <PageNotFound />;
|
||||||
|
}
|
||||||
+1
-2
@@ -3,10 +3,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { redirectToSSO } from '@/lib/auth-helper';
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user, isLoadingUser } = useAuth();
|
const { isLoadingUser } = useAuth();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
||||||
import React, { useImperativeHandle } from 'react';
|
import React from 'react';
|
||||||
import toast from 'react-hot-toast';
|
// import React, { useImperativeHandle } from 'react';
|
||||||
|
|
||||||
const AddProjectFlock = () => {
|
const AddProjectFlock = () => {
|
||||||
// useImperativeHandle(ref, () => ({
|
// useImperativeHandle(ref, () => ({
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ const ProjectFlockEdit = () => {
|
|||||||
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
const {
|
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||||
data: projectFlock,
|
projectFlockId,
|
||||||
isLoading: isLoadingProjectFlock,
|
(id: number) => ProjectFlockApi.getSingle(id)
|
||||||
mutate: refreshProjectFlocks,
|
);
|
||||||
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
|
||||||
|
|
||||||
if (!projectFlockId) {
|
if (!projectFlockId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
|
import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail';
|
||||||
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
@@ -13,11 +12,10 @@ const ProjectFlockDetailPage = () => {
|
|||||||
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
const projectFlockId = searchParams.get('projectFlockId');
|
||||||
|
|
||||||
const {
|
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
|
||||||
data: projectFlock,
|
projectFlockId,
|
||||||
isLoading: isLoadingProjectFlock,
|
(id: number) => ProjectFlockApi.getSingle(id)
|
||||||
mutate: refreshProjectFlock,
|
);
|
||||||
} = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id));
|
|
||||||
|
|
||||||
if (!projectFlockId) {
|
if (!projectFlockId) {
|
||||||
router.back();
|
router.back();
|
||||||
@@ -50,5 +48,3 @@ const ProjectFlockDetailPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectFlockDetailPage;
|
export default ProjectFlockDetailPage;
|
||||||
ProjectFlockDetail;
|
|
||||||
ProjectFlockDetail;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import Drawer from '@/components/Drawer';
|
import React, { ReactNode, useEffect } from 'react';
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
|
||||||
export default function ProjectFlockLayout({
|
export default function ProjectFlockLayout({
|
||||||
children,
|
children,
|
||||||
@@ -23,9 +23,12 @@ export default function ProjectFlockLayout({
|
|||||||
|
|
||||||
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
|
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
|
||||||
|
|
||||||
|
const formModal = useModal();
|
||||||
|
|
||||||
const handleBackdropClick = () => {
|
const handleBackdropClick = () => {
|
||||||
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
|
formModal.closeModal();
|
||||||
unsub(); // berhenti listen
|
unsub(); // berhenti listen
|
||||||
router.push('/production/project-flock');
|
router.push('/production/project-flock');
|
||||||
}
|
}
|
||||||
@@ -34,6 +37,14 @@ export default function ProjectFlockLayout({
|
|||||||
toggleValidate();
|
toggleValidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && !formModal.open) {
|
||||||
|
formModal.openModal();
|
||||||
|
} else {
|
||||||
|
formModal.closeModal();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* List page always rendered */}
|
{/* List page always rendered */}
|
||||||
@@ -43,18 +54,19 @@ export default function ProjectFlockLayout({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render Drawer only on /add */}
|
{/* Render Modal only on /add */}
|
||||||
<Drawer
|
<Modal
|
||||||
open={isOpen}
|
ref={formModal.ref}
|
||||||
setOpen={(v) => {
|
position='end'
|
||||||
if (!v) router.push('/production/project-flock');
|
|
||||||
}}
|
|
||||||
closeOnBackdropClick={isDetail ? true : false}
|
|
||||||
onBackdropClick={handleBackdropClick}
|
onBackdropClick={handleBackdropClick}
|
||||||
variant='right'
|
className={{
|
||||||
zIndex='99999'
|
modalBox: 'w-full sm:w-fit p-3 rounded-xl bg-transparent shadow-none',
|
||||||
sidebarContent={isOpen && <div className=''>{children}</div>}
|
}}
|
||||||
/>
|
>
|
||||||
|
<div className='w-full sm:w-[446px] h-full flex flex-col sm:flex-row items-stretch bg-base-100 rounded-xl overflow-hidden'>
|
||||||
|
{isOpen && children}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import RecordingTable from '@/components/pages/production/recording/RecordingTab
|
|||||||
|
|
||||||
const Recording = () => {
|
const Recording = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4 sm:p-0'>
|
<section className='w-full'>
|
||||||
<RecordingTable />
|
<RecordingTable />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
|
import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
|
||||||
import TransferToLayingFormModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFormModal';
|
import TransferToLayingFormModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFormModal';
|
||||||
import TransferToLayingDetailModal from '@/components/pages/production/transfer-to-laying/TransferToLayingDetailModal';
|
import TransferToLayingDetailModal from '@/components/pages/production/transfer-to-laying/TransferToLayingDetailModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const TransferToLaying = () => {
|
const TransferToLaying = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
<TransferToLayingsTable />
|
<TransferToLayingsTable />
|
||||||
|
|
||||||
<TransferToLayingFormModal />
|
<RequirePermission
|
||||||
|
permissions={[
|
||||||
|
'lti.production.transfer_to_laying.create',
|
||||||
|
'lti.production.transfer_to_laying.update',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TransferToLayingFormModal />
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
<TransferToLayingDetailModal />
|
<RequirePermission permissions='lti.production.transfer_to_laying.detail'>
|
||||||
|
<TransferToLayingDetailModal />
|
||||||
|
</RequirePermission>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable';
|
import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs';
|
||||||
|
|
||||||
const ReportExpense = () => {
|
const ReportExpense = () => {
|
||||||
return (
|
return <ReportExpenseTabs />;
|
||||||
<div className='w-full p-4'>
|
|
||||||
<ReportExpenseTable />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReportExpense;
|
export default ReportExpense;
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs';
|
||||||
|
|
||||||
const MarketingReportPage = () => {
|
const MarketingReportPage = () => {
|
||||||
return (
|
return <MarketingReportContent />;
|
||||||
<section className='w-full p-4'>
|
|
||||||
<MarketingReportContent />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarketingReportPage;
|
export default MarketingReportPage;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
|
import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs';
|
||||||
|
|
||||||
const ProductionResultReportPage = () => {
|
const ProductionResultReportPage = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full max-w-full'>
|
||||||
<ProductionResultContent />
|
<ProductionResultTabs />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode, Ref } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
interface AlertProps {
|
interface AlertProps {
|
||||||
|
ref?: Ref<HTMLDivElement> | undefined;
|
||||||
variant?: 'outline' | 'dash' | 'soft';
|
variant?: 'outline' | 'dash' | 'soft';
|
||||||
color?: 'info' | 'success' | 'warning' | 'error';
|
color?: 'info' | 'success' | 'warning' | 'error';
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Alert = ({ children, variant, color, className }: AlertProps) => {
|
const Alert = ({ children, ref, variant, color, className }: AlertProps) => {
|
||||||
const alertBaseClassName = cn('alert', {
|
const alertBaseClassName = cn('alert', {
|
||||||
'alert-soft': variant === 'soft',
|
'alert-soft': variant === 'soft',
|
||||||
'alert-outline': variant === 'outline',
|
'alert-outline': variant === 'outline',
|
||||||
@@ -21,7 +22,11 @@ const Alert = ({ children, variant, color, className }: AlertProps) => {
|
|||||||
'alert-error': color === 'error',
|
'alert-error': color === 'error',
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={cn(alertBaseClassName, className)}>{children}</div>;
|
return (
|
||||||
|
<div ref={ref} className={cn(alertBaseClassName, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Alert;
|
export default Alert;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ interface DrawerProps {
|
|||||||
onBackdropClick?: () => void;
|
onBackdropClick?: () => void;
|
||||||
closeOnBackdropClick?: boolean;
|
closeOnBackdropClick?: boolean;
|
||||||
expandedContent?: ReactNode;
|
expandedContent?: ReactNode;
|
||||||
expandedWidth?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DrawerClassName = {
|
type DrawerClassName = {
|
||||||
@@ -25,6 +24,7 @@ type DrawerClassName = {
|
|||||||
drawerSide?: string;
|
drawerSide?: string;
|
||||||
drawerOverlay?: string;
|
drawerOverlay?: string;
|
||||||
drawerSidebarContent?: string;
|
drawerSidebarContent?: string;
|
||||||
|
drawerExpandedContent?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Drawer = ({
|
const Drawer = ({
|
||||||
@@ -39,7 +39,6 @@ const Drawer = ({
|
|||||||
onBackdropClick,
|
onBackdropClick,
|
||||||
closeOnBackdropClick = true,
|
closeOnBackdropClick = true,
|
||||||
expandedContent,
|
expandedContent,
|
||||||
expandedWidth = 'w-[400px]',
|
|
||||||
}: DrawerProps) => {
|
}: DrawerProps) => {
|
||||||
const getDrawerClassNames = (): DrawerClassName => {
|
const getDrawerClassNames = (): DrawerClassName => {
|
||||||
const baseClassNames = {
|
const baseClassNames = {
|
||||||
@@ -56,6 +55,9 @@ const Drawer = ({
|
|||||||
? 'w-full lg:min-w-[600px] lg:max-w-[600px]'
|
? 'w-full lg:min-w-[600px] lg:max-w-[600px]'
|
||||||
: 'w-full max-w-[300px] lg:w-[300px]';
|
: 'w-full max-w-[300px] lg:w-[300px]';
|
||||||
}
|
}
|
||||||
|
if (className?.drawerSidebarContent) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
return 'w-full sm:min-w-120 sm:w-fit';
|
return 'w-full sm:min-w-120 sm:w-fit';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +108,9 @@ const Drawer = ({
|
|||||||
if (closeOnBackdropClick) {
|
if (closeOnBackdropClick) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
onBackdropClick && onBackdropClick();
|
if (onBackdropClick) {
|
||||||
|
onBackdropClick();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -174,7 +178,7 @@ const Drawer = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-l border-gray-200 bg-white flex flex-col h-full',
|
'border-l border-gray-200 bg-white flex flex-col h-full',
|
||||||
expandedWidth
|
className?.drawerExpandedContent
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='overflow-y-auto flex-1 h-full'>
|
<div className='overflow-y-auto flex-1 h-full'>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
@@ -13,7 +12,6 @@ import PermissionNotFound from '@/components/helper/PermissionNotFound';
|
|||||||
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||||
import { isPathActive } from '@/lib/helper';
|
|
||||||
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
@@ -74,6 +72,8 @@ const MainDrawer = ({
|
|||||||
|
|
||||||
const formattedPathname = pathname.endsWith('/') ? pathname : `${pathname}/`;
|
const formattedPathname = pathname.endsWith('/') ? pathname : `${pathname}/`;
|
||||||
|
|
||||||
|
const isPathnameNotFoundPage = formattedPathname === '/404/';
|
||||||
|
|
||||||
const isPermitted = ROUTE_PERMISSIONS[formattedPathname]?.some((permission) =>
|
const isPermitted = ROUTE_PERMISSIONS[formattedPathname]?.some((permission) =>
|
||||||
permissionCheck(permission)
|
permissionCheck(permission)
|
||||||
);
|
);
|
||||||
@@ -82,10 +82,14 @@ const MainDrawer = ({
|
|||||||
setMainDrawerOpen(!mainDrawerOpen);
|
setMainDrawerOpen(!mainDrawerOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isPermitted) {
|
if (!isPermitted && !isPathnameNotFoundPage) {
|
||||||
return <PermissionNotFound />;
|
return <PermissionNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPathnameNotFoundPage) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={mainDrawerOpen}
|
open={mainDrawerOpen}
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ export const useModal = (isNestingModal = false) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
open ? closeModal() : openModal();
|
if (open) {
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
openModal();
|
||||||
|
}
|
||||||
}, [open, closeModal, openModal]);
|
}, [open, closeModal, openModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -26,13 +26,17 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => {
|
|||||||
|
|
||||||
const logoutClickHandler = async () => {
|
const logoutClickHandler = async () => {
|
||||||
const logoutRes = await AuthApi.logout();
|
const logoutRes = await AuthApi.logout();
|
||||||
|
|
||||||
if (isResponseError(logoutRes)) {
|
if (isResponseError(logoutRes)) {
|
||||||
toast.error('Gagal logout! Coba lagi!');
|
toast.error('Gagal logout! Coba lagi!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
|
const redirect = (logoutRes as { redirect?: string })?.redirect;
|
||||||
|
if (redirect) {
|
||||||
|
window.location.href = redirect;
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
|
router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const emptyContentDefaultValue = (
|
const emptyContentDefaultValue = (
|
||||||
<div className='w-full p-5 text-center'>
|
<div className='w-full text-center py-4'>
|
||||||
<span className='text-lg opacity-50'>
|
<span className='text-sm opacity-50'>
|
||||||
Tidak ada data yang dapat ditampilkan...
|
Tidak ada data yang dapat ditampilkan...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -452,6 +452,20 @@ const Table = <TData extends object>({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
|
||||||
|
!isLoading && (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={
|
||||||
|
table.getAllLeafColumns().length + (withCheckbox ? 1 : 0)
|
||||||
|
}
|
||||||
|
className='p-0'
|
||||||
|
>
|
||||||
|
{emptyContent}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot
|
<tfoot
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -489,10 +503,6 @@ const Table = <TData extends object>({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(data.length === 0 || table.getRowModel().rows.length === 0) &&
|
|
||||||
!isLoading &&
|
|
||||||
emptyContent}
|
|
||||||
|
|
||||||
{data.length > 0 &&
|
{data.length > 0 &&
|
||||||
table.getRowModel().rows.length > 0 &&
|
table.getRowModel().rows.length > 0 &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export interface TabItem {
|
export interface TabItem {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Button from '@/components/Button';
|
|||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
|
|
||||||
interface ApprovalStepsV2Props {
|
interface ApprovalStepsV2Props {
|
||||||
|
title?: string;
|
||||||
approvals?: BaseApproval[];
|
approvals?: BaseApproval[];
|
||||||
steps: {
|
steps: {
|
||||||
step_number: number;
|
step_number: number;
|
||||||
@@ -23,6 +24,7 @@ interface ApprovalStepsV2Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ApprovalStepsV2 = ({
|
const ApprovalStepsV2 = ({
|
||||||
|
title = 'Progress Details',
|
||||||
approvals,
|
approvals,
|
||||||
steps,
|
steps,
|
||||||
maxVisibleSteps = 2,
|
maxVisibleSteps = 2,
|
||||||
@@ -99,7 +101,7 @@ const ApprovalStepsV2 = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
Progress Details
|
{title}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,15 +3,51 @@ import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { FormikValues } from 'formik';
|
import { FormikValues } from 'formik';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export type ButtonFilterProps = ButtonProps & {
|
export type ButtonFilterProps = ButtonProps & {
|
||||||
values: FormikValues;
|
values: FormikValues;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
excludeFields?: string[];
|
||||||
|
fieldGroups?: string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
||||||
|
|
||||||
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
const ButtonFilter = ({
|
||||||
|
values,
|
||||||
|
onClick,
|
||||||
|
excludeFields = [],
|
||||||
|
fieldGroups = [],
|
||||||
|
...props
|
||||||
|
}: ButtonFilterProps) => {
|
||||||
|
const activeCount = useMemo(() => {
|
||||||
|
const filteredValues: FormikValues = {};
|
||||||
|
Object.keys(values).forEach((key) => {
|
||||||
|
if (!excludeFields.includes(key)) {
|
||||||
|
filteredValues[key] = values[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let count = getFilledFormikValuesCount(filteredValues);
|
||||||
|
|
||||||
|
fieldGroups.forEach((group) => {
|
||||||
|
const groupFields = group.filter(
|
||||||
|
(field) => !excludeFields.includes(field)
|
||||||
|
);
|
||||||
|
const filledGroupFields = groupFields.filter(
|
||||||
|
(field) => filteredValues[field]
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
filledGroupFields.length === groupFields.length &&
|
||||||
|
groupFields.length > 1
|
||||||
|
) {
|
||||||
|
count -= groupFields.length - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}, [values, excludeFields, fieldGroups]);
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
@@ -21,7 +57,7 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
|
'rounded-lg max-h-10 font-semibold text-sm gap-1.5',
|
||||||
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
||||||
getFilledFormikValuesCount(values) > 0
|
activeCount > 0
|
||||||
? 'border-primary-gradient text-primary rounded-lg!'
|
? 'border-primary-gradient text-primary rounded-lg!'
|
||||||
: 'rounded-lg',
|
: 'rounded-lg',
|
||||||
props.className
|
props.className
|
||||||
@@ -31,14 +67,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
|||||||
icon='heroicons:funnel'
|
icon='heroicons:funnel'
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className={
|
className={activeCount > 0 ? 'text-blue-600' : ''}
|
||||||
getFilledFormikValuesCount(values) > 0 ? 'text-blue-600' : ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
Filter
|
Filter
|
||||||
{getFilledFormikValuesCount(values) > 0 && (
|
{activeCount > 0 && (
|
||||||
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
<span className='w-5 h-5 text-white bg-[#FF3535] rounded-lg border border-base-300 flex items-center justify-center text-xs'>
|
||||||
{getFilledFormikValuesCount(values)}
|
{activeCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
|
const PageNotFound = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex-1 flex flex-col justify-center items-center gap-4'>
|
||||||
|
<h2 className='text-2xl font-bold text-error'>Halaman Tidak Ditemukan</h2>
|
||||||
|
<p className='text-gray-600 text-center'>
|
||||||
|
Halaman atau data yang anda cari tidak ditemukan.
|
||||||
|
</p>
|
||||||
|
<Button href='/dashboard' className='text-base-100'>
|
||||||
|
Kembali ke Dashboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageNotFound;
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
const PermissionNotFound = () => {
|
const PermissionNotFound = () => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
|
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
|
||||||
<h2 className='text-2xl font-bold text-error'>Permission Not Found</h2>
|
<h2 className='text-2xl font-bold text-error'>
|
||||||
|
Hak Akses Tidak Ditemukan
|
||||||
|
</h2>
|
||||||
<p className='text-gray-600 text-center'>
|
<p className='text-gray-600 text-center'>
|
||||||
You do not have permission to access this page.
|
Anda tidak memiliki hak akses untuk mengakses halaman ini.
|
||||||
</p>
|
</p>
|
||||||
|
<Button href='/dashboard' className='text-base-100'>
|
||||||
|
Kembali ke Dashboard
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
color: Color;
|
color: Color;
|
||||||
text: string;
|
text: ReactNode;
|
||||||
className?: {
|
className?: {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
};
|
};
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusBadge = ({
|
const StatusBadge = ({
|
||||||
color = 'neutral',
|
color = 'neutral',
|
||||||
text,
|
text,
|
||||||
className,
|
className,
|
||||||
|
onClick,
|
||||||
}: StatusBadgeProps) => {
|
}: StatusBadgeProps) => {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
|
onClick={onClick}
|
||||||
className={{
|
className={{
|
||||||
badge: cn(
|
badge: cn(
|
||||||
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
|
'px-2 py-1 w-full flex flex-row justify-start gap-1 rounded-lg border border-base-content/10 text-xs font-medium text-base-content',
|
||||||
@@ -28,6 +34,7 @@ const StatusBadge = ({
|
|||||||
'bg-error/20': color === 'error',
|
'bg-error/20': color === 'error',
|
||||||
'bg-primary/20': color === 'info',
|
'bg-primary/20': color === 'info',
|
||||||
'bg-[#FF9A20]/12': color === 'warning',
|
'bg-[#FF9A20]/12': color === 'warning',
|
||||||
|
'bg-[#1166EF]/12': color === 'primary',
|
||||||
},
|
},
|
||||||
className?.badge
|
className?.badge
|
||||||
),
|
),
|
||||||
@@ -45,6 +52,7 @@ const StatusBadge = ({
|
|||||||
'text-error': color === 'error',
|
'text-error': color === 'error',
|
||||||
'text-primary': color === 'info',
|
'text-primary': color === 'info',
|
||||||
'text-[#FF9A20]': color === 'warning',
|
'text-[#FF9A20]': color === 'warning',
|
||||||
|
'text-[#1166EF]': color === 'primary',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<circle r='6' cx='6' cy='6' fill='currentColor' />
|
<circle r='6' cx='6' cy='6' fill='currentColor' />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface DrawerHeaderProps {
|
|||||||
|
|
||||||
const DrawerHeader = ({
|
const DrawerHeader = ({
|
||||||
leftIcon = 'mdi:close',
|
leftIcon = 'mdi:close',
|
||||||
leftIconSize = 24,
|
leftIconSize = 20,
|
||||||
leftIconHref,
|
leftIconHref,
|
||||||
leftIconOnClick,
|
leftIconOnClick,
|
||||||
leftIconClassName,
|
leftIconClassName,
|
||||||
@@ -43,7 +43,7 @@ const DrawerHeader = ({
|
|||||||
icon={leftIcon}
|
icon={leftIcon}
|
||||||
width={leftIconSize}
|
width={leftIconSize}
|
||||||
height={leftIconSize}
|
height={leftIconSize}
|
||||||
className={cn('cursor-pointer', leftIconClassName)}
|
className={cn('cursor-pointer text-base-content ', leftIconClassName)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ const DrawerHeader = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row justify-between items-center px-4 pt-4 pb-4 border-b border-base-content/10',
|
'flex flex-row justify-between items-center p-4 border-b border-base-content/10',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -82,7 +82,7 @@ const DrawerHeader = ({
|
|||||||
{renderLeftIcon()}
|
{renderLeftIcon()}
|
||||||
|
|
||||||
{showDivider && subtitle && (
|
{showDivider && subtitle && (
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
<div className='w-px h-full border-none bg-base-content/10' />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Alert from '@/components/Alert';
|
import Alert from '@/components/Alert';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alert Unique Error List
|
* Alert Unique Error List
|
||||||
@@ -10,34 +13,81 @@ import { useState } from 'react';
|
|||||||
*/
|
*/
|
||||||
const AlertErrorList = ({
|
const AlertErrorList = ({
|
||||||
formErrorList,
|
formErrorList,
|
||||||
|
className,
|
||||||
onClose,
|
onClose,
|
||||||
|
title,
|
||||||
}: {
|
}: {
|
||||||
formErrorList: string[];
|
formErrorList: string[];
|
||||||
|
className?: {
|
||||||
|
alert?: string;
|
||||||
|
button?: string;
|
||||||
|
headerWrapper?: string;
|
||||||
|
headerIcon?: string;
|
||||||
|
headerText?: string;
|
||||||
|
titleWrapper?: string;
|
||||||
|
ul?: string;
|
||||||
|
li?: string;
|
||||||
|
};
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const alertRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formErrorList.length > 0) {
|
||||||
|
alertRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [formErrorList.length]);
|
||||||
|
|
||||||
if (formErrorList.length === 0) return null;
|
if (formErrorList.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert color='error' className='w-full flex flex-col gap-2 px-4'>
|
<Alert
|
||||||
<div className='flex justify-between items-center gap-2 w-full'>
|
ref={alertRef}
|
||||||
<div className='flex items-center gap-2'>
|
color='error'
|
||||||
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
className={cn(
|
||||||
<span className='font-semibold'>
|
'w-full flex flex-col gap-2 px-3 rounded-lg',
|
||||||
Terdapat {formErrorList.length} error pada form:
|
className?.alert
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex justify-between items-center gap-2 w-full',
|
||||||
|
className?.headerWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn('flex items-center gap-2', className?.titleWrapper)}>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
className={cn(className?.headerIcon)}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<span className={cn('font-semibold text-sm', className?.headerText)}>
|
||||||
|
{title || `Terdapat ${formErrorList.length} error pada form:`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
variant='link'
|
variant='link'
|
||||||
className='ml-auto p-0 w-fit text-white'
|
className={cn('ml-auto p-0 w-fit text-white', className?.button)}
|
||||||
color='none'
|
color='none'
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon icon='material-symbols:close' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ul className='list-disc list-inside pl-8 space-y-1 w-full'>
|
<ul
|
||||||
|
className={cn(
|
||||||
|
'list-disc list-inside pl-4 space-y-1.5 w-full',
|
||||||
|
className?.ul
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formErrorList.map((error, index) => (
|
{formErrorList.map((error, index) => (
|
||||||
<li key={index} className='text-sm'>
|
<li key={index} className={cn('text-sm', className?.li)}>
|
||||||
{error}
|
{error}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
|
type PdfParamBadgeProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
style?: Style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
parameterBadge: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
color: '#333333',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
|
||||||
|
return (
|
||||||
|
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
|
||||||
|
<Text>{children}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
|
type PdfStatusBadgeProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
style?: Style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
statusBadge: {
|
||||||
|
paddingVertical: 2,
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
fontSize: 7,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderColor: '#E5E7EB',
|
||||||
|
},
|
||||||
|
statusBadgeText: {
|
||||||
|
fontSize: 7,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#333333',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => {
|
||||||
|
const styleRecord = style as Record<string, unknown>;
|
||||||
|
const color = styleRecord?.color as string | undefined;
|
||||||
|
|
||||||
|
const viewStyle = Object.entries(styleRecord || {}).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
if (key !== 'color') {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.statusBadge,
|
||||||
|
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.statusBadgeText, ...(color ? [{ color }] : [])]}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
|
type PdfPageNumberProps = {
|
||||||
|
style?: Style;
|
||||||
|
/**
|
||||||
|
* Format template for page number.
|
||||||
|
* Use {pageNumber} and {totalPages} as placeholders.
|
||||||
|
* Default: "{pageNumber} / {totalPages}"
|
||||||
|
*/
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
footer: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
position: 'absolute',
|
||||||
|
fontSize: 8,
|
||||||
|
bottom: 30,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'grey',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PdfPageNumber = ({
|
||||||
|
style,
|
||||||
|
format = '{pageNumber} / {totalPages}',
|
||||||
|
}: PdfPageNumberProps) => {
|
||||||
|
return (
|
||||||
|
<View style={style || styles.footer} fixed>
|
||||||
|
<Text
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
format
|
||||||
|
.replace('{pageNumber}', String(pageNumber))
|
||||||
|
.replace('{totalPages}', String(totalPages))
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { View, StyleSheet } from '@react-pdf/renderer';
|
import { View, StyleSheet } from '@react-pdf/renderer';
|
||||||
import { PdfThead, PdfColumn } from './PdfThead';
|
import type { PdfColumn } from './types';
|
||||||
import { PdfTbody, PdfTbodyCell } from './PdfTbody';
|
import { PdfThead } from './PdfThead';
|
||||||
import { PdfTfoot, PdfTfootCell } from './PdfTfoot';
|
import { PdfTbody } from './PdfTbody';
|
||||||
|
import { PdfTfoot } from './PdfTfoot';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
table: {
|
table: {
|
||||||
@@ -13,10 +14,10 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTableProps {
|
interface PdfTableProps<TData = Record<string, unknown>> {
|
||||||
columns: PdfColumn[];
|
columns: PdfColumn<TData>[];
|
||||||
data: PdfTbodyCell[][];
|
data: TData[];
|
||||||
footer?: PdfTfootCell[];
|
showFooter?: boolean;
|
||||||
footerLabel?: string;
|
footerLabel?: string;
|
||||||
firstRow?: {
|
firstRow?: {
|
||||||
valueKey: string;
|
valueKey: string;
|
||||||
@@ -26,20 +27,26 @@ interface PdfTableProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfTable = ({
|
export const PdfTable = <TData = Record<string, unknown>,>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
footer,
|
showFooter = false,
|
||||||
footerLabel = 'Total',
|
footerLabel = 'Total',
|
||||||
firstRow,
|
firstRow,
|
||||||
}: PdfTableProps) => {
|
}: PdfTableProps<TData>) => {
|
||||||
|
// Check if any column has footer defined
|
||||||
|
const hasFooter =
|
||||||
|
showFooter || columns.some((col) => col.footer !== undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.table}>
|
<View style={styles.table}>
|
||||||
<PdfThead columns={columns} />
|
<PdfThead columns={columns} data={data} />
|
||||||
<PdfTbody columns={columns} rows={data} firstRow={firstRow} />
|
<PdfTbody columns={columns} data={data} firstRow={firstRow} />
|
||||||
{footer && footer.length > 0 && (
|
{hasFooter && data.length > 0 && (
|
||||||
<PdfTfoot columns={columns} cells={footer} label={footerLabel} />
|
<PdfTfoot columns={columns} data={data} label={footerLabel} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { PdfColumn };
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
export interface PdfColumn {
|
import type { PdfColumn } from './types';
|
||||||
key: string;
|
|
||||||
header: string;
|
|
||||||
flex: number;
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PdfTbodyCell {
|
|
||||||
key: string;
|
|
||||||
value: string | number | React.ReactNode;
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
color?: string;
|
|
||||||
formatAs?: 'text' | 'date' | 'currency' | 'number';
|
|
||||||
formatDate?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tableRow: {
|
tableRow: {
|
||||||
@@ -71,21 +57,22 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTbodyProps {
|
interface PdfTbodyProps<TData = Record<string, unknown>> {
|
||||||
columns: PdfColumn[];
|
columns: PdfColumn<TData>[];
|
||||||
rows: PdfTbodyCell[][];
|
data: TData[];
|
||||||
firstRow?: {
|
firstRow?: {
|
||||||
valueKey: string;
|
valueKey: string;
|
||||||
value: number;
|
value: number;
|
||||||
align?: 'right';
|
align?: 'right';
|
||||||
color?: string;
|
color?: string;
|
||||||
};
|
};
|
||||||
formatDate?: (date: string, format: string) => string;
|
|
||||||
formatNumber?: (num: number) => string;
|
|
||||||
formatCurrency?: (num: number) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
export const PdfTbody = <TData = Record<string, unknown>,>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
firstRow,
|
||||||
|
}: PdfTbodyProps<TData>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* First Row */}
|
{/* First Row */}
|
||||||
@@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
const isLastColumn = index === columns.length - 1;
|
const isLastColumn = index === columns.length - 1;
|
||||||
const isfirstRowColumn = column.key === firstRow.valueKey;
|
const isFirstRowColumn = column.key === firstRow.valueKey;
|
||||||
const align = column.align || 'center';
|
const align = column.align || 'left';
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
column.key === 'no'
|
column.key === 'no'
|
||||||
? [styles.tableCellNo, { flex: column.flex }]
|
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
||||||
: isfirstRowColumn
|
: isFirstRowColumn
|
||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
color: firstRow.color || 'black',
|
color: firstRow.color || 'black',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
@@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
? [
|
? [
|
||||||
styles.tableCellCenter,
|
styles.tableCellCenter,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
? [
|
? [
|
||||||
styles.tableCellLast,
|
styles.tableCellLast,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
borderRightWidth: 0,
|
borderRightWidth: 0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [styles.tableCell, { flex: column.flex }];
|
: [styles.tableCell, { flex: column.flex || 1 }];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
<Text>{isfirstRowColumn ? firstRow.value : ''}</Text>
|
<Text>{isFirstRowColumn ? firstRow.value : ''}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Data Rows */}
|
{/* Data Rows */}
|
||||||
{rows.map((row, rowIndex) => {
|
{data.map((row, rowIndex) => {
|
||||||
const isLastRow = rowIndex === rows.length - 1;
|
const isLastRow = rowIndex === data.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -156,19 +143,27 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{columns.map((column, colIndex) => {
|
{columns.map((column, colIndex) => {
|
||||||
const cell = row.find((c) => c.key === column.key);
|
|
||||||
const isLastColumn = colIndex === columns.length - 1;
|
const isLastColumn = colIndex === columns.length - 1;
|
||||||
const align = cell?.align || column.align || 'center';
|
const align = column.align || 'left';
|
||||||
|
|
||||||
|
// Get cell content from column.cell function or fallback to row value
|
||||||
|
let cellContent: ReactNode;
|
||||||
|
if (column.cell) {
|
||||||
|
cellContent = column.cell({ row, index: rowIndex });
|
||||||
|
} else {
|
||||||
|
cellContent =
|
||||||
|
((row as Record<string, unknown>)[column.key] as ReactNode) ??
|
||||||
|
'-';
|
||||||
|
}
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
column.key === 'no'
|
column.key === 'no'
|
||||||
? [styles.tableCellNo, { flex: column.flex }]
|
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
||||||
: align === 'right'
|
: align === 'right'
|
||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
color: cell?.color || 'black',
|
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
? [
|
? [
|
||||||
styles.tableCellCenter,
|
styles.tableCellCenter,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
color: cell?.color || 'black',
|
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: isLastColumn
|
: isLastColumn
|
||||||
? [
|
? [
|
||||||
styles.tableCellLast,
|
styles.tableCellLast,
|
||||||
{ flex: column.flex, borderRightWidth: 0 },
|
{ flex: column.flex || 1, borderRightWidth: 0 },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
styles.tableCell,
|
styles.tableCell,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
color: cell?.color || 'black',
|
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
{cell?.value !== undefined &&
|
{typeof cellContent === 'string' ||
|
||||||
cell?.value !== null &&
|
typeof cellContent === 'number' ? (
|
||||||
cell?.value !== '' ? (
|
<Text>{String(cellContent)}</Text>
|
||||||
typeof cell.value === 'object' ? (
|
|
||||||
cell.value
|
|
||||||
) : (
|
|
||||||
<Text>{String(cell.value)}</Text>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Text>-</Text>
|
cellContent
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { PdfColumn };
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
export interface PdfColumn {
|
import type { PdfColumn } from './types';
|
||||||
key: string;
|
|
||||||
header: string;
|
|
||||||
flex: number;
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PdfTfootCell {
|
|
||||||
key: string;
|
|
||||||
value: string | number;
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
flex?: number;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tableRow: {
|
tableRow: {
|
||||||
@@ -69,63 +56,86 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTfootProps {
|
interface PdfTfootProps<TData = Record<string, unknown>> {
|
||||||
columns: PdfColumn[];
|
columns: PdfColumn<TData>[];
|
||||||
cells: PdfTfootCell[];
|
data: TData[];
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfTfoot = ({
|
export const PdfTfoot = <TData = Record<string, unknown>,>({
|
||||||
columns,
|
columns,
|
||||||
cells,
|
data,
|
||||||
label = 'Total',
|
label = 'Total',
|
||||||
}: PdfTfootProps) => {
|
}: PdfTfootProps<TData>) => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.tableRow, styles.summaryRow]}>
|
<View style={[styles.tableRow, styles.summaryRow]}>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
const isLastColumn = index === columns.length - 1;
|
const isLastColumn = index === columns.length - 1;
|
||||||
const cellData = cells.find((c) => c.key === column.key);
|
|
||||||
|
// Get footer content from column definition
|
||||||
|
let footerContent: ReactNode;
|
||||||
|
if (typeof column.footer === 'function') {
|
||||||
|
footerContent = column.footer(data);
|
||||||
|
} else {
|
||||||
|
footerContent = column.footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use label for first column (usually 'no' column)
|
||||||
|
const displayContent = column.key === 'no' ? label : footerContent;
|
||||||
|
|
||||||
|
// Determine alignment
|
||||||
|
const align = column.footerAlign || column.align || 'left';
|
||||||
|
const color = column.footerColor || 'black';
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
column.key === 'no'
|
column.key === 'no'
|
||||||
? [
|
? [
|
||||||
styles.tableCellNo,
|
styles.tableCellNo,
|
||||||
{ flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 },
|
{
|
||||||
|
flex: column.flex || 1,
|
||||||
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
|
color,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: cellData?.align === 'right'
|
: align === 'right'
|
||||||
? [
|
? [
|
||||||
styles.tableCellRight,
|
styles.tableCellRight,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
color: cellData?.color || 'black',
|
color,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: cellData?.align === 'center'
|
: align === 'center'
|
||||||
? [
|
? [
|
||||||
styles.tableCellCenter,
|
styles.tableCellCenter,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
color: cellData?.color || 'black',
|
color,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: isLastColumn
|
: isLastColumn
|
||||||
? [styles.tableCellLast, { flex: column.flex }]
|
? [styles.tableCellLast, { flex: column.flex || 1, color }]
|
||||||
: [
|
: [styles.tableCell, { flex: column.flex || 1, color }];
|
||||||
styles.tableCell,
|
|
||||||
{
|
|
||||||
flex: column.flex,
|
|
||||||
color: cellData?.color || 'black',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
<Text>{column.key === 'no' ? label : cellData?.value || ''}</Text>
|
{displayContent !== undefined && displayContent !== null ? (
|
||||||
|
typeof displayContent === 'string' ||
|
||||||
|
typeof displayContent === 'number' ? (
|
||||||
|
<Text>{String(displayContent)}</Text>
|
||||||
|
) : (
|
||||||
|
displayContent
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Text>-</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { PdfColumn };
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
export interface PdfColumn {
|
import type { PdfColumn } from './types';
|
||||||
key: string;
|
|
||||||
header: string;
|
|
||||||
flex: number;
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tableRow: {
|
tableRow: {
|
||||||
@@ -48,23 +43,37 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PdfTheadProps {
|
interface PdfTheadProps<TData = Record<string, unknown>> {
|
||||||
columns: PdfColumn[];
|
columns: PdfColumn<TData>[];
|
||||||
|
data?: TData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PdfThead = ({ columns }: PdfTheadProps) => {
|
export const PdfThead = <TData = Record<string, unknown>,>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: PdfTheadProps<TData>) => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||||
{columns.map((column, index) => {
|
{columns.map((column, index) => {
|
||||||
const align = column.align || 'center';
|
|
||||||
const isLastColumn = index === columns.length - 1;
|
const isLastColumn = index === columns.length - 1;
|
||||||
|
|
||||||
|
// Get header content from column definition
|
||||||
|
let headerContent: ReactNode;
|
||||||
|
if (typeof column.header === 'function') {
|
||||||
|
headerContent = column.header(data || []);
|
||||||
|
} else {
|
||||||
|
headerContent = column.header || column.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine alignment - columns align right by default for numeric data
|
||||||
|
const align = column.align || 'left';
|
||||||
|
|
||||||
const cellStyle =
|
const cellStyle =
|
||||||
align === 'right'
|
align === 'right'
|
||||||
? [
|
? [
|
||||||
styles.tableCellHeaderRight,
|
styles.tableCellHeaderRight,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
textAlign: 'right' as const,
|
textAlign: 'right' as const,
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
@@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
|
|||||||
: [
|
: [
|
||||||
styles.tableCellHeader,
|
styles.tableCellHeader,
|
||||||
{
|
{
|
||||||
flex: column.flex,
|
flex: column.flex || 1,
|
||||||
textAlign: align as 'left' | 'center' | 'right',
|
textAlign: align as 'left' | 'center' | 'right',
|
||||||
borderRightWidth: isLastColumn ? 0 : 1,
|
borderRightWidth: isLastColumn ? 0 : 1,
|
||||||
},
|
},
|
||||||
@@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={column.key} style={cellStyle}>
|
<View key={column.key} style={cellStyle}>
|
||||||
<Text>{column.header}</Text>
|
{typeof headerContent === 'string' ? (
|
||||||
|
<Text>{headerContent}</Text>
|
||||||
|
) : (
|
||||||
|
headerContent
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { PdfColumn };
|
||||||
|
|||||||
@@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable';
|
|||||||
export { PdfThead } from './PdfThead';
|
export { PdfThead } from './PdfThead';
|
||||||
export { PdfTbody } from './PdfTbody';
|
export { PdfTbody } from './PdfTbody';
|
||||||
export { PdfTfoot } from './PdfTfoot';
|
export { PdfTfoot } from './PdfTfoot';
|
||||||
export type { PdfColumn } from './PdfThead';
|
export type { PdfColumn } from './types';
|
||||||
export type { PdfTbodyCell } from './PdfTbody';
|
|
||||||
export type { PdfTfootCell } from './PdfTfoot';
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PdfColumn - Mirip dengan ColumnDef di TanStack Table
|
||||||
|
* Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi
|
||||||
|
*/
|
||||||
|
export interface PdfColumn<TData = Record<string, unknown>> {
|
||||||
|
key: string;
|
||||||
|
flex?: number;
|
||||||
|
|
||||||
|
// Header configuration (thead)
|
||||||
|
header?: string | ((data: TData[]) => ReactNode);
|
||||||
|
|
||||||
|
// Body configuration (tbody)
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
cell?: (props: { row: TData; index: number }) => ReactNode | string | number;
|
||||||
|
|
||||||
|
// Footer configuration (tfoot)
|
||||||
|
footer?: string | number | ((data: TData[]) => ReactNode | string | number);
|
||||||
|
footerAlign?: 'left' | 'center' | 'right';
|
||||||
|
footerColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { PdfColumn as default };
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Color } from '@/types/theme';
|
||||||
|
import { Text, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
|
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
|
||||||
|
|
||||||
|
type TypographyVariant = Color | 'default';
|
||||||
|
|
||||||
|
type PdfTypographyProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
size?: TypographySize;
|
||||||
|
variant?: TypographyVariant;
|
||||||
|
color?: string;
|
||||||
|
style?: Style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
h1: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 3,
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
fontSize: 10,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
fontSize: 8,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 9,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const variantColors: Record<TypographyVariant, string> = {
|
||||||
|
default: '#333333',
|
||||||
|
primary: '#1f74bf',
|
||||||
|
secondary: '#6B7280',
|
||||||
|
accent: '#8B5CF6',
|
||||||
|
neutral: '#6B7280',
|
||||||
|
info: '#3B82F6',
|
||||||
|
success: '#065F46',
|
||||||
|
warning: '#92400E',
|
||||||
|
error: '#DC2626',
|
||||||
|
none: '#333333',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PdfTypography = ({
|
||||||
|
children,
|
||||||
|
size = 'p',
|
||||||
|
variant = 'default',
|
||||||
|
color,
|
||||||
|
style,
|
||||||
|
}: PdfTypographyProps) => {
|
||||||
|
const sizeStyle = styles[size];
|
||||||
|
const textColor = color || variantColors[variant];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text style={[sizeStyle, { color: textColor }, ...(style ? [style] : [])]}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
export type StatusColor = {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Due status colors (for debt supplier reports)
|
||||||
|
export const dueStatusColors: Record<string, StatusColor> = {
|
||||||
|
'SUDAH JATUH TEMPO': {
|
||||||
|
bg: '#FEE2E2',
|
||||||
|
text: '#991B1B',
|
||||||
|
border: '#F87171',
|
||||||
|
}, // error/red
|
||||||
|
'BELUM JATUH TEMPO': {
|
||||||
|
bg: '#D1FAE5',
|
||||||
|
text: '#065F46',
|
||||||
|
border: '#34D399',
|
||||||
|
}, // success/green
|
||||||
|
'MENDEKATI JATUH TEMPO': {
|
||||||
|
bg: '#FEF3C7',
|
||||||
|
text: '#92400E',
|
||||||
|
border: '#FBBF24',
|
||||||
|
}, // warning/yellow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Payment status colors (for customer payment & debt supplier reports)
|
||||||
|
export const paymentStatusColors: Record<string, StatusColor> = {
|
||||||
|
'BELUM LUNAS': {
|
||||||
|
bg: '#FEF3C7',
|
||||||
|
text: '#92400E',
|
||||||
|
border: '#FBBF24',
|
||||||
|
}, // warning/yellow
|
||||||
|
LUNAS: {
|
||||||
|
bg: '#DBEAFE',
|
||||||
|
text: '#1E40AF',
|
||||||
|
border: '#60A5FA',
|
||||||
|
}, // primary/blue
|
||||||
|
'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
||||||
|
PEMBAYARAN: {
|
||||||
|
bg: '#D1FAE5',
|
||||||
|
text: '#065F46',
|
||||||
|
border: '#34D399',
|
||||||
|
}, // success/green
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback color for unknown statuses
|
||||||
|
export const fallbackStatusColor: StatusColor = {
|
||||||
|
bg: '#F3F4F6',
|
||||||
|
text: '#374151',
|
||||||
|
border: '#D1D5DB',
|
||||||
|
}; // neutral
|
||||||
|
|
||||||
|
export const getPDFBadgeStyle = (
|
||||||
|
statusText: string,
|
||||||
|
type: 'due' | 'payment' = 'payment'
|
||||||
|
): StatusColor => {
|
||||||
|
const normalizedStatus = statusText.toUpperCase().trim();
|
||||||
|
|
||||||
|
const colors =
|
||||||
|
type === 'due'
|
||||||
|
? dueStatusColors[normalizedStatus]
|
||||||
|
: paymentStatusColors[normalizedStatus];
|
||||||
|
|
||||||
|
return colors || fallbackStatusColor;
|
||||||
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import IconSkeleton from '@/components/helper/skeleton/IconSkeleton';
|
import IconSkeleton from '@/components/helper/skeleton/IconSkeleton';
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
|
|
||||||
const DataStateSkeleton = ({
|
const DataStateSkeleton = ({
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import TextArea, { TextAreaProps } from '@/components/input/TextArea';
|
|||||||
|
|
||||||
interface DebouncedTextAreaProps extends TextAreaProps {
|
interface DebouncedTextAreaProps extends TextAreaProps {
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
ref?: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
||||||
@@ -19,6 +20,11 @@ const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
|||||||
const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300);
|
const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300);
|
||||||
const [debouncedValue] = useDebounce(internalValue, delay ?? 300);
|
const [debouncedValue] = useDebounce(internalValue, delay ?? 300);
|
||||||
|
|
||||||
|
// Sync internal value with external props.value when it changes (e.g., form reset)
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(props.value);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
const internalChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (
|
const internalChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (
|
||||||
e
|
e
|
||||||
) => {
|
) => {
|
||||||
@@ -35,6 +41,7 @@ const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
|||||||
return (
|
return (
|
||||||
<TextArea
|
<TextArea
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={props.ref}
|
||||||
value={internalValue}
|
value={internalValue}
|
||||||
onChange={internalChangeHandler}
|
onChange={internalChangeHandler}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -134,14 +134,20 @@ const DropFileInput: React.FC<DropFileInputProps> = ({
|
|||||||
|
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p
|
<p
|
||||||
className={cn('w-full text-sm opacity-60', className?.bottomLabel)}
|
className={cn(
|
||||||
|
'w-full mt-1.5 text-xs opacity-60',
|
||||||
|
className?.bottomLabel
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{bottomLabel}
|
{bottomLabel}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{isError && (
|
{isError && (
|
||||||
<p
|
<p
|
||||||
className={cn('w-full text-sm text-error', className?.errorMessage)}
|
className={cn(
|
||||||
|
'w-full mt-1.5 text-xs text-error',
|
||||||
|
className?.errorMessage
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ChangeEvent } from 'react';
|
|||||||
import {
|
import {
|
||||||
PatternFormat,
|
PatternFormat,
|
||||||
NumberFormatBase,
|
NumberFormatBase,
|
||||||
NumberFormatBaseProps,
|
|
||||||
OnValueChange,
|
OnValueChange,
|
||||||
} from 'react-number-format';
|
} from 'react-number-format';
|
||||||
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
import TextInput, { TextInputProps } from '@/components/input/TextInput';
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ export const RadioGroup = ({
|
|||||||
|
|
||||||
{/* Label bawah */}
|
{/* Label bawah */}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='text-sm opacity-60'>{bottomLabel}</p>
|
<p className='mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pesan error */}
|
{/* Pesan error */}
|
||||||
{isError && errorMessage && (
|
{isError && errorMessage && (
|
||||||
<p className='text-sm text-error'>{errorMessage}</p>
|
<p className='mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</RadioGroupContext.Provider>
|
</RadioGroupContext.Provider>
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-base-content/10': !isDisabled,
|
'bg-base-100 border-base-content/10': !isDisabled,
|
||||||
'bg-gray-50 border-base-content/10': isDisabled,
|
'bg-base-200 border-base-content/10': isDisabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
},
|
},
|
||||||
className?.inputPrefix
|
className?.inputPrefix
|
||||||
@@ -278,28 +278,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn('w-full flex-1', className?.select)}
|
className={cn('w-full flex-1', className?.select)}
|
||||||
classNames={{
|
classNames={{
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn('w-full border bg-white transition-shadow', 'rounded-lg!', {
|
cn('w-full border transition-shadow', 'rounded-lg!', {
|
||||||
'cursor-pointer!': !readOnly && !isDisabled,
|
'bg-base-100!': !isDisabled && !readOnly,
|
||||||
'border-red-500! ring-2 ring-red-200': isError,
|
'bg-base-200! text-gray-400 cursor-not-allowed':
|
||||||
'border-indigo-500 ring-2 ring-indigo-200':
|
|
||||||
isFocused && !startAdornment,
|
|
||||||
'border-base-content/10!': !isError && !isFocused,
|
|
||||||
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
|
||||||
isDisabled && !readOnly,
|
isDisabled && !readOnly,
|
||||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||||
|
'cursor-pointer!': !readOnly && !isDisabled,
|
||||||
|
'border-error!': isError,
|
||||||
|
'ring-2 ring-error/20': isError,
|
||||||
|
'border-indigo-500 ring-2 ring-indigo-200':
|
||||||
|
isFocused && !startAdornment && !isError,
|
||||||
|
'border-base-content/10!': !isError && !isFocused,
|
||||||
'rounded-l-none!': inputPrefix && !startAdornment,
|
'rounded-l-none!': inputPrefix && !startAdornment,
|
||||||
'rounded-r-none!': inputSuffix && !startAdornment,
|
'rounded-r-none!': inputSuffix && !startAdornment,
|
||||||
}),
|
}),
|
||||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||||
placeholder: () =>
|
placeholder: () =>
|
||||||
cn({
|
cn('text-gray-400 text-sm leading-tight', {
|
||||||
'text-gray-400 text-sm leading-tight': !isError,
|
'text-error!': isError,
|
||||||
'text-red-300!': isError,
|
|
||||||
}),
|
}),
|
||||||
singleValue: () =>
|
singleValue: () =>
|
||||||
cn({
|
cn('m-0! text-gray-900 text-sm leading-tight', {
|
||||||
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
'text-error!': isError && !readOnly,
|
||||||
'text-error!': isError,
|
|
||||||
'text-gray-900!': readOnly,
|
'text-gray-900!': readOnly,
|
||||||
}),
|
}),
|
||||||
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||||
@@ -370,8 +370,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-base-content/10': !isDisabled,
|
'bg-base-100 border-base-content/10': !isDisabled,
|
||||||
'bg-gray-50 border-base-content/10': isDisabled,
|
'bg-base-200 border-base-content/10': isDisabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
},
|
},
|
||||||
className?.inputSuffix
|
className?.inputSuffix
|
||||||
@@ -403,31 +403,26 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
className={cn('w-full', className?.select)}
|
className={cn('w-full', className?.select)}
|
||||||
classNames={{
|
classNames={{
|
||||||
control: ({ isFocused, isDisabled }) =>
|
control: ({ isFocused, isDisabled }) =>
|
||||||
cn(
|
cn('w-full border transition-shadow rounded-lg!', {
|
||||||
'w-full border bg-white transition-shadow',
|
'bg-base-100!': !isDisabled && !readOnly,
|
||||||
// Gunakan rounded-lg untuk semua kasus
|
'bg-base-200! text-gray-400 cursor-not-allowed':
|
||||||
'rounded-lg!',
|
isDisabled && !readOnly,
|
||||||
{
|
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||||
'cursor-pointer!': !readOnly && !isDisabled,
|
'cursor-pointer!': !readOnly && !isDisabled,
|
||||||
'border-red-500! ring-2 ring-red-200': isError,
|
'border-error!': isError,
|
||||||
'border-indigo-500 ring-2 ring-indigo-200':
|
'ring-2 ring-error/20': isError,
|
||||||
isFocused && !startAdornment,
|
'border-indigo-500 ring-2 ring-indigo-200':
|
||||||
'border-base-content/10!': !isError && !isFocused,
|
isFocused && !startAdornment && !isError,
|
||||||
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
'border-base-content/10!': !isError && !isFocused,
|
||||||
isDisabled && !readOnly,
|
}),
|
||||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||||
placeholder: () =>
|
placeholder: () =>
|
||||||
cn({
|
cn('text-gray-400 text-sm leading-tight', {
|
||||||
'text-gray-400 text-sm leading-tight': !isError,
|
'text-error!': isError,
|
||||||
'text-red-300!': isError,
|
|
||||||
}),
|
}),
|
||||||
singleValue: () =>
|
singleValue: () =>
|
||||||
cn({
|
cn('m-0! text-gray-900 text-sm leading-tight', {
|
||||||
'm-0! text-gray-900 text-sm leading-tight': !isError,
|
'text-error!': isError && !readOnly,
|
||||||
'text-error!': isError,
|
|
||||||
'text-gray-900!': readOnly,
|
'text-gray-900!': readOnly,
|
||||||
}),
|
}),
|
||||||
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'),
|
||||||
@@ -493,9 +488,11 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
{isError && (
|
||||||
|
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
|
)}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -159,9 +159,11 @@ const TagInput: React.FC<TagInputProps> = ({
|
|||||||
|
|
||||||
{/* Bottom label or error message */}
|
{/* Bottom label or error message */}
|
||||||
{!isError && bottomLabel && (
|
{!isError && bottomLabel && (
|
||||||
<p className='w-full text-sm opacity-60'>{bottomLabel}</p>
|
<p className='w-full mt-1.5 text-xs opacity-60'>{bottomLabel}</p>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<p className='w-full mt-1.5 text-xs text-error'>{errorMessage}</p>
|
||||||
)}
|
)}
|
||||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface TextAreaProps {
|
|||||||
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
ref?: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextArea = ({
|
const TextArea = ({
|
||||||
@@ -49,6 +50,7 @@ const TextArea = ({
|
|||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
rows = 3,
|
rows = 3,
|
||||||
|
ref,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -99,6 +101,7 @@ const TextArea = ({
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(isLoading || endAdornment) && (
|
{(isLoading || endAdornment) && (
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ const TextInput = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-base-content/10': !disabled,
|
'bg-base-100 border-base-content/10': !disabled,
|
||||||
'bg-gray-50 border-base-content/10': disabled,
|
'bg-base-200 border-base-content/10': disabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
},
|
},
|
||||||
@@ -118,7 +118,7 @@ const TextInput = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
|
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
@@ -126,7 +126,8 @@ const TextInput = ({
|
|||||||
'rounded-r-none!': inputSuffix,
|
'rounded-r-none!': inputSuffix,
|
||||||
'input-disabled': disabled,
|
'input-disabled': disabled,
|
||||||
'cursor-not-allowed': disabled,
|
'cursor-not-allowed': disabled,
|
||||||
'bg-gray-50': disabled,
|
'bg-base-100': !disabled,
|
||||||
|
'bg-base-200': disabled,
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
)}
|
||||||
@@ -167,8 +168,8 @@ const TextInput = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||||
{
|
{
|
||||||
'bg-gray-100 border-base-content/10': !disabled,
|
'bg-base-100 border-base-content/10': !disabled,
|
||||||
'bg-gray-50 border-base-content/10': disabled,
|
'bg-base-200 border-base-content/10': disabled,
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
},
|
},
|
||||||
@@ -182,10 +183,12 @@ const TextInput = ({
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
|
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
||||||
{
|
{
|
||||||
'border-error': isError,
|
'border-error': isError,
|
||||||
'border-success!': isValid,
|
'border-success!': isValid,
|
||||||
|
'bg-base-100': !disabled,
|
||||||
|
'bg-base-200': disabled,
|
||||||
},
|
},
|
||||||
className?.inputWrapper
|
className?.inputWrapper
|
||||||
)}
|
)}
|
||||||
@@ -201,7 +204,14 @@ const TextInput = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn('grow', className?.input)}
|
className={cn(
|
||||||
|
'grow bg-transparent outline-none',
|
||||||
|
{
|
||||||
|
'cursor-not-allowed': disabled,
|
||||||
|
'text-gray-500': disabled,
|
||||||
|
},
|
||||||
|
className?.input
|
||||||
|
)}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -122,10 +122,18 @@ const ConfirmationModal = ({
|
|||||||
closeOnBackdrop={closeOnBackdrop}
|
closeOnBackdrop={closeOnBackdrop}
|
||||||
className={{
|
className={{
|
||||||
...className,
|
...className,
|
||||||
modalBox: cn('rounded-xl p-4', className?.modalBox),
|
modalBox: cn(
|
||||||
|
'rounded-xl p-4 flex flex-col gap-4 max-h-[90vh]',
|
||||||
|
className?.modalBox
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='w-full flex flex-col gap-4'>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-4',
|
||||||
|
children && 'sticky top-0 bg-inherit z-10'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{iconPosition === 'center' ? (
|
{iconPosition === 'center' ? (
|
||||||
<>
|
<>
|
||||||
<div className='w-fit mx-auto'>
|
<div className='w-fit mx-auto'>
|
||||||
@@ -164,71 +172,74 @@ const ConfirmationModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{children && <div className='w-full'>{children}</div>}
|
{children && (
|
||||||
|
<div className='w-full flex-1 overflow-y-auto'>{children}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{(secondaryButton || primaryButton) && (
|
{(secondaryButton || primaryButton) && (
|
||||||
<div
|
<div
|
||||||
className={cn('w-full grid gap-3', {
|
className={cn(
|
||||||
|
'w-full grid gap-3',
|
||||||
|
children && 'sticky bottom-0 bg-inherit z-10',
|
||||||
|
{
|
||||||
'grid-cols-2': secondaryButton && primaryButton,
|
'grid-cols-2': secondaryButton && primaryButton,
|
||||||
'grid-cols-1':
|
'grid-cols-1':
|
||||||
(secondaryButton && !primaryButton) ||
|
(secondaryButton && !primaryButton) ||
|
||||||
(!secondaryButton && primaryButton),
|
(!secondaryButton && primaryButton),
|
||||||
})}
|
}
|
||||||
>
|
)}
|
||||||
{secondaryButton && secondaryButton.text && (
|
>
|
||||||
<Button
|
{secondaryButton && secondaryButton.text && (
|
||||||
{...secondaryButton}
|
<Button
|
||||||
variant='outline'
|
{...secondaryButton}
|
||||||
color={secondaryButton?.color}
|
variant='outline'
|
||||||
isLoading={secondaryButton?.isLoading}
|
color={secondaryButton?.color}
|
||||||
disabled={
|
isLoading={secondaryButton?.isLoading}
|
||||||
secondaryButton?.isLoading !== undefined
|
disabled={
|
||||||
? secondaryButton?.isLoading
|
secondaryButton?.isLoading !== undefined
|
||||||
: isPrimaryButtonLoading
|
? secondaryButton?.isLoading
|
||||||
|
: isPrimaryButtonLoading
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (secondaryButton?.onClick) {
|
||||||
|
secondaryButton.onClick(e);
|
||||||
|
} else {
|
||||||
|
closeModalHandler();
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
}}
|
||||||
if (secondaryButton?.onClick) {
|
className={cn(
|
||||||
secondaryButton.onClick(e);
|
'p-2 rounded-xl text-sm',
|
||||||
} else {
|
secondaryButton?.className
|
||||||
closeModalHandler();
|
)}
|
||||||
}
|
>
|
||||||
}}
|
{secondaryButton?.text ?? 'Tidak'}
|
||||||
className={cn(
|
</Button>
|
||||||
'p-2 rounded-xl text-sm',
|
)}
|
||||||
secondaryButton?.className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{secondaryButton?.text ?? 'Tidak'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{primaryButton && primaryButton.text && (
|
{primaryButton && primaryButton.text && (
|
||||||
<Button
|
<Button
|
||||||
{...primaryButton}
|
{...primaryButton}
|
||||||
color={primaryButton?.color ?? 'info'}
|
color={primaryButton?.color ?? 'info'}
|
||||||
onClick={primaryButtonClickHandler}
|
onClick={primaryButtonClickHandler}
|
||||||
isLoading={
|
isLoading={
|
||||||
primaryButton?.isLoading !== undefined
|
primaryButton?.isLoading !== undefined
|
||||||
? primaryButton?.isLoading
|
? primaryButton?.isLoading
|
||||||
: isPrimaryButtonLoading
|
: isPrimaryButtonLoading
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
primaryButton?.isLoading !== undefined
|
primaryButton?.isLoading !== undefined
|
||||||
? primaryButton?.isLoading
|
? primaryButton?.isLoading
|
||||||
: isPrimaryButtonLoading
|
: isPrimaryButtonLoading
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn('p-2 rounded-xl text-sm', primaryButton?.className)}
|
||||||
'p-2 rounded-xl text-sm',
|
>
|
||||||
primaryButton?.className
|
{primaryButton?.text ?? 'Ya'}
|
||||||
)}
|
</Button>
|
||||||
>
|
)}
|
||||||
{primaryButton?.text ?? 'Ya'}
|
</div>
|
||||||
</Button>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface ConfirmationModalWithNotesProps
|
|||||||
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
|
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
|
||||||
rows?: number;
|
rows?: number;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
|
||||||
primaryButton?: {
|
primaryButton?: {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -32,6 +33,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
className,
|
className,
|
||||||
rows = 3,
|
rows = 3,
|
||||||
placeholder = 'Catatan...',
|
placeholder = 'Catatan...',
|
||||||
|
onClose,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const randomId = useId();
|
const randomId = useId();
|
||||||
@@ -41,6 +43,11 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
setNotes(e.target.value);
|
setNotes(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
onClose?.();
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -50,11 +57,31 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
|
|||||||
primaryButton={{
|
primaryButton={{
|
||||||
...primaryButton,
|
...primaryButton,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
primaryButton?.onClick?.(notes);
|
if (primaryButton && primaryButton?.onClick) {
|
||||||
|
primaryButton?.onClick?.(notes);
|
||||||
|
} else {
|
||||||
|
closeModalHandler();
|
||||||
|
}
|
||||||
|
|
||||||
setNotes('');
|
setNotes('');
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
secondaryButton={secondaryButton}
|
secondaryButton={
|
||||||
|
secondaryButton
|
||||||
|
? {
|
||||||
|
text: secondaryButton?.text ?? 'Tidak',
|
||||||
|
onClick: (e) => {
|
||||||
|
if (secondaryButton && secondaryButton?.onClick) {
|
||||||
|
secondaryButton.onClick?.(e);
|
||||||
|
} else {
|
||||||
|
closeModalHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotes('');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={className}
|
className={className}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -309,9 +309,10 @@ const useApprovalSteps = ({
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
params?: {
|
params?: {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit: number | string;
|
limit?: number | string;
|
||||||
search?: string;
|
search?: string;
|
||||||
group_step_number?: boolean;
|
group_step_number?: boolean;
|
||||||
|
order_by_date?: 'ASC' | 'DESC';
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
// Membuat URL Parameters
|
// Membuat URL Parameters
|
||||||
@@ -319,6 +320,8 @@ const useApprovalSteps = ({
|
|||||||
page: params?.page?.toString() || '',
|
page: params?.page?.toString() || '',
|
||||||
limit: params?.limit?.toString() || '',
|
limit: params?.limit?.toString() || '',
|
||||||
search: params?.search || '',
|
search: params?.search || '',
|
||||||
|
group_step_number: params?.group_step_number?.toString() || '',
|
||||||
|
order_by_date: params?.order_by_date || '',
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
// fetching data approvals
|
// fetching data approvals
|
||||||
|
|||||||
+35
-31
@@ -5,28 +5,23 @@ import { useMemo, useState } from 'react';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable';
|
||||||
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab';
|
||||||
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab';
|
||||||
|
|
||||||
import {
|
import { ClosingGeneralInformation } from '@/types/api/closing';
|
||||||
ClosingGeneralInformation,
|
import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab';
|
||||||
BaseClosingSales,
|
import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab';
|
||||||
ClosingHppExpedition,
|
import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab';
|
||||||
} from '@/types/api/closing';
|
import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab';
|
||||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab';
|
||||||
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
|
||||||
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
|
||||||
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
|
||||||
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
|
|
||||||
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
import ClosingKandangList from '@/components/pages/closing/ClosingKandangList';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
interface ClosingDetailProps {
|
interface ClosingDetailProps {
|
||||||
id: number;
|
id: number;
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
salesData?: BaseClosingSales;
|
|
||||||
hppExpeditionData?: ClosingHppExpedition;
|
|
||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
kandangData?: ProjectFlockKandang;
|
kandangData?: ProjectFlockKandang;
|
||||||
}
|
}
|
||||||
@@ -34,25 +29,24 @@ interface ClosingDetailProps {
|
|||||||
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
salesData,
|
|
||||||
hppExpeditionData,
|
|
||||||
projectData,
|
projectData,
|
||||||
kandangData,
|
kandangData,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
const [activeTabId, setActiveTabId] = useState<string>('sapronak');
|
||||||
|
const tabActions = useTabActionsStore((state) => state.tabActions);
|
||||||
|
|
||||||
const closingDetailTabs = useMemo(() => {
|
const closingDetailTabs = useMemo(() => {
|
||||||
const validTabs = [
|
const validTabs = [
|
||||||
{
|
{
|
||||||
id: 'sapronak',
|
id: 'sapronak',
|
||||||
label: 'Sapronak',
|
label: 'Sapronak',
|
||||||
content: <ClosingSapronakTabContent projectFlockId={id} />,
|
content: <SapronakClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'perhitunganSapronak',
|
id: 'perhitunganSapronak',
|
||||||
label: 'Perhitungan Sapronak',
|
label: 'Perhitungan Sapronak',
|
||||||
content: (
|
content: (
|
||||||
<ClosingSapronakCalculationTabContent
|
<SapronakCalculationClosingTab
|
||||||
closingGeneralInformation={initialValue}
|
closingGeneralInformation={initialValue}
|
||||||
projectFlockId={id}
|
projectFlockId={id}
|
||||||
/>
|
/>
|
||||||
@@ -61,36 +55,42 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
{
|
{
|
||||||
id: 'penjualan',
|
id: 'penjualan',
|
||||||
label: 'Penjualan',
|
label: 'Penjualan',
|
||||||
content: <SalesReportTable initialValues={salesData} />,
|
content: <SalesClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'overhead',
|
id: 'overhead',
|
||||||
label: 'Overhead',
|
label: 'Overhead',
|
||||||
content: <ClosingOverheadTabContent projectFlockId={id} />,
|
content: (
|
||||||
|
<OverheadClosingTab
|
||||||
|
projectFlockId={id}
|
||||||
|
generalInformation={initialValue}
|
||||||
|
kandangData={kandangData}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hppEkspedisi',
|
id: 'hppEkspedisi',
|
||||||
label: 'HPP Ekspedisi',
|
label: 'HPP Ekspedisi',
|
||||||
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
content: <HppExpeditionClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dataProduksi',
|
id: 'dataProduksi',
|
||||||
label: 'Data Produksi',
|
label: 'Data Produksi',
|
||||||
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
content: <ProductionDataClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'keuangan',
|
id: 'keuangan',
|
||||||
label: 'Keuangan',
|
label: 'Keuangan',
|
||||||
content: <ClosingFinanceTabContent projectFlockId={id} />,
|
content: <FinanceClosingTab projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return validTabs;
|
return validTabs;
|
||||||
}, [initialValue]);
|
}, [initialValue, kandangData, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href={
|
href={
|
||||||
@@ -120,13 +120,17 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTabId={activeTab}
|
activeTabId={activeTabId}
|
||||||
onTabChange={setActiveTab}
|
onTabChange={setActiveTabId}
|
||||||
tabs={closingDetailTabs}
|
tabs={closingDetailTabs}
|
||||||
variant='lifted'
|
variant='boxed'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full mt-4',
|
tabHeaderWrapper:
|
||||||
|
'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10',
|
||||||
|
tab: 'w-fit',
|
||||||
|
content: 'p-0 m-0',
|
||||||
}}
|
}}
|
||||||
|
sideContent={tabActions[activeTabId] || null}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable';
|
|
||||||
|
|
||||||
const ClosingFinanceTabContent = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: {
|
|
||||||
projectFlockId: number;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
{projectFlockId && (
|
|
||||||
<ClosingFinanceTable projectFlockId={projectFlockId} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingFinanceTabContent;
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
import Card from '@/components/Card';
|
|
||||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
|
||||||
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const ClosingFinanceTable = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: {
|
|
||||||
projectFlockId: number;
|
|
||||||
}) => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const kandangId = searchParams.get('kandangId');
|
|
||||||
|
|
||||||
const { data: finance, isLoading } = useSWR(
|
|
||||||
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
|
||||||
() =>
|
|
||||||
ClosingApi.getFinance(
|
|
||||||
projectFlockId,
|
|
||||||
kandangId ? Number(kandangId) : undefined
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const hppTableData: HppItem[] = useMemo(() => {
|
|
||||||
if (isResponseSuccess(finance)) {
|
|
||||||
const customItems = {
|
|
||||||
label: 'HPP dan Pengeluaran',
|
|
||||||
code: 'custom_row',
|
|
||||||
} as HppItem;
|
|
||||||
const purchases = finance.data.hpp.items.filter(
|
|
||||||
(item) => item.category === 'purchase'
|
|
||||||
);
|
|
||||||
const totalBudgeting = {
|
|
||||||
label: 'HPP dan Bahan Baku',
|
|
||||||
code: 'custom_row',
|
|
||||||
} as HppItem;
|
|
||||||
const overheads = finance.data.hpp.items.filter(
|
|
||||||
(item) => item.category === 'overhead'
|
|
||||||
);
|
|
||||||
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}, [finance]);
|
|
||||||
|
|
||||||
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
|
||||||
if (isResponseSuccess(finance)) {
|
|
||||||
const incomes = finance.data.profit_loss.items.filter(
|
|
||||||
(item) => item.type === 'income'
|
|
||||||
);
|
|
||||||
const purchases = finance.data.profit_loss.items.filter(
|
|
||||||
(item) => item.type === 'purchase'
|
|
||||||
);
|
|
||||||
const overheads = finance.data.profit_loss.items.filter(
|
|
||||||
(item) => item.type === 'overhead'
|
|
||||||
);
|
|
||||||
const grossProfit = {
|
|
||||||
label: 'LABA RUGI BRUTO',
|
|
||||||
code: 'custom_row',
|
|
||||||
type: 'gross_profit',
|
|
||||||
rp_per_bird:
|
|
||||||
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
|
||||||
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
|
||||||
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
|
||||||
} as ProfitLossItem;
|
|
||||||
const subtotal = {
|
|
||||||
label: 'Subtotal',
|
|
||||||
code: 'custom_row',
|
|
||||||
type: 'subtotal',
|
|
||||||
rp_per_bird:
|
|
||||||
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
|
||||||
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
|
||||||
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
|
||||||
} as ProfitLossItem;
|
|
||||||
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}, [finance]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
<>
|
|
||||||
<Card
|
|
||||||
variant='bordered'
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='grid grid-cols-2 gap-6'>
|
|
||||||
<div className='flex flex-col gap-1'>
|
|
||||||
<div>Laba Rugi Brutto</div>
|
|
||||||
<div className='text-lg font-bold'>
|
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.profit_loss.summary.gross_profit.amount
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-1'>
|
|
||||||
<div>Laba Rugi Netto</div>
|
|
||||||
<div className='text-lg font-bold'>
|
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.profit_loss.summary.net_profit.amount
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
title='HPP Purchases'
|
|
||||||
variant='bordered'
|
|
||||||
collapsible
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='mt-6 p-0 mb-0'>
|
|
||||||
<Table<HppItem>
|
|
||||||
data={hppTableData}
|
|
||||||
isLoading={isLoading}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'No.',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item, index) => {
|
|
||||||
if (item.code === 'custom_row') return '-';
|
|
||||||
const dataRowsBefore = hppTableData
|
|
||||||
.slice(0, index)
|
|
||||||
.filter((row) => row.code !== 'custom_row').length;
|
|
||||||
return dataRowsBefore + 1;
|
|
||||||
},
|
|
||||||
footer: (props) => {
|
|
||||||
return 'HPP';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jenis',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Budgeting',
|
|
||||||
enableSorting: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'Rp/Ekor',
|
|
||||||
id: 'budgeting_rp_per_bird',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) =>
|
|
||||||
formatCurrency(item.budgeting?.rp_per_bird || 0),
|
|
||||||
footer: (props) => {
|
|
||||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
|
||||||
isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.hpp.summary?.budgeting
|
|
||||||
?.rp_per_bird || 0
|
|
||||||
)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Rp/Kg',
|
|
||||||
id: 'budgeting_rp_per_kg',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) =>
|
|
||||||
formatCurrency(item.budgeting?.rp_per_kg || 0),
|
|
||||||
footer: (props) => {
|
|
||||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
|
||||||
isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
|
|
||||||
0
|
|
||||||
)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jumlah (Rp)',
|
|
||||||
id: 'budgeting_amount',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) =>
|
|
||||||
formatCurrency(item.budgeting?.amount || 0),
|
|
||||||
footer: (props) => {
|
|
||||||
return props.column.id === 'budgeting_amount' &&
|
|
||||||
isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.hpp.summary?.budgeting?.amount || 0
|
|
||||||
)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Realization',
|
|
||||||
enableSorting: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
header: 'Rp/Ekor',
|
|
||||||
id: 'realization_rp_per_bird',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) =>
|
|
||||||
formatCurrency(item.realization?.rp_per_bird || 0),
|
|
||||||
footer: (props) => {
|
|
||||||
return props.column.id === 'realization_rp_per_bird' &&
|
|
||||||
isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.hpp.summary?.realization
|
|
||||||
?.rp_per_bird || 0
|
|
||||||
)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Rp/Kg',
|
|
||||||
id: 'realization_rp_per_kg',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) =>
|
|
||||||
formatCurrency(item.realization?.rp_per_kg || 0),
|
|
||||||
footer: (props) => {
|
|
||||||
return props.column.id === 'realization_rp_per_kg' &&
|
|
||||||
isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.hpp.summary?.realization
|
|
||||||
?.rp_per_kg || 0
|
|
||||||
)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jumlah (Rp)',
|
|
||||||
id: 'realization_amount',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) =>
|
|
||||||
formatCurrency(item.realization?.amount || 0),
|
|
||||||
footer: (props) => {
|
|
||||||
return props.column.id === 'realization_amount' &&
|
|
||||||
isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.hpp.summary?.realization?.amount || 0
|
|
||||||
)
|
|
||||||
: '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
renderCustomRow={(row) => {
|
|
||||||
const rowData = row.original;
|
|
||||||
if (rowData.code === 'custom_row') {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
></td>
|
|
||||||
<td
|
|
||||||
colSpan={7}
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatTitleCase(rowData.label ?? '-')}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
renderFooter={isResponseSuccess(finance)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
title='Profit/Loss'
|
|
||||||
variant='bordered'
|
|
||||||
collapsible
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='mt-6 p-0 mb-0'>
|
|
||||||
<Table<ProfitLossItem>
|
|
||||||
data={profitLossTableData}
|
|
||||||
isLoading={isLoading}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'Jenis',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) => item.label,
|
|
||||||
cell: (item) => (
|
|
||||||
<div className=''>
|
|
||||||
{formatTitleCase(item.row.original.label || '-')}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
footer: () => (
|
|
||||||
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Rp/Ekor',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
|
||||||
footer: () => (
|
|
||||||
<div className='font-bold'>
|
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.profit_loss.summary.net_profit
|
|
||||||
.rp_per_bird || 0
|
|
||||||
)
|
|
||||||
: formatCurrency(0)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Rp/Kg',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
|
||||||
footer: () => (
|
|
||||||
<div className='font-bold'>
|
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.profit_loss.summary.net_profit
|
|
||||||
.rp_per_kg || 0
|
|
||||||
)
|
|
||||||
: formatCurrency(0)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Jumlah (Rp)',
|
|
||||||
enableSorting: false,
|
|
||||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
|
||||||
footer: () => (
|
|
||||||
<div className='font-bold'>
|
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatCurrency(
|
|
||||||
finance.data.profit_loss.summary.net_profit
|
|
||||||
.amount || 0
|
|
||||||
)
|
|
||||||
: formatCurrency(0)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
renderCustomRow={(row) => {
|
|
||||||
const rowData = row.original;
|
|
||||||
if (rowData.code === 'custom_row') {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
|
||||||
>
|
|
||||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
|
||||||
<div className='font-bold ps-6 uppercase'>
|
|
||||||
{formatTitleCase(rowData.label ?? '-')}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.amount ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
className={{
|
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
|
||||||
renderFooter={isResponseSuccess(finance)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingFinanceTable;
|
|
||||||
@@ -10,18 +10,18 @@ const ClosingKandangList = ({
|
|||||||
projectData?: ProjectFlock;
|
projectData?: ProjectFlock;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full my-4 @container'>
|
<div className='w-full py-3 @container relative before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10'>
|
||||||
<div className='flex flex-col @sm:flex-row gap-4'>
|
<div className='flex flex-col @sm:flex-row gap-4'>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
<h1 className='font-bold my-4'>Kandang</h1>
|
<h1 className='font-bold mb-3'>Kandang</h1>
|
||||||
<div className='flex flex-wrap gap-2 mb-4'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
{projectData?.kandangs?.map((kandang) => (
|
{projectData?.kandangs?.map((kandang) => (
|
||||||
<Button
|
<Button
|
||||||
key={kandang.id}
|
key={kandang.id}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
|
className='px-3 py-2.5 w-fit text-sm rounded-lg shadow-sm'
|
||||||
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
href={`/closing/detail/?closingId=${initialValue?.flock_id}&kandangId=${kandang.project_flock_kandang_id}`}
|
||||||
className='min-w-32'
|
|
||||||
>
|
>
|
||||||
{kandang.name}
|
{kandang.name}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|
||||||
import Card from '@/components/Card';
|
|
||||||
import Collapse from '@/components/Collapse';
|
|
||||||
|
|
||||||
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
|
||||||
import { ClosingOutgoingSapronak } from '@/types/api/closing';
|
|
||||||
|
|
||||||
interface ClosingOutgoingSapronaksTableProps {
|
|
||||||
projectFlockId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClosingOutgoingSapronaksTable = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: ClosingOutgoingSapronaksTableProps) => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const kandangId = searchParams.get('kandangId');
|
|
||||||
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
} = useTableFilter({
|
|
||||||
initial: {
|
|
||||||
search: '',
|
|
||||||
nameSort: '',
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
nameSort: 'sort_name',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
|
||||||
useSWR(
|
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
|
||||||
ClosingApi.getAllOutgoingSapronakFetcher,
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronak>[] = [
|
|
||||||
{
|
|
||||||
header: '#',
|
|
||||||
cell: (props) => props.row.index + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'date',
|
|
||||||
header: 'Tanggal',
|
|
||||||
cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'reference_number',
|
|
||||||
header: 'No. Referensi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transaction_type',
|
|
||||||
header: 'Jenis Transaksi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'product_name',
|
|
||||||
header: 'Produk',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'product_category',
|
|
||||||
header: 'Kategori Produk',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'source_warehouse',
|
|
||||||
header: 'Gudang Asal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'destination_warehouse',
|
|
||||||
header: 'Gudang Tujuan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'quantity',
|
|
||||||
header: 'Kuantitas',
|
|
||||||
cell: (props) =>
|
|
||||||
`${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'notes',
|
|
||||||
header: 'Keterangan',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
updateFilter('search', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// track sorting
|
|
||||||
useEffect(() => {
|
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
|
||||||
|
|
||||||
if (!isNameSorted) {
|
|
||||||
updateFilter('nameSort', '');
|
|
||||||
} else {
|
|
||||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
|
||||||
}
|
|
||||||
}, [sorting, updateFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
setOpen(
|
|
||||||
isResponseSuccess(outgoingSapronaks)
|
|
||||||
? outgoingSapronaks.data.length > 0
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [outgoingSapronaks, isResponseSuccess]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
body: 'p-4 shadow',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Collapse
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
title={
|
|
||||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
|
||||||
<div className='card-title'>Sapronak Keluar</div>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:keyboard-arrow-down'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
className={cn('text-primary transition-transform', {
|
|
||||||
'-rotate-180': open,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
className='w-full!'
|
|
||||||
titleClassName='w-full p-0!'
|
|
||||||
>
|
|
||||||
<div className='w-full p-0'>
|
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Sapronak Keluar'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table<ClosingOutgoingSapronak>
|
|
||||||
data={
|
|
||||||
isResponseSuccess(outgoingSapronaks)
|
|
||||||
? outgoingSapronaks?.data
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={outgoingSapronaksColumns}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
rowOptions={[10, 20, 50, 100]}
|
|
||||||
page={
|
|
||||||
isResponseSuccess(outgoingSapronaks)
|
|
||||||
? outgoingSapronaks?.meta?.page
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(outgoingSapronaks)
|
|
||||||
? outgoingSapronaks?.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
isLoading={isLoadingOutgoingSapronaks}
|
|
||||||
sorting={sorting}
|
|
||||||
setSorting={setSorting}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
setRowSelection={setRowSelection}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'w-full mb-20':
|
|
||||||
isResponseSuccess(outgoingSapronaks) &&
|
|
||||||
outgoingSapronaks?.data?.length === 0,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingOutgoingSapronaksTable;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable';
|
|
||||||
|
|
||||||
interface ClosingOverheadTabContentProps {
|
|
||||||
projectFlockId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClosingOverheadTabContent = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: ClosingOverheadTabContentProps) => {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
{projectFlockId && (
|
|
||||||
<ClosingOverheadTable projectFlockId={projectFlockId} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingOverheadTabContent;
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import Card from '@/components/Card';
|
|
||||||
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
|
||||||
import { Overhead, OverheadTotal } from '@/types/api/closing';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
interface ClosingOverheadTableProps {
|
|
||||||
projectFlockId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClosingOverheadTable = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: ClosingOverheadTableProps) => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const kandangId = searchParams.get('kandangId');
|
|
||||||
|
|
||||||
const { data: overhead, isLoading: isLoadingOverhead } = useSWR(
|
|
||||||
`${ClosingApi.basePath}/${projectFlockId}${kandangId ? `/${kandangId}` : ''}/overhead`,
|
|
||||||
() =>
|
|
||||||
ClosingApi.getOverhead(
|
|
||||||
projectFlockId,
|
|
||||||
kandangId ? Number(kandangId) : undefined
|
|
||||||
),
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper function to create columns with footer support
|
|
||||||
const createColumns = (
|
|
||||||
total?: OverheadTotal,
|
|
||||||
kandangId?: number
|
|
||||||
): ColumnDef<Overhead>[] => {
|
|
||||||
const flockColumn: ColumnDef<Overhead>[] = [
|
|
||||||
{
|
|
||||||
header: 'Budget Pengajuan',
|
|
||||||
footer: '',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
id: 'budget_quantity',
|
|
||||||
header: 'Jumlah',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
|
||||||
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'budget_unit_price',
|
|
||||||
header: 'Harga Satuan',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.budget_unit_price
|
|
||||||
? formatCurrency(props.budget_unit_price)
|
|
||||||
: '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'budget_total_amount',
|
|
||||||
header: 'Total',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.budget_total_amount
|
|
||||||
? formatCurrency(props.budget_total_amount)
|
|
||||||
: '-',
|
|
||||||
footer: total
|
|
||||||
? () => formatCurrency(total.budget_total_amount)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Realisasi',
|
|
||||||
footer: '',
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
id: 'actual_date',
|
|
||||||
header: 'Tanggal',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_date
|
|
||||||
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
|
||||||
: '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actual_quantity',
|
|
||||||
header: 'Jumlah',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
|
||||||
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actual_unit_price',
|
|
||||||
header: 'Harga Satuan',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_unit_price
|
|
||||||
? formatCurrency(props.actual_unit_price)
|
|
||||||
: '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actual_total_amount',
|
|
||||||
header: 'Total',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_total_amount
|
|
||||||
? formatCurrency(props.actual_total_amount)
|
|
||||||
: '-',
|
|
||||||
footer: total
|
|
||||||
? () => formatCurrency(total.actual_total_amount)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const kandangColumn: ColumnDef<Overhead>[] = [
|
|
||||||
{
|
|
||||||
id: 'actual_date',
|
|
||||||
header: 'Tanggal',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_date
|
|
||||||
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
|
||||||
: '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actual_quantity',
|
|
||||||
header: 'Jumlah',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
|
||||||
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actual_unit_price',
|
|
||||||
header: 'Harga Satuan',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_unit_price
|
|
||||||
? formatCurrency(props.actual_unit_price)
|
|
||||||
: '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actual_total_amount',
|
|
||||||
header: 'Total',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.actual_total_amount
|
|
||||||
? formatCurrency(props.actual_total_amount)
|
|
||||||
: '-',
|
|
||||||
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const finalColumns: ColumnDef<Overhead>[] = [
|
|
||||||
// Group untuk kolom tanpa footer
|
|
||||||
{
|
|
||||||
header: 'No',
|
|
||||||
accessorFn: (_, index) => index,
|
|
||||||
cell: (props) => props.row.index + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Item',
|
|
||||||
accessorFn: (props) => props.item_name,
|
|
||||||
footer: 'Total Pengeluaran Overhead',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Satuan',
|
|
||||||
accessorFn: (props) => props.uom_name,
|
|
||||||
},
|
|
||||||
...(kandangId ? kandangColumn : flockColumn),
|
|
||||||
{
|
|
||||||
id: 'cost_per_bird',
|
|
||||||
header: 'Rp/Ekor',
|
|
||||||
accessorFn: (props) =>
|
|
||||||
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
|
||||||
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return finalColumns;
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() =>
|
|
||||||
isResponseSuccess(overhead)
|
|
||||||
? createColumns(
|
|
||||||
overhead.data?.total,
|
|
||||||
kandangId ? Number(kandangId) : undefined
|
|
||||||
)
|
|
||||||
: createColumns(),
|
|
||||||
[overhead]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card
|
|
||||||
title='Pengeluaran Overhead'
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
body: 'p-4 shadow',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<Overhead>
|
|
||||||
data={
|
|
||||||
isResponseSuccess(overhead) ? (overhead.data?.overheads ?? []) : []
|
|
||||||
}
|
|
||||||
columns={columns}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'my-4',
|
|
||||||
headerColumnClassName: cn(
|
|
||||||
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
|
||||||
'whitespace-nowrap'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
isLoading={isLoadingOverhead}
|
|
||||||
renderFooter={
|
|
||||||
isResponseSuccess(overhead)
|
|
||||||
? overhead.data?.overheads.length > 0
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingOverheadTable;
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { formatNumber } from '@/lib/helper';
|
|
||||||
|
|
||||||
interface ClosingProductionDataTabContentProps {
|
|
||||||
projectFlockId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClosingProductionDataTabContent = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: ClosingProductionDataTabContentProps) => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const kandangId = searchParams.get('kandangId');
|
|
||||||
|
|
||||||
const { data: productionData, isLoading } = useSWR(
|
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
|
||||||
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className='w-full flex justify-center py-8'>
|
|
||||||
<span className='loading loading-spinner loading-lg' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!productionData || !isResponseSuccess(productionData)) {
|
|
||||||
return (
|
|
||||||
<div className='w-full text-center py-8 text-gray-500'>
|
|
||||||
Gagal memuat data produksi.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { purchase, sales, performance } = productionData.data;
|
|
||||||
|
|
||||||
// Helper for consistent row styling
|
|
||||||
const DataRow = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
unit = '',
|
|
||||||
valueClassName = 'font-bold text-gray-800',
|
|
||||||
unitClassName = 'text-gray-500 w-12 text-right',
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
unit?: string;
|
|
||||||
valueClassName?: string;
|
|
||||||
unitClassName?: string;
|
|
||||||
}) => (
|
|
||||||
<div className='flex justify-between items-center py-1'>
|
|
||||||
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
|
|
||||||
<div className='flex gap-2 w-1/2 justify-end items-center'>
|
|
||||||
<span className={valueClassName}>{value}</span>
|
|
||||||
{unit && <span className={unitClassName}>{unit}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full rounded-xl p-8 shadow-sm'>
|
|
||||||
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
|
|
||||||
{/* Left Column */}
|
|
||||||
<div className='space-y-10'>
|
|
||||||
{/* Purchase Section */}
|
|
||||||
<section>
|
|
||||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
|
||||||
Pembelian
|
|
||||||
</h3>
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<DataRow
|
|
||||||
label='Populasi Awal'
|
|
||||||
value={formatNumber(purchase.initial_population)}
|
|
||||||
unit='Ekor'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Claim Culling'
|
|
||||||
value={formatNumber(purchase.claim_culling)}
|
|
||||||
unit='Ekor'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Populasi Akhir'
|
|
||||||
value={formatNumber(purchase.final_population)}
|
|
||||||
unit='Ekor'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Pakan Masuk'
|
|
||||||
value={formatNumber(purchase.feed_in)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Pakan Terpakai'
|
|
||||||
value={formatNumber(purchase.feed_used)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Sales Section */}
|
|
||||||
<section>
|
|
||||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
|
||||||
Penjualan
|
|
||||||
</h3>
|
|
||||||
<div className='space-y-4'>
|
|
||||||
{/* Chicken Sales */}
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<DataRow
|
|
||||||
label='Penjualan (Ekor)'
|
|
||||||
value={formatNumber(sales.chicken.sales_population)}
|
|
||||||
unit='Ekor'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Penjualan (Kg)'
|
|
||||||
value={formatNumber(sales.chicken.sales_weight)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Bobot Rata-Rata'
|
|
||||||
value={formatNumber(sales.chicken.avg_weight)}
|
|
||||||
unit='Kg/Ekor'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Harga Jual Rata-Rata'
|
|
||||||
value={formatNumber(sales.chicken.avg_selling_price)}
|
|
||||||
unit='Rupiah'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Egg Sales (if available) */}
|
|
||||||
{sales.egg && (
|
|
||||||
<>
|
|
||||||
<div className='h-px bg-gray-100 my-2' />
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<DataRow
|
|
||||||
label='Telur (Butir)'
|
|
||||||
value={formatNumber(sales.egg.egg_pieces)}
|
|
||||||
unit='Butir'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Telur (Kg)'
|
|
||||||
value={formatNumber(sales.egg.egg_mass)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Berat Telur Rata-Rata'
|
|
||||||
value={formatNumber(sales.egg.avg_egg_weight)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Harga Jual Telur Rata-Rata'
|
|
||||||
value={formatNumber(sales.egg.avg_selling_price)}
|
|
||||||
unit='Rupiah'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider Line (Absolute centered) */}
|
|
||||||
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
|
|
||||||
|
|
||||||
{/* Right Column */}
|
|
||||||
<div className='space-y-10 flex flex-col h-full'>
|
|
||||||
{/* Performance Section */}
|
|
||||||
<section>
|
|
||||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
|
||||||
Performance
|
|
||||||
</h3>
|
|
||||||
<div className='space-y-1'>
|
|
||||||
<DataRow
|
|
||||||
label='Deplesi'
|
|
||||||
value={formatNumber(performance.depletion)}
|
|
||||||
unit='Ekor'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Umur'
|
|
||||||
value={formatNumber(performance.age_day)}
|
|
||||||
unit='Hari'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Mortalitas Std'
|
|
||||||
value={formatNumber(performance.mor_std)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Mortalitas Act'
|
|
||||||
value={formatNumber(performance.mor_act)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='DEFF Mortalitas'
|
|
||||||
value={formatNumber(performance.mor_diff)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
{/* <DataRow
|
|
||||||
label='AWG Std'
|
|
||||||
value={formatNumber(performance.awg_std)}
|
|
||||||
unit='Gr/Hari'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='AWG Act'
|
|
||||||
value={formatNumber(performance.awg_act)}
|
|
||||||
unit='Gr/Hari'
|
|
||||||
/> */}
|
|
||||||
<DataRow
|
|
||||||
label='Feed Intake Std'
|
|
||||||
value={formatNumber(performance.feed_intake_std)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Feed Intake Act'
|
|
||||||
value={formatNumber(performance.feed_intake)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='FCR Std'
|
|
||||||
value={formatNumber(performance.fcr_std)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='FCR Act'
|
|
||||||
value={formatNumber(performance.fcr_act)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='DEFF FCR'
|
|
||||||
value={formatNumber(performance.fcr_diff)}
|
|
||||||
unitClassName='hidden'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Laying Specific Fields */}
|
|
||||||
{performance.hen_day_act !== undefined && (
|
|
||||||
<>
|
|
||||||
<DataRow
|
|
||||||
label='Hen Day Std'
|
|
||||||
value={formatNumber(performance.hen_day_std!)}
|
|
||||||
unit='%'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Hen Day Act'
|
|
||||||
value={formatNumber(performance.hen_day_act)}
|
|
||||||
unit='%'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{performance.egg_mass !== undefined && (
|
|
||||||
<>
|
|
||||||
<DataRow
|
|
||||||
label='Egg Mass Std'
|
|
||||||
value={formatNumber(performance.egg_mass_std!)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Egg Mass Act'
|
|
||||||
value={formatNumber(performance.egg_mass)}
|
|
||||||
unit='Kg'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{performance.egg_weight !== undefined && (
|
|
||||||
<>
|
|
||||||
<DataRow
|
|
||||||
label='Egg Weight Std'
|
|
||||||
value={formatNumber(performance.egg_weight_std!)}
|
|
||||||
unit='Gr'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Egg Weight Act'
|
|
||||||
value={formatNumber(performance.egg_weight)}
|
|
||||||
unit='Gr'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{performance.hen_housed_act !== undefined && (
|
|
||||||
<>
|
|
||||||
<DataRow
|
|
||||||
label='Hen Housed Std'
|
|
||||||
value={formatNumber(performance.hen_housed_std!)}
|
|
||||||
unit='%'
|
|
||||||
/>
|
|
||||||
<DataRow
|
|
||||||
label='Hen Housed Act'
|
|
||||||
value={formatNumber(performance.hen_housed_act)}
|
|
||||||
unit='%'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingProductionDataTabContent;
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
|
||||||
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
||||||
import {
|
|
||||||
RowSapronakCalculation,
|
|
||||||
TotalSapronakCalculation,
|
|
||||||
} from '@/types/api/closing';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { ClosingGeneralInformation } from '@/types/api/closing';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
interface ClosingSapronakCalculationTableProps {
|
|
||||||
projectFlockId: number;
|
|
||||||
closingGeneralInformation?: ClosingGeneralInformation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClosingSapronakCalculationTable = ({
|
|
||||||
projectFlockId,
|
|
||||||
closingGeneralInformation,
|
|
||||||
}: ClosingSapronakCalculationTableProps) => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const kandangId = searchParams.get('kandangId');
|
|
||||||
|
|
||||||
const { data: sapronakCalculation, isLoading } = useSWR(
|
|
||||||
`/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
|
||||||
() => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)),
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper function to create columns with footer support
|
|
||||||
const createColumns = (
|
|
||||||
total?: TotalSapronakCalculation
|
|
||||||
): ColumnDef<RowSapronakCalculation>[] => [
|
|
||||||
{
|
|
||||||
header: 'Tanggal',
|
|
||||||
accessorKey: 'date',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.date
|
|
||||||
? formatDate(props.row.original.date, 'DD MMM YYYY')
|
|
||||||
: '-',
|
|
||||||
footer: 'Total',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'No. Referensi',
|
|
||||||
accessorKey: 'reference_number',
|
|
||||||
cell: (props) => (props.row.original.reference_number as string) || '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'QTY Masuk',
|
|
||||||
accessorKey: 'qty_in',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.qty_in
|
|
||||||
? formatNumber(props.row.original.qty_in as number)
|
|
||||||
: '0',
|
|
||||||
footer: total
|
|
||||||
? () => (
|
|
||||||
<div className='font-semibold text-gray-900'>
|
|
||||||
{total?.qty_in ? formatNumber(total?.qty_in) : '0'}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'QTY Keluar',
|
|
||||||
accessorKey: 'qty_out',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.qty_out
|
|
||||||
? formatNumber(props.row.original.qty_out as number)
|
|
||||||
: '0',
|
|
||||||
footer: total
|
|
||||||
? () => (
|
|
||||||
<div className='font-semibold text-gray-900'>
|
|
||||||
{total?.qty_out ? formatNumber(total?.qty_out) : '0'}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'QTY Pakai',
|
|
||||||
accessorKey: 'qty_used',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.qty_used
|
|
||||||
? formatNumber(props.row.original.qty_used as number)
|
|
||||||
: '0',
|
|
||||||
footer: total
|
|
||||||
? () => (
|
|
||||||
<div className='font-semibold text-gray-900'>
|
|
||||||
{total?.qty_used ? formatNumber(total?.qty_used) : '0'}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Uraian',
|
|
||||||
accessorKey: 'description',
|
|
||||||
cell: (props) => (props.row.original.description as string) || '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kategori Produk',
|
|
||||||
accessorKey: 'product_category',
|
|
||||||
cell: (props) => (props.row.original.product_category as string) || '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Harga Beli/Qty (Rp)',
|
|
||||||
accessorKey: 'unit_price',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.unit_price
|
|
||||||
? formatCurrency(props.row.original.unit_price as number)
|
|
||||||
: '-',
|
|
||||||
footer: total
|
|
||||||
? () => (
|
|
||||||
<div className='font-semibold text-gray-900'>
|
|
||||||
{total?.avg_unit_price
|
|
||||||
? formatCurrency(total?.avg_unit_price)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Total Harga (Rp)',
|
|
||||||
accessorKey: 'total_amount',
|
|
||||||
cell: (props) =>
|
|
||||||
props.row.original.total_amount
|
|
||||||
? formatCurrency(props.row.original.total_amount as number)
|
|
||||||
: '-',
|
|
||||||
footer: total
|
|
||||||
? () => (
|
|
||||||
<div className='font-semibold text-gray-900'>
|
|
||||||
{total?.total_amount ? formatCurrency(total?.total_amount) : '-'}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Keterangan',
|
|
||||||
accessorKey: 'notes',
|
|
||||||
cell: (props) => (props.row.original.notes as string) || '-',
|
|
||||||
footer: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Memoize columns untuk setiap kategori
|
|
||||||
const docColumns = useMemo(
|
|
||||||
() =>
|
|
||||||
isResponseSuccess(sapronakCalculation)
|
|
||||||
? createColumns(sapronakCalculation.data?.doc?.total)
|
|
||||||
: createColumns(),
|
|
||||||
[sapronakCalculation]
|
|
||||||
);
|
|
||||||
|
|
||||||
const ovkColumns = useMemo(
|
|
||||||
() =>
|
|
||||||
isResponseSuccess(sapronakCalculation)
|
|
||||||
? createColumns(sapronakCalculation.data?.ovk?.total)
|
|
||||||
: createColumns(),
|
|
||||||
[sapronakCalculation]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pakanColumns = useMemo(
|
|
||||||
() =>
|
|
||||||
isResponseSuccess(sapronakCalculation)
|
|
||||||
? createColumns(sapronakCalculation.data?.pakan?.total)
|
|
||||||
: createColumns(),
|
|
||||||
[sapronakCalculation]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
{/* Table DOC jika kategori Project Flock Growing */}
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
closingGeneralInformation?.project_type == 'GROWING'
|
|
||||||
? 'DOC'
|
|
||||||
: 'Pullet'
|
|
||||||
}
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={false}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
body: 'p-4 shadow',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<RowSapronakCalculation>
|
|
||||||
data={
|
|
||||||
isResponseSuccess(sapronakCalculation)
|
|
||||||
? (sapronakCalculation.data?.doc?.rows ?? [])
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={docColumns}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'my-4',
|
|
||||||
}}
|
|
||||||
renderFooter={
|
|
||||||
isResponseSuccess(sapronakCalculation) &&
|
|
||||||
sapronakCalculation.data?.doc?.rows.length > 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
title='OVK'
|
|
||||||
variant='bordered'
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={true}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<RowSapronakCalculation>
|
|
||||||
data={
|
|
||||||
isResponseSuccess(sapronakCalculation)
|
|
||||||
? (sapronakCalculation.data?.ovk?.rows ?? [])
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={ovkColumns}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'my-4',
|
|
||||||
}}
|
|
||||||
renderFooter={
|
|
||||||
isResponseSuccess(sapronakCalculation) &&
|
|
||||||
sapronakCalculation.data?.ovk?.rows.length > 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
title='Pakan'
|
|
||||||
variant='bordered'
|
|
||||||
collapsible
|
|
||||||
defaultCollapsed={true}
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<RowSapronakCalculation>
|
|
||||||
data={
|
|
||||||
isResponseSuccess(sapronakCalculation)
|
|
||||||
? (sapronakCalculation.data?.pakan?.rows ?? [])
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={pakanColumns}
|
|
||||||
className={{
|
|
||||||
containerClassName: 'my-4',
|
|
||||||
}}
|
|
||||||
renderFooter={
|
|
||||||
isResponseSuccess(sapronakCalculation) &&
|
|
||||||
sapronakCalculation.data?.pakan?.rows.length > 0
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingSapronakCalculationTable;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
|
||||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
|
||||||
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
|
|
||||||
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
|
|
||||||
|
|
||||||
interface ClosingSapronakTableProps {
|
|
||||||
projectFlockId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClosingSapronakTabContent = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: ClosingSapronakTableProps) => {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
{projectFlockId && (
|
|
||||||
<>
|
|
||||||
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
|
||||||
|
|
||||||
<ClosingIncomingSapronaksSummaryTable
|
|
||||||
projectFlockId={projectFlockId}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
|
||||||
|
|
||||||
<ClosingOutgoingSapronaksSummaryTable
|
|
||||||
projectFlockId={projectFlockId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClosingSapronakTabContent;
|
|
||||||
@@ -1,68 +1,122 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState, useMemo } from 'react';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import SelectInput, {
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
OptionType,
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
useSelect,
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { Closing } from '@/types/api/closing';
|
import { Closing } from '@/types/api/closing';
|
||||||
|
import { Color } from '@/types/theme';
|
||||||
const PROJECT_STATUS_OPTIONS = [
|
import {
|
||||||
{
|
ClosingFilterSchema,
|
||||||
value: 1,
|
ClosingFilterType,
|
||||||
label: 'Pengajuan',
|
} from '@/components/pages/closing/filter/ClosingFilter';
|
||||||
},
|
import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton';
|
||||||
{
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
value: 2,
|
|
||||||
label: 'Aktif',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
|
||||||
props,
|
props,
|
||||||
|
popoverPosition = 'bottom',
|
||||||
|
detailClickHandler,
|
||||||
}: {
|
}: {
|
||||||
type: 'dropdown' | 'collapse';
|
|
||||||
props: CellContext<Closing, unknown>;
|
props: CellContext<Closing, unknown>;
|
||||||
|
popoverPosition: 'bottom' | 'top';
|
||||||
|
detailClickHandler: (id: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const popoverId = `closing#${props.row.original.id}`;
|
||||||
|
const popoverAnchorName = `--anchor-closing#${props.row.original.id}`;
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
const detailClickHandlerWrapper = () => {
|
||||||
|
detailClickHandler(props.row.original.id);
|
||||||
|
closePopover();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<div className='relative'>
|
||||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
<PopoverButton
|
||||||
<RequirePermission permissions='lti.closing.detail'>
|
tabIndex={0}
|
||||||
<Button
|
variant='ghost'
|
||||||
href={`/closing/detail/?closingId=${props.row.original.id}`}
|
color='none'
|
||||||
variant='ghost'
|
popoverTarget={popoverId}
|
||||||
color='primary'
|
anchorName={popoverAnchorName}
|
||||||
className='justify-start text-sm'
|
>
|
||||||
>
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
</PopoverButton>
|
||||||
Detail
|
|
||||||
</Button>
|
<PopoverContent
|
||||||
</RequirePermission>
|
id={popoverId}
|
||||||
</div>
|
anchorName={popoverAnchorName}
|
||||||
</RowOptionsMenuWrapper>
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
|
<RequirePermission permissions='lti.closing.detail'>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={detailClickHandlerWrapper}
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:eye' width={20} height={20} />
|
||||||
|
View Details
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClosingsTable = () => {
|
const ClosingsTable = () => {
|
||||||
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// ===== ROUTER =====
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ===== STATUS BADGE COLOR HELPER =====
|
||||||
|
const getProjectStatusBadgeColor = (status: string): Color => {
|
||||||
|
const normalizedValue = status.toLowerCase();
|
||||||
|
|
||||||
|
if (normalizedValue === 'aktif') {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedValue === 'pengajuan') {
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'neutral';
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== FILTER MODAL STATE =====
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -72,36 +126,68 @@ const ClosingsTable = () => {
|
|||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
search: '',
|
search: '',
|
||||||
nameSort: '',
|
// nameSort: '',
|
||||||
transactionDate: '',
|
// transactionDate: '',
|
||||||
realizationDate: '',
|
// realizationDate: '',
|
||||||
locationId: '',
|
location_id: '',
|
||||||
projectStatus: '',
|
project_status: '',
|
||||||
userId: '',
|
// userId: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
nameSort: 'sort_name',
|
// nameSort: 'sort_name',
|
||||||
transactionDate: 'transaction_date',
|
// transactionDate: 'transaction_date',
|
||||||
realizationDate: 'realization_date',
|
// realizationDate: 'realization_date',
|
||||||
locationId: 'location_id',
|
// locationId: 'location_id',
|
||||||
projectStatus: 'project_status',
|
// projectStatus: 'project_status',
|
||||||
userId: 'user_id',
|
// userId: 'user_id',
|
||||||
|
search: 'search',
|
||||||
|
location_id: 'location_id',
|
||||||
|
project_status: 'project_status',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== FORMIK SETUP =====
|
||||||
|
const formik = useFormik<ClosingFilterType>({
|
||||||
|
initialValues: {
|
||||||
|
location_id: null,
|
||||||
|
project_status: null,
|
||||||
|
},
|
||||||
|
validationSchema: ClosingFilterSchema,
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
updateFilter('location_id', values.location_id || '');
|
||||||
|
updateFilter('project_status', values.project_status || '');
|
||||||
|
filterModal.closeModal();
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
updateFilter('location_id', '');
|
||||||
|
updateFilter('project_status', '');
|
||||||
|
filterModal.closeModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== DATA FETCHING =====
|
||||||
const { data: closings, isLoading: isLoadingClosings } = useSWR(
|
const { data: closings, isLoading: isLoadingClosings } = useSWR(
|
||||||
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
|
`${ClosingApi.basePath}${getTableFilterQueryString()}`,
|
||||||
ClosingApi.getAllFetcher
|
ClosingApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() =>
|
||||||
|
isResponseSuccess(closings) ? (closings?.data as Closing[]) || [] : [],
|
||||||
|
[closings]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== PAGINATION & STATE =====
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// ===== TABLE COLUMNS =====
|
||||||
const closingsColumns: ColumnDef<Closing>[] = [
|
const closingsColumns: ColumnDef<Closing>[] = [
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,6 +219,19 @@ const ClosingsTable = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'project_status',
|
accessorKey: 'project_status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
|
cell: (props) => {
|
||||||
|
const status = props.row.original.project_status;
|
||||||
|
const badgeColor = getProjectStatusBadgeColor(status);
|
||||||
|
return (
|
||||||
|
<StatusBadge
|
||||||
|
color={badgeColor}
|
||||||
|
text={status}
|
||||||
|
className={{
|
||||||
|
badge: 'whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
@@ -142,27 +241,24 @@ const ClosingsTable = () => {
|
|||||||
const currentRowRelativeIndex =
|
const currentRowRelativeIndex =
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
const detailClickHandler = (id: number) => {
|
||||||
|
router.push(`/closing/detail/?closingId=${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RowOptionsMenu
|
||||||
{currentPageSize > 3 && (
|
props={props}
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
detailClickHandler={detailClickHandler}
|
||||||
<RowOptionsMenu type='dropdown' props={props} />
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
</RowDropdownOptions>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 3 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu type='collapse' props={props} />
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ===== LOCATION OPTIONS =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
@@ -170,115 +266,222 @@ const ClosingsTable = () => {
|
|||||||
loadMore: loadMoreLocations,
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
// ===== PROJECT STATUS OPTIONS =====
|
||||||
null
|
const projectStatusOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: '1', label: 'Pengajuan' },
|
||||||
|
{ value: '2', label: 'Aktif' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
// ===== FILTER HELPERS =====
|
||||||
setSelectedLocation(val as OptionType);
|
const locationIdValue = useMemo(() => {
|
||||||
updateFilter(
|
if (!formik.values.location_id) return null;
|
||||||
'locationId',
|
return (
|
||||||
val ? ((val as OptionType).value as string) : ''
|
locationOptions.find(
|
||||||
|
(opt) => String(opt.value) === formik.values.location_id
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
};
|
}, [formik.values.location_id, locationOptions]);
|
||||||
|
|
||||||
const [selectedProjectStatus, setSelectedProjectStatus] =
|
const projectStatusValue = useMemo(() => {
|
||||||
useState<OptionType | null>(null);
|
if (!formik.values.project_status) return null;
|
||||||
|
return (
|
||||||
const projectStatusChangeHandler = (
|
projectStatusOptions.find(
|
||||||
val: OptionType | OptionType[] | null
|
(opt) => opt.value === formik.values.project_status
|
||||||
) => {
|
) || null
|
||||||
setSelectedProjectStatus(val as OptionType);
|
|
||||||
updateFilter(
|
|
||||||
'projectStatus',
|
|
||||||
val ? ((val as OptionType).value as string) : ''
|
|
||||||
);
|
);
|
||||||
};
|
}, [formik.values.project_status, projectStatusOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFilter('search', searchValue);
|
||||||
|
}, [searchValue, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableState('closing-table', pathname);
|
||||||
|
}, [pathname, setTableState]);
|
||||||
|
|
||||||
|
// ===== SEARCH CHANGE HANDLER =====
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
setSearchValue(e.target.value);
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
|
const handleFilterModalOpen = () => {
|
||||||
|
filterModal.openModal();
|
||||||
|
formik.validateForm();
|
||||||
|
};
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
if (!isNameSorted) {
|
if (!isNameSorted) {
|
||||||
updateFilter('nameSort', '');
|
// updateFilter('nameSort', '');
|
||||||
} else {
|
} else {
|
||||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
// updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
}
|
}
|
||||||
}, [sorting, updateFilter]);
|
}, [sorting]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
<div className='w-full'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col mb-4'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='relative w-full p-3 pt-0 px-0 flex flex-row justify-between gap-3 flex-wrap after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-end items-end sm:items-center gap-4'>
|
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
placeholder='Cari Closing'
|
placeholder='Search'
|
||||||
value={tableFilterState.search}
|
value={tableFilterState.search ?? ''}
|
||||||
onChange={searchChangeHandler}
|
onChange={searchChangeHandler}
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
startAdornment={
|
||||||
/>
|
<Icon
|
||||||
</div>
|
icon='heroicons:magnifying-glass'
|
||||||
|
width={20}
|
||||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
height={20}
|
||||||
<SelectInput
|
/>
|
||||||
label='Lokasi'
|
}
|
||||||
options={locationOptions}
|
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={locationChangeHandler}
|
|
||||||
onInputChange={setLocationInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
|
||||||
isClearable
|
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6',
|
wrapper: 'w-full min-w-24 max-w-3xs',
|
||||||
|
inputWrapper: 'rounded-xl! shadow-button-soft',
|
||||||
|
input:
|
||||||
|
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<ButtonFilter
|
||||||
label='Status Project'
|
values={tableFilterState}
|
||||||
placeholder='Pilih Status'
|
excludeFields={['page', 'pageSize', 'search']}
|
||||||
options={PROJECT_STATUS_OPTIONS}
|
onClick={handleFilterModalOpen}
|
||||||
value={selectedProjectStatus}
|
className='px-3 py-2.5'
|
||||||
onChange={projectStatusChangeHandler}
|
|
||||||
isClearable
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 sm:col-span-6',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table<Closing>
|
{isLoadingClosings ? (
|
||||||
data={isResponseSuccess(closings) ? closings?.data : []}
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
columns={closingsColumns}
|
<span className='loading loading-spinner loading-xl' />
|
||||||
pageSize={tableFilterState.pageSize}
|
</div>
|
||||||
onPageSizeChange={setPageSize}
|
) : data.length === 0 ? (
|
||||||
rowOptions={[10, 20, 50, 100]}
|
<div className='mt-3'>
|
||||||
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
|
<ClosingTableSkeleton
|
||||||
totalItems={
|
columns={closingsColumns}
|
||||||
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
|
icon={
|
||||||
}
|
<Icon
|
||||||
onPageChange={setPage}
|
icon='heroicons:document-text'
|
||||||
isLoading={isLoadingClosings}
|
className='text-white'
|
||||||
sorting={sorting}
|
width={20}
|
||||||
setSorting={setSorting}
|
height={20}
|
||||||
rowSelection={rowSelection}
|
/>
|
||||||
setRowSelection={setRowSelection}
|
}
|
||||||
className={{
|
/>
|
||||||
containerClassName: cn({
|
</div>
|
||||||
'w-full mb-20':
|
) : (
|
||||||
isResponseSuccess(closings) && closings?.data?.length === 0,
|
<Table<Closing>
|
||||||
}),
|
data={isResponseSuccess(closings) ? closings?.data : []}
|
||||||
}}
|
columns={closingsColumns}
|
||||||
/>
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={isResponseSuccess(closings) ? closings?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(closings) ? closings?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingClosings}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn('mt-3 mb-0'),
|
||||||
|
headerColumnClassName: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
<Modal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
className={{
|
||||||
|
modal: 'p-0',
|
||||||
|
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Modal Header */}
|
||||||
|
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
onClick={filterModal.closeModal}
|
||||||
|
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationIdValue}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!Array.isArray(val)) {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'location_id',
|
||||||
|
val?.value ? String(val.value) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
label='Status Project'
|
||||||
|
placeholder='Pilih Status'
|
||||||
|
options={projectStatusOptions}
|
||||||
|
value={projectStatusValue}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (!Array.isArray(val)) {
|
||||||
|
formik.setFieldValue('project_status', val?.value || null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isClearable={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='soft'
|
||||||
|
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
export type ClosingFilterType = {
|
||||||
|
location_id: string | null;
|
||||||
|
project_status: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClosingFilterSchema = yup.object({
|
||||||
|
location_id: yup.string().nullable(),
|
||||||
|
project_status: yup.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ClosingFilterValues = yup.InferType<typeof ClosingFilterSchema>;
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import Card from '@/components/Card';
|
|
||||||
import { formatCurrency } from '@/lib/helper';
|
|
||||||
import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing';
|
|
||||||
|
|
||||||
interface HppExpeditionReportTableProps {
|
|
||||||
type?: 'detail';
|
|
||||||
initialValues?: BaseHppExpedition;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HppExpeditionReportTable = ({
|
|
||||||
initialValues,
|
|
||||||
}: HppExpeditionReportTableProps) => {
|
|
||||||
const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => {
|
|
||||||
return initialValues?.expedition_costs || [];
|
|
||||||
}, [initialValues]);
|
|
||||||
|
|
||||||
const totals = useMemo(() => {
|
|
||||||
const totalHpp = initialValues?.total_hpp_amount || 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalHpp,
|
|
||||||
};
|
|
||||||
}, [initialValues]);
|
|
||||||
|
|
||||||
const costOfRevenueExpeditionColumns: ColumnDef<BaseExpeditionCost>[] =
|
|
||||||
useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: 'id',
|
|
||||||
accessorKey: 'id',
|
|
||||||
header: 'No',
|
|
||||||
cell: (props) => {
|
|
||||||
return <div>{props.row.index + 1}</div>;
|
|
||||||
},
|
|
||||||
footer: () => (
|
|
||||||
<div className='font-semibold text-gray-900'>
|
|
||||||
Total HPP Ekspedisi
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'expedition_vendor_name',
|
|
||||||
accessorKey: 'expedition_vendor_name',
|
|
||||||
header: 'Nama Ekspedisi',
|
|
||||||
cell: (props) => props.getValue() || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'hpp_amount',
|
|
||||||
accessorKey: 'hpp_amount',
|
|
||||||
header: 'HPP Ekspedisi',
|
|
||||||
cell: (props) => {
|
|
||||||
const value = props.getValue() as number;
|
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
||||||
},
|
|
||||||
footer: () => (
|
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
|
||||||
{formatCurrency(totals.totalHpp)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[totals]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className='w-full'>
|
|
||||||
<div className='p-4'>
|
|
||||||
<h2 className='text-xl font-semibold mb-4'>HPP Ekspedisi</h2>
|
|
||||||
<Card
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full bg-base-100',
|
|
||||||
body: 'p-0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
data={costOfRevenueExpeditionData}
|
|
||||||
columns={costOfRevenueExpeditionColumns}
|
|
||||||
renderFooter={costOfRevenueExpeditionData.length > 0}
|
|
||||||
className={{
|
|
||||||
tableWrapperClassName: 'overflow-x-auto',
|
|
||||||
tableClassName: 'w-full table-auto text-sm',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap',
|
|
||||||
bodyRowClassName:
|
|
||||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
tableFooterClassName:
|
|
||||||
'bg-gray-100 font-semibold border border-gray-200',
|
|
||||||
footerRowClassName: 'border-t-2 border-gray-300',
|
|
||||||
footerColumnClassName:
|
|
||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HppExpeditionReportTable;
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const ClosingTabSkeleton = <T extends object>({
|
||||||
|
columns,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
columns: ColumnDef<T, unknown>[];
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='relative size-full'>
|
||||||
|
<Table
|
||||||
|
data={[]}
|
||||||
|
columns={columns}
|
||||||
|
isLoading={true}
|
||||||
|
className={{
|
||||||
|
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
|
||||||
|
headerColumnClassName: 'whitespace-nowrap',
|
||||||
|
containerClassName: 'mb-0 overflow-hidden',
|
||||||
|
tableWrapperClassName: 'overflow-hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='absolute inset-0 flex items-center justify-center'>
|
||||||
|
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingTabSkeleton;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { Closing } from '@/types/api/closing';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const ClosingTableSkeleton = ({
|
||||||
|
columns,
|
||||||
|
icon,
|
||||||
|
title = 'No Data Available',
|
||||||
|
subtitle = 'There is no closing data displayed. Enter closing data to get started.',
|
||||||
|
}: {
|
||||||
|
columns: ColumnDef<Closing>[];
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='relative size-full'>
|
||||||
|
<Table
|
||||||
|
data={[]}
|
||||||
|
columns={columns}
|
||||||
|
isLoading={true}
|
||||||
|
className={{
|
||||||
|
skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4',
|
||||||
|
headerColumnClassName: 'whitespace-nowrap',
|
||||||
|
containerClassName: 'mb-0 overflow-hidden',
|
||||||
|
tableWrapperClassName: 'overflow-hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className='absolute inset-0 flex items-center justify-center'>
|
||||||
|
<DataStateSkeleton icon={icon} title={title} description={subtitle} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingTableSkeleton;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton';
|
||||||
|
|
||||||
|
const FinanceClosingSkeleton = ({
|
||||||
|
title = 'Data Keuangan Belum Tersedia',
|
||||||
|
subtitle = 'Tidak ada data keuangan untuk periode ini.',
|
||||||
|
iconName = 'heroicons:chart-bar',
|
||||||
|
}: {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
iconName?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-8',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-center p-8'>
|
||||||
|
<DataStateSkeleton
|
||||||
|
icon={
|
||||||
|
<Icon
|
||||||
|
icon={iconName}
|
||||||
|
className='text-white'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={title}
|
||||||
|
description={subtitle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceClosingSkeleton;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import ClosingTabSkeleton from './ClosingTabSkeleton';
|
||||||
|
import { BaseExpeditionCost } from '@/types/api/closing';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const HppExpeditionClosingSkeleton = ({
|
||||||
|
columns,
|
||||||
|
title = 'Data HPP Ekspedisi Belum Tersedia',
|
||||||
|
subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.',
|
||||||
|
iconName = 'heroicons:chart-bar',
|
||||||
|
}: {
|
||||||
|
columns: ColumnDef<BaseExpeditionCost>[];
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
iconName?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ClosingTabSkeleton<BaseExpeditionCost>
|
||||||
|
columns={columns}
|
||||||
|
icon={
|
||||||
|
<Icon icon={iconName} className='text-white' width={20} height={20} />
|
||||||
|
}
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HppExpeditionClosingSkeleton;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user