diff --git a/package-lock.json b/package-lock.json index a20c8c4d..38844543 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@react-pdf/renderer": "^4.3.1", - "@supabase/supabase-js": "^2.89.0", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.21.3", "axios": "^1.12.2", @@ -3951,86 +3950,6 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, - "node_modules/@supabase/auth-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.89.0.tgz", - "integrity": "sha512-wiWZdz8WMad8LQdJMWYDZ2SJtZP5MwMqzQq3ehtW2ngiI3UTgbKiFrvMUUS3KADiVlk4LiGfODB2mrYx7w2f8w==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/functions-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.89.0.tgz", - "integrity": "sha512-XEueaC5gMe5NufNYfBh9kPwJlP5M2f+Ogr8rvhmRDAZNHgY6mI35RCkYDijd92pMcNM7g8pUUJov93UGUnqfyw==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/postgrest-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.89.0.tgz", - "integrity": "sha512-/b0fKrxV9i7RNOEXMno/I1862RsYhuUo+Q6m6z3ar1f4ulTMXnDfv0y4YYxK2POcgrOXQOgKYQx1eArybyNvtg==", - "license": "MIT", - "dependencies": { - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/realtime-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.89.0.tgz", - "integrity": "sha512-aMOvfDb2a52u6PX6jrrjvACHXGV3zsOlWRzZsTIOAJa0hOVvRp01AwC1+nLTGUzxzezejrYeCX+KnnM1xHdl+w==", - "license": "MIT", - "dependencies": { - "@types/phoenix": "^1.6.6", - "@types/ws": "^8.18.1", - "tslib": "2.8.1", - "ws": "^8.18.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/storage-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.89.0.tgz", - "integrity": "sha512-6zKcXofk/M/4Eato7iqpRh+B+vnxeiTumCIP+Tz26xEqIiywzD9JxHq+udRrDuv6hXE+pmetvJd8n5wcf4MFRQ==", - "license": "MIT", - "dependencies": { - "iceberg-js": "^0.8.1", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@supabase/supabase-js": { - "version": "2.89.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.89.0.tgz", - "integrity": "sha512-KlaRwSfFA0fD73PYVMHj5/iXFtQGCcX7PSx0FdQwYEEw9b2wqM7GxadY+5YwcmuEhalmjFB/YvqaoNVF+sWUlg==", - "license": "MIT", - "dependencies": { - "@supabase/auth-js": "2.89.0", - "@supabase/functions-js": "2.89.0", - "@supabase/postgrest-js": "2.89.0", - "@supabase/realtime-js": "2.89.0", - "@supabase/storage-js": "2.89.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -4471,6 +4390,7 @@ "version": "20.19.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4488,12 +4408,6 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/phoenix": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", - "license": "MIT" - }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -4506,7 +4420,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4517,7 +4430,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4544,15 +4456,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", @@ -4599,7 +4502,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -5123,7 +5025,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5829,8 +5730,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -6206,8 +6106,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6468,7 +6367,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6642,7 +6540,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7519,15 +7416,6 @@ "integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==", "license": "ISC" }, - "node_modules/iceberg-js": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8160,7 +8048,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -9380,7 +9267,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9411,7 +9297,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9479,8 +9364,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-number-format": { "version": "5.4.4", @@ -9497,7 +9381,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9666,8 +9549,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -10533,7 +10415,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10701,7 +10582,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10733,6 +10613,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unicode-properties": { @@ -11064,27 +10945,6 @@ "node": ">=0.10.0" } }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xlsx": { "version": "0.20.3", "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", diff --git a/package.json b/package.json index aa90e44a..3a775db2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@react-pdf/renderer": "^4.3.1", - "@supabase/supabase-js": "^2.89.0", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.21.3", "axios": "^1.12.2", diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index f3a78d9d..309addbd 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -7,26 +7,58 @@ import ClosingDetail from '@/components/pages/closing/ClosingDetail'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { FlockApi } from '@/services/api/master-data'; +import { ProjectFlockApi } from '@/services/api/production/project-flock'; +import { ProjectFlockKandangApi } from '@/services/api/production'; const ClosingDetailPage = () => { const router = useRouter(); const searchParams = useSearchParams(); const closingId = searchParams.get('closingId'); + const kandangId = searchParams.get('kandangId'); // project flock kandang ID const { data: closing, isLoading: isLoadingClosing } = useSWR( closingId, (id: number) => ClosingApi.getGeneralInfo(id) ); - // const { data: salesData, isLoading: isLoadingSales } = useSWR( - // closingId ? `sales-${closingId}` : null, - // () => ClosingApi.getPenjualan(Number(closingId)) - // ); + // WORKAROUND - get flock data from closing ID + const { data: projectData, isLoading: isLoadingProject } = useSWR( + `flock-${closingId}`, + () => ProjectFlockApi.getSingle(Number(closingId)) + ); + // WORKAROUND - get kandang data from closing ID + const { data: kandangData, isLoading: isLoadingKandang } = useSWR( + kandangId ? `kandang-${closingId}-${kandangId}` : null, + () => 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( - closingId ? `hpp-ekspedisi-${closingId}` : null, - () => ClosingApi.getHppEkspedisi(Number(closingId)) + kandangId + ? `hpp-ekspedisi-${closingId}-${kandangId}` + : closingId + ? `hpp-ekspedisi-${closingId}` + : null, + () => + kandangId + ? ClosingApi.getHppEkspedisiByKandang( + Number(closingId), + Number(kandangId) + ) + : ClosingApi.getHppEkspedisi(Number(closingId)) ); if (!closingId) { @@ -44,8 +76,12 @@ const ClosingDetailPage = () => { return; } - const isLoading = isLoadingClosing || isLoadingHppEkspedisi; - // const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi; + const isLoading = + isLoadingClosing || + isLoadingSales || + isLoadingHppEkspedisi || + isLoadingProject || + isLoadingKandang; return (
@@ -55,12 +91,18 @@ const ClosingDetailPage = () => { )}
diff --git a/src/app/daily-checklist/master-data/configuration/page.tsx b/src/app/daily-checklist/master-data/configuration/page.tsx new file mode 100644 index 00000000..7b55c2ea --- /dev/null +++ b/src/app/daily-checklist/master-data/configuration/page.tsx @@ -0,0 +1,11 @@ +import { MasterConfigurationContent } from '@/figma-make/components/pages/master-data/configuration/MasterConfigurationContent'; + +const MasterConfigurationPage = () => { + return ( +
+ +
+ ); +}; + +export default MasterConfigurationPage; diff --git a/src/app/expense/realization/page.tsx b/src/app/expense/realization/page.tsx index 027e8d65..50b64e11 100644 --- a/src/app/expense/realization/page.tsx +++ b/src/app/expense/realization/page.tsx @@ -37,7 +37,7 @@ const ExpenseRealization = () => { const isExpenseCanBeRealized = isResponseSuccess(expense) && expense.data.latest_approval.action !== 'REJECTED' && - expense.data.latest_approval.step_number === 3; + expense.data.latest_approval.step_number === 4; if (isResponseSuccess(expense) && !isExpenseCanBeRealized) { if (typeof window !== 'undefined') { diff --git a/src/app/report/finance/page.tsx b/src/app/report/finance/page.tsx new file mode 100644 index 00000000..ae2e85e0 --- /dev/null +++ b/src/app/report/finance/page.tsx @@ -0,0 +1,7 @@ +import FinanceTabs from '@/components/pages/report/finance/FinanceTabs'; + +const Finance = () => { + return ; +}; + +export default Finance; diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index 974ca280..9bfbb1a1 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -39,8 +39,8 @@ const FloatingActionsButton = ({ // Jika tidak ada baris yang dipilih, jangan tampilkan FAB const positionStyles = selectedRowIds.length > 0 - ? 'bottom-[10%] opacity-100' - : 'bottom-[-10%] opacity-0'; + ? 'bottom-[5%] opacity-100' + : 'bottom-[-5%] opacity-0'; // Helper untuk menentukan gaya warna tombol approval const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => { @@ -60,7 +60,7 @@ const FloatingActionsButton = ({ // Container utama FAB
void; }) => { + if (formErrorList.length === 0) return null; + return ( - +
diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index 778124be..07745878 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -19,12 +19,16 @@ import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverhea 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 { ProjectFlock } from '@/types/api/production/project-flock'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; salesData?: BaseClosingSales; hppExpeditionData?: ClosingHppExpedition; + projectData?: ProjectFlock; + kandangData?: ProjectFlockKandang; } const ClosingDetail: React.FC = ({ @@ -32,6 +36,8 @@ const ClosingDetail: React.FC = ({ initialValue, salesData, hppExpeditionData, + projectData, + kandangData, }) => { const [activeTab, setActiveTab] = useState('sapronak'); @@ -52,11 +58,11 @@ const ClosingDetail: React.FC = ({ /> ), }, - // { - // id: 'penjualan', - // label: 'Penjualan', - // content: , - // }, + { + id: 'penjualan', + label: 'Penjualan', + content: , + }, { id: 'overhead', label: 'Overhead', @@ -87,7 +93,9 @@ const ClosingDetail: React.FC = ({
- + + + {!kandangData && ( + + )} { + const chickinPopulation = useMemo(() => { + if (kandangData) { + return kandangData?.chickins?.reduce( + (acc, chickin) => acc + chickin.usage_qty, + 0 + ); + } + return 0; + }, [kandangData]); + return (
@@ -17,7 +34,9 @@ const ClosingGeneralInformationTable = ({ Lokasi : - {initialValue?.location_name} + + {initialValue?.location_name ?? projectData?.location?.name} + Periode @@ -27,12 +46,20 @@ const ClosingGeneralInformationTable = ({ Project Flock : - {initialValue?.project_flock?.name} + + {initialValue?.project_flock?.name ?? + projectData?.flock_name} + Populasi : - {initialValue?.population} Ekor + + {!kandangData + ? (initialValue?.population ?? 0) + : (chickinPopulation ?? 0)}{' '} + Ekor + Jenis Project @@ -40,9 +67,13 @@ const ClosingGeneralInformationTable = ({ {initialValue?.project_type} - Kandang Aktif + Kandang {!kandangData && 'Aktif'} : - {initialValue?.active_house_count} Kandang + + {!kandangData + ? `${initialValue?.active_house_count} Kandang` + : kandangData?.kandang?.name} + Status Pembayaran Penjualan @@ -69,9 +100,13 @@ const ClosingGeneralInformationTable = ({ - + - + diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx new file mode 100644 index 00000000..dd3083a7 --- /dev/null +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -0,0 +1,37 @@ +import Button from '@/components/Button'; +import { ClosingGeneralInformation } from '@/types/api/closing'; +import { ProjectFlock } from '@/types/api/production/project-flock'; + +const ClosingKandangList = ({ + initialValue, + projectData, +}: { + initialValue?: ClosingGeneralInformation; + projectData?: ProjectFlock; +}) => { + return ( +
+
+
+
+

Kandang

+
+ {projectData?.kandangs?.map((kandang) => ( + + ))} +
+
+
+
+
+ ); +}; + +export default ClosingKandangList; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index 6e3b1a95..77cef803 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -14,6 +14,7 @@ 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; @@ -24,9 +25,12 @@ const ClosingSapronakCalculationTable = ({ projectFlockId, closingGeneralInformation, }: ClosingSapronakCalculationTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { data: sapronakCalculation, isLoading } = useSWR( - `/closing/sapronak-calculation/${projectFlockId}`, - () => ClosingApi.getPerhitunganSapronak(projectFlockId), + `/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)), { keepPreviousData: true, } @@ -57,11 +61,11 @@ const ClosingSapronakCalculationTable = ({ cell: (props) => props.row.original.qty_in ? formatNumber(props.row.original.qty_in as number) - : '-', + : '0', footer: total ? () => (
- {total?.qty_in ? formatNumber(total?.qty_in) : '-'} + {total?.qty_in ? formatNumber(total?.qty_in) : '0'}
) : '', @@ -72,11 +76,11 @@ const ClosingSapronakCalculationTable = ({ cell: (props) => props.row.original.qty_out ? formatNumber(props.row.original.qty_out as number) - : '-', + : '0', footer: total ? () => (
- {total?.qty_out ? formatNumber(total?.qty_out) : '-'} + {total?.qty_out ? formatNumber(total?.qty_out) : '0'}
) : '', @@ -87,11 +91,11 @@ const ClosingSapronakCalculationTable = ({ cell: (props) => props.row.original.qty_used ? formatNumber(props.row.original.qty_used as number) - : '-', + : '0', footer: total ? () => (
- {total?.qty_used ? formatNumber(total?.qty_used) : '-'} + {total?.qty_used ? formatNumber(total?.qty_used) : '0'}
) : '', @@ -173,20 +177,12 @@ const ClosingSapronakCalculationTable = ({ [sapronakCalculation] ); - const pulletColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.pullet?.total) - : createColumns(), - [sapronakCalculation] - ); - return (
{/* Table DOC jika kategori Project Flock Growing */} data={ isResponseSuccess(sapronakCalculation) - ? ((closingGeneralInformation?.project_category === 'GROWING' - ? sapronakCalculation.data?.doc?.rows - : sapronakCalculation.data?.pullet?.rows) ?? []) + ? (sapronakCalculation.data?.doc?.rows ?? []) : [] } - columns={ - closingGeneralInformation?.project_category === 'GROWING' - ? docColumns - : pulletColumns - } + columns={docColumns} className={{ containerClassName: 'my-4', }} - renderFooter={isResponseSuccess(sapronakCalculation)} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows.length > 0 + } /> @@ -236,7 +229,10 @@ const ClosingSapronakCalculationTable = ({ className={{ containerClassName: 'my-4', }} - renderFooter={isResponseSuccess(sapronakCalculation)} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows.length > 0 + } /> @@ -259,7 +255,10 @@ const ClosingSapronakCalculationTable = ({ className={{ containerClassName: 'my-4', }} - renderFooter={isResponseSuccess(sapronakCalculation)} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows.length > 0 + } />
diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx index 89cb6615..fe8d46a5 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/sale/SalesReportTable.tsx @@ -215,31 +215,31 @@ const SalesReportTable = ({ return kandang?.name || '-'; }, }, - { - id: 'payment_status', - accessorKey: 'payment_status', - header: 'Status Pembayaran', - cell: (props) => { - const status = props.getValue() as string; - const getStatusColor = (status: string) => { - if (!status) return 'neutral'; - switch (status.toLowerCase()) { - case 'paid': - return 'success'; - case 'tempo': - return 'warning'; - default: - return 'neutral'; - } - }; + // { + // id: 'payment_status', + // accessorKey: 'payment_status', + // header: 'Status Pembayaran', + // cell: (props) => { + // const status = props.getValue() as string; + // const getStatusColor = (status: string) => { + // if (!status) return 'neutral'; + // switch (status.toLowerCase()) { + // case 'paid': + // return 'success'; + // case 'tempo': + // return 'warning'; + // default: + // return 'neutral'; + // } + // }; - return ( - - {status || '-'} - - ); - }, - }, + // return ( + // + // {status || '-'} + // + // ); + // }, + // }, ], [] ); diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index fb8190aa..b65508cb 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -1,63 +1,90 @@ 'use client'; import Button from '@/components/Button'; -import Card from '@/components/Card'; import { Icon } from '@iconify/react'; -import ProductionLineChart from '@/components/pages/dashboard/chart/ProductionLineChart'; -import StandardLineChart from '@/components/pages/dashboard/chart/StandardLineChart'; -import EggWeightBarChart from '@/components/pages/dashboard/chart/EggWeightBarChart'; -import FCRBarChart from '@/components/pages/dashboard/chart/FCRBarChart'; -import ProductionStat from '@/components/pages/dashboard/chart/ProductionStat'; import Modal, { useModal } from '@/components/Modal'; import DateInput from '@/components/input/DateInput'; import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; -import { RadioGroup } from '@/components/input/RadioInput'; import { useState } from 'react'; import useSWR from 'swr'; import { DashboardApi } from '@/services/api/dashboard'; import { useFormik } from 'formik'; -import dashboardProductionFilterSchema from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema'; import { ProjectFlockApi } from '@/services/api/production'; -import { ProductionStandardApi } from '@/services/api/master-data'; +import { KandangApi, LocationApi } from '@/services/api/master-data'; + +import { + DashboardFilterType, + getDashboardFilterSchema, +} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema'; +import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart'; +import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton'; +import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput'; +import { + DashboardFilter, + DashboardMeta, +} from '@/types/api/dashboard/dashboard'; +import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats'; +import { isResponseSuccess } from '@/lib/api-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; + +// Helper function to normalize values to array +const normalizeToArray = ( + value: OptionType | OptionType[] | null | undefined +): number[] => { + if (!value) return []; + if (Array.isArray(value)) { + return value.map((v) => Number(v.value)); + } + return [Number(value.value)]; +}; const DashboardProduction = () => { const filterModal = useModal(); - const [selectedPeriod, setSelectedPeriod] = useState('daily'); - const [selectedStandards, setSelectedStandards] = useState([ - 'hen_day', - 'hen_house', - ]); - const [endpointUrl, setEndpointUrl] = useState('/dashboard'); + const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>( + 'OVERVIEW' + ); + const [endpointUrl, setEndpointUrl] = useState('/dashboards'); + const [selectedLocationIds, setSelectedLocationIds] = useState([]); // ===== FETCH DATA ===== const { data: dashboardProductionResponse, isLoading: isLoadingDashboardProductionData, - error: dashboardProductionError, + mutate: refreshDashboardProductionData, } = useSWR(endpointUrl, () => DashboardApi.getDashboardProductionFetcher(endpointUrl) ); - const dashboardProductionData = - dashboardProductionResponse?.status === 'success' - ? dashboardProductionResponse.data - : undefined; + const dashboardProductionData = isResponseSuccess(dashboardProductionResponse) + ? dashboardProductionResponse.data + : undefined; // ===== SELECT ===== const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { limit: 'limit', - category: 'LAYING', + location_id: selectedLocationIds ? selectedLocationIds.toString() : '', }); const { - options: standardProductionOptions, - isLoadingOptions: isLoadingStandardProductionOptions, - } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name', '', { limit: 'limit', }); + const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } = + useSelect(KandangApi.basePath, 'id', 'name', '', { + limit: 'limit', + location_id: selectedLocationIds ? selectedLocationIds.toString() : '', + }); + const comparisonTypeOptions = [ + { value: 'FARM', label: 'Farm' }, + { value: 'FLOCK', label: 'Flock' }, + { value: 'KANDANG', label: 'Kandang' }, + ]; // ===== FORMIK ===== const formik = useFormik({ @@ -65,57 +92,63 @@ const DashboardProduction = () => { startDate: '', endDate: '', flock: [] as OptionType[], - standard_production_id: [] as OptionType[], - standard_productions: [] as OptionType[], - period: selectedPeriod, - }, - validationSchema: dashboardProductionFilterSchema, + location: [] as OptionType[], + kandang: [] as OptionType[], + analysisMode: analysisMode, + comparisonType: '', + lokasiIds: [], + flockIds: [], + kandangIds: [], + } as DashboardFilterType, + validationSchema: getDashboardFilterSchema(analysisMode), onSubmit: (values) => { console.log(values); - // Build URL with query parameters - const params = new URLSearchParams(); - if (values.startDate) params.set('startDate', values.startDate); - if (values.endDate) params.set('endDate', values.endDate); - - if (values.flock && values.flock.length > 0) { - const flockIds = values.flock - .map((f: OptionType) => f.value || f) - .join(','); - params.set('flock', flockIds); - } - - if ( - values.standard_production_id && - values.standard_production_id.length > 0 - ) { - const standardIds = values.standard_production_id - .map((s: OptionType) => s.value || s) - .join(','); - params.set('standard_production_id', standardIds); - } - - if (selectedStandards.length > 0) { - params.set('standards', selectedStandards.join(',')); - } - - params.set('period', selectedPeriod); - - const newUrl = `/dashboard?${params.toString()}`; - setEndpointUrl(newUrl); - - // Close modal after applying filter - filterModal.closeModal(); + handleApplyFilter({ + start_date: values.startDate || '', + end_date: values.endDate || '', + analysis_mode: values.analysisMode as 'OVERVIEW' | 'COMPARISON', + location_ids: normalizeToArray(values.location), + flock_ids: normalizeToArray(values.flock), + kandang_ids: normalizeToArray(values.kandang), + comparison_type: values.comparisonType, + }); }, }); const handleResetFilter = () => { formik.resetForm(); - setSelectedPeriod('daily'); - setSelectedStandards(['hen_day', 'hen_house']); - setEndpointUrl('/dashboard'); + setAnalysisMode('OVERVIEW'); + setEndpointUrl('/dashboards'); }; + const handleApplyFilter = (values: DashboardFilter) => { + console.log(values); + + // Build query params object, only include non-empty values + const params: Record = {}; + + if (values.start_date) params.start_date = values.start_date; + if (values.end_date) params.end_date = values.end_date; + if (values.analysis_mode) params.analysis_mode = values.analysis_mode; + if (values.location_ids.length > 0) + params.location_ids = values.location_ids.toString(); + if (values.flock_ids.length > 0) + params.flock_ids = values.flock_ids.toString(); + if (values.kandang_ids.length > 0) + params.kandang_ids = values.kandang_ids.toString(); + if (values.comparison_type) params.comparison_type = values.comparison_type; + + setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`); + console.log(endpointUrl); + filterModal.closeModal(); + refreshDashboardProductionData(); + formik.resetForm(); + }; + + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + if (isLoadingDashboardProductionData) { return (
@@ -127,15 +160,57 @@ const DashboardProduction = () => { <>
-

Dashboard

+
- {/* Dashboard Statistics */} - + {/* Dashboard Stats */} + - {/* Charts Grid */} -
- {/* Production Line Chart */} - - - - - {/* Standard Line Chart */} - - - - - {/* Bar Charts Grid - 2 columns */} -
- {/* FCR Bar Chart */} - - - - - {/* Egg Weight Bar Chart */} - - - -
-
+ {/* Use DashboardLineChart component or skeleton */} + {isLoadingDashboardProductionData ? ( + + ) : dashboardProductionData && + dashboardProductionData.charts && + Object.keys(dashboardProductionData.charts).length > 0 ? ( + + ) : ( + + )}
+ {
-
+ {/* Rentang Waktu */}
- +
{
- {/* Flock */} + {/* Analysis Mode */}
- formik.setFieldValue('flock', selected)} - errorMessage={formik.errors.flock as string} - options={flockOptions} - isLoading={isLoadingFlockOptions} - isMulti - isError={ - Boolean(formik.errors.flock) && Boolean(formik.touched.flock) - } - /> -
- - {/* Production */} -
- - formik.setFieldValue('standard_production_id', selected) - } - errorMessage={formik.errors.standard_production_id as string} - options={standardProductionOptions} - isLoading={isLoadingStandardProductionOptions} - isMulti - isError={ - Boolean(formik.errors.standard_production_id) && - Boolean(formik.touched.standard_production_id) - } - /> -
- - {/* Standard */} -
- ({ - value: s, - label: - s === 'hen_day' - ? 'Hen Day' - : s === 'hen_house' - ? 'Hen House' - : s === 'uniformity' - ? 'Uniformity' - : s === 'egg_weight' - ? 'Egg Weight' - : 'Egg Mass', - }))} - options={[ - { value: 'hen_day', label: 'Hen Day' }, - { value: 'hen_house', label: 'Hen House' }, - { value: 'uniformity', label: 'Uniformity' }, - { value: 'egg_weight', label: 'Egg Weight' }, - { value: 'egg_mass', label: 'Egg Mass' }, - ]} - isMulti - onChange={(selected: OptionType | OptionType[] | null) => { - const values = Array.isArray(selected) - ? selected.map((item) => String(item.value)) - : []; - setSelectedStandards( - values.length > 0 ? values : ['hen_day'] - ); + + { + formik.handleChange(e); + setAnalysisMode(e.target.value as 'OVERVIEW' | 'COMPARISON'); + // Reset all dependent fields when analysis mode changes + formik.setFieldValue('location', []); + formik.setFieldValue('flock', []); + formik.setFieldValue('kandang', []); + formik.setFieldValue('comparisonType', ''); + setSelectedLocationIds([]); }} + color='primary' + className={{ + wrapper: 'w-full my-6 font-semibold text-neutral-500', + }} + > + + + +
+ + {formik.values.analysisMode === 'COMPARISON' && ( +
+ option.value === formik.values.comparisonType + )} + onChange={(selected) => + formik.setFieldValue( + 'comparisonType', + selected ? (selected as OptionType).value : '' + ) + } + errorMessage={formik.errors.comparisonType as string} + options={comparisonTypeOptions} + isLoading={isLoadingLocationOptions} + isError={ + Boolean(formik.errors.comparisonType) && + Boolean(formik.touched.comparisonType) + } + /> +
+ )} + + {/* Location */} +
+ { + formik.setFieldValue('location', selected); + // Update selectedLocationIds for kandang filter + setSelectedLocationIds(normalizeToArray(selected)); + // Reset dependent fields when location changes + formik.setFieldValue('flock', []); + formik.setFieldValue('kandang', []); + }} + errorMessage={formik.errors.location as string} + options={locationOptions} + isLoading={isLoadingLocationOptions} + isMulti={ + comparisonTypeOptions.find( + (option) => option.value === formik.values.comparisonType + )?.value === 'FARM' + } isError={ - Boolean(formik.errors.standard_productions) && - Boolean(formik.touched.standard_productions) + Boolean(formik.errors.location) && + Boolean(formik.touched.location) } />
- {/* Periode Perbandingan */} -
- -
- - - - + {/* Flock */} + {!( + formik.values.analysisMode === 'COMPARISON' && + !( + formik.values.comparisonType === 'FLOCK' || + formik.values.comparisonType === 'KANDANG' + ) + ) && ( +
+ + formik.setFieldValue('flock', selected) + } + errorMessage={formik.errors.flock as string} + options={flockOptions} + isLoading={isLoadingFlockOptions} + isMulti={ + comparisonTypeOptions.find( + (option) => option.value === formik.values.comparisonType + )?.value === 'FLOCK' + } + isError={ + Boolean(formik.errors.flock) && + Boolean(formik.touched.flock) + } + />
-
+ )} + + {/* Kandang */} + {!( + formik.values.analysisMode === 'COMPARISON' && + !(formik.values.comparisonType === 'KANDANG') + ) && ( +
+ + formik.setFieldValue('kandang', selected) + } + errorMessage={formik.errors.kandang as string} + options={kandangOptions} + isLoading={isLoadingKandangOptions} + isMulti={ + comparisonTypeOptions.find( + (option) => option.value === formik.values.comparisonType + )?.value === 'KANDANG' + } + isError={ + Boolean(formik.errors.kandang) && + Boolean(formik.touched.kandang) + } + /> +
+ )} + + {/* Action Buttons */}
diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx new file mode 100644 index 00000000..e586b4a3 --- /dev/null +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -0,0 +1,545 @@ +import Button from '@/components/Button'; +import Card from '@/components/Card'; +import Dropdown from '@/components/Dropdown'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import { + Dashboard, + DashboardOverviewCharts, + DashboardComparisonCharts, + DashboardChartsSeries, + DashboardChartsDataset, +} from '@/types/api/dashboard/dashboard'; +import { Icon } from '@iconify/react'; +import { useState, useEffect } from 'react'; +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +type DashboardLineChartProps = { + analysisMode: 'OVERVIEW' | 'COMPARISON'; + data: Dashboard; +}; + +// Type guard to check if charts is DashboardOverviewCharts +function isOverviewCharts( + charts: DashboardOverviewCharts | DashboardComparisonCharts +): charts is DashboardOverviewCharts { + return 'deplesi' in charts; +} + +// Type guard to check if charts is DashboardComparisonCharts +function isComparisonCharts( + charts: DashboardOverviewCharts | DashboardComparisonCharts +): charts is DashboardComparisonCharts { + return 'location' in charts || 'flock' in charts || 'kandang' in charts; +} + +const lineColors: Record = { + body_weight: '#10B981', + std_body_weight: '#10B981', + act_laying: '#1062B9', + std_laying: '#1062B9', + act_egg_weight: '#10B981', + std_egg_weight: '#10B981', + act_feed_intake: '#F52419', + std_feed_intake: '#F52419', + act_uniformity: '#F59E0B', + std_uniformity: '#F59E0B', + act_fcr: '#10B981', + std_fcr: '#10B981', + act_fcr_cum: '#F52419', + std_fcr_cum: '#10B981', + normal: '#10B981', + abnormal: '#F52419', + act_deplesi: '#10B981', + std_deplesi: '#10B981', +}; + +const defaultLineColors: string[] = [ + '#10B981', + '#1062B9', + '#F52419', + '#F59E0B', + '#7F56D9', +]; + +// Helper function to get line color +const getLineColor = ( + seriesId: string | number, + index: number, + mode: 'OVERVIEW' | 'COMPARISON' +): string => { + // For COMPARISON mode, use default colors with cycling + if (mode === 'COMPARISON') { + return defaultLineColors[index % defaultLineColors.length]; + } + + // For OVERVIEW mode, use predefined colors or fallback to default + const predefinedColor = lineColors[seriesId]; + if (predefinedColor) { + return predefinedColor; + } + + // Fallback to default colors with cycling + return defaultLineColors[index % defaultLineColors.length]; +}; + +const DashboardLineChart = ({ + analysisMode, + data, +}: DashboardLineChartProps) => { + const [chartData, setChartData] = + useState('body_weight'); + const [open, setOpen] = useState(false); + // Track which series are visible (by series id) + const [visibleSeries, setVisibleSeries] = useState>( + new Set() + ); + + // Mapping for chart type labels + const chartTypeLabels: Record = { + body_weight: 'Body Weight', + performance: 'Performance', + fcr: 'FCR', + quality_control: 'Quality Control', + deplesi: 'Deplesi', + }; + + // Initialize all series as visible when chartData changes + useEffect(() => { + let seriesData: DashboardChartsSeries[] = []; + + if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || data.charts.flock || data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + // Set all series as visible by default + const allSeriesIds = new Set(seriesData.map((s) => s.id)); + setVisibleSeries(allSeriesIds); + }, [chartData, analysisMode, data.charts]); + + return ( + +
+
+ Performance{' '} + +
+ {analysisMode == 'OVERVIEW' && ( + setOpen(!open)} + > + {chartTypeLabels[chartData]}{' '} +
+ + + } + className={{ + content: 'w-52 mt-3', + }} + controlled={open} + > + + { + setChartData('body_weight'); + setOpen(!open); + }} + /> + { + setChartData('performance'); + setOpen(!open); + }} + /> + { + setChartData('fcr'); + setOpen(!open); + }} + /> + { + setChartData('quality_control'); + setOpen(!open); + }} + /> + { + setChartData('deplesi'); + setOpen(!open); + }} + /> + +
+ )} +
+ + {/* Legend - Dynamic based on series data */} +
+ {(() => { + // Get series data based on current mode and chartData + let seriesData: DashboardChartsSeries[] = []; + + if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || data.charts.flock || data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + return seriesData.map((series, index) => { + const isVisible = visibleSeries.has(series.id); + const isStandard = series.id + .toString() + .toLowerCase() + .includes('std'); + + return ( + + ); + }); + })()} +
+ + {/* Chart */} + + { + // Transform data based on analysisMode + if (analysisMode === 'OVERVIEW') { + // For OVERVIEW mode, use the selected chart data + if (isOverviewCharts(data.charts)) { + const selectedChartData = data.charts[chartData]; + if (!selectedChartData || !selectedChartData.dataset) return []; + return selectedChartData.dataset; + } + return []; + } else { + // For COMPARISON mode, use the first available comparison chart + if (isComparisonCharts(data.charts)) { + const chartData = + data.charts.location || + data.charts.flock || + data.charts.kandang; + + if (!chartData || !chartData.dataset) return []; + return chartData.dataset; + } + return []; + } + })()} + margin={{ + top: 5, + right: 10, + left: 0, + bottom: 5, + }} + > + + + { + // Calculate dynamic domain based on visible data + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } + + // Get all values from visible series + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; + + dataset.forEach((item: DashboardChartsDataset) => { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); + } + }); + }); + + if (allValues.length === 0) return [0, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + + // Add padding (10% on each side) + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + return [domainMin, domainMax]; + })()} + ticks={(() => { + // Calculate dynamic ticks based on domain + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } + + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; + + dataset.forEach((item: DashboardChartsDataset) => { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); + } + }); + }); + + if (allValues.length === 0) return [0, 25, 50, 75, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + // Generate 5 evenly spaced ticks + const range = domainMax - domainMin; + const step = range / 4; + + return [ + domainMin, + Math.round(domainMin + step), + Math.round(domainMin + step * 2), + Math.round(domainMin + step * 3), + domainMax, + ]; + })()} + /> + `Week ${value}`} + formatter={( + value: number | undefined, + name: string | undefined + ) => { + if (value === undefined || name === undefined) return ['', '']; + + // Get series data to find the unit + let seriesData: DashboardChartsSeries[] = []; + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + // Find the series that matches this line's name + const series = seriesData.find((s) => s.label === name); + const unit = series?.unit || ''; + + return [`${value} ${unit}`, name]; + }} + /> + {/* Dynamic Line rendering based on visible series */} + {(() => { + let seriesData: DashboardChartsSeries[] = []; + + if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + return seriesData + .filter((series) => visibleSeries.has(series.id)) + .map((series, index) => { + const isStandard = series.id + .toString() + .toLowerCase() + .includes('std'); + // Use series.id directly as dataKey to match dataset fields + const dataKey = series.id.toString(); + + return ( + + ); + }); + })()} + + +
+ ); +}; + +export default DashboardLineChart; diff --git a/src/components/pages/dashboard/chart/DashboardStats.tsx b/src/components/pages/dashboard/chart/DashboardStats.tsx new file mode 100644 index 00000000..dcb0707f --- /dev/null +++ b/src/components/pages/dashboard/chart/DashboardStats.tsx @@ -0,0 +1,166 @@ +import Alert from '@/components/Alert'; +import Card from '@/components/Card'; +import { formatNumber } from '@/lib/helper'; +import { DashboardStatisticsData } from '@/types/api/dashboard/dashboard'; +import { Icon } from '@iconify/react'; + +interface DashboardStatsProps { + data: DashboardStatisticsData[]; +} + +// Konfigurasi untuk setiap kartu +const CARD_CONFIG = [ + { + key: 'HPP Global', + icon: 'heroicons:banknotes', + alertColor: 'warning' as const, + suffix: ' /Kg', + prefix: 'RP ', + }, + { + key: 'Avg. Selling Price', + icon: 'heroicons:document-currency-dollar', + alertColor: 'success' as const, + suffix: ' /Kg', + prefix: '', + }, + { + key: 'FCR', + icon: 'heroicons:clipboard-document-list', + alertColor: 'info' as const, + suffix: '', + prefix: '', + }, + { + key: 'Mortality', + icon: 'heroicons:exclamation-triangle', + alertColor: 'error' as const, + suffix: ' %', + prefix: '', + }, +]; + +const DashboardStats = ({ data }: DashboardStatsProps) => { + // Helper to get trend icon and color + const getTrendDisplay = (percent: number) => { + const isPositive = percent >= 0; + return { + icon: isPositive + ? 'heroicons:arrow-trending-up' + : 'heroicons:arrow-trending-down', + color: isPositive ? 'text-success' : 'text-error', + value: Math.abs(percent), + }; + }; + + // Helper to format value + const formatValue = (value: number, prefix: string, suffix: string) => { + return ( + <> + {prefix} + {formatNumber(value)} + {suffix && ( + {suffix} + )} + + ); + }; + + return ( +
+ {CARD_CONFIG.map((config) => { + // Find matching data from API + const cardData = data.find((item) => item.label === config.key); + + if (!cardData) { + // Show placeholder card for missing data (FCR & Mortality) + return ( + +
+ From last month +
+
+ Filter Required +
+
+ } + > +
+ + + +
+

+ {config.key} +

+

+ ******** +

+
+
+ + ); + } + + const trend = getTrendDisplay(cardData.percent_last_month); + + return ( + +
+ From last month +
+
+ + {trend.value}% +
+
+ } + > +
+ + + +
+

+ {cardData.label} +

+

+ {formatValue(cardData.value, config.prefix, config.suffix)} +

+
+
+ + ); + })} +
+ ); +}; + +export default DashboardStats; diff --git a/src/components/pages/dashboard/chart/EggWeightBarChart.tsx b/src/components/pages/dashboard/chart/EggWeightBarChart.tsx deleted file mode 100644 index 7a9a02c6..00000000 --- a/src/components/pages/dashboard/chart/EggWeightBarChart.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - Cell, -} from 'recharts'; -import { DashboardProductionEggWeights } from '@/types/api/dashboard/dashboard-production'; - -interface EggWeightBarChartProps { - data?: DashboardProductionEggWeights[]; -} - -const EggWeightBarChart = ({ data }: EggWeightBarChartProps) => { - // Show loading state if no data - if (!data || data.length === 0) { - return ( -
-

- Rata-rata Berat Telur (EW) -

-
-

Memuat data...

-
-
- ); - } - - return ( -
-

Rata-rata Berat Telur (EW)

- - - - - - - value !== undefined ? [`${value} gram`, ''] : ['', ''] - } - cursor={{ fill: 'rgba(59, 130, 246, 0.1)' }} - /> - - {data.map((entry, index) => ( - - ))} - - - -
- ); -}; - -export default EggWeightBarChart; diff --git a/src/components/pages/dashboard/chart/FCRBarChart.tsx b/src/components/pages/dashboard/chart/FCRBarChart.tsx deleted file mode 100644 index 2647c7f7..00000000 --- a/src/components/pages/dashboard/chart/FCRBarChart.tsx +++ /dev/null @@ -1,97 +0,0 @@ -'use client'; - -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - Cell, -} from 'recharts'; -import { DashboardProductionFcrData } from '@/types/api/dashboard/dashboard-production'; - -interface FCRBarChartProps { - data?: DashboardProductionFcrData[]; -} - -// Alternating colors: green and red -const colors = ['#10b981', '#ef4444']; - -const FCRBarChart = ({ data }: FCRBarChartProps) => { - // Show loading state if no data - if (!data || data.length === 0) { - return ( -
-

- Feed Conversion Ratio (FCR) -

-
-

Memuat data...

-
-
- ); - } - - return ( -
-

- Feed Conversion Ratio (FCR) -

- - - - - - - value !== undefined ? [value.toFixed(2), 'FCR'] : ['', ''] - } - cursor={{ fill: 'rgba(16, 185, 129, 0.1)' }} - /> - - {data.map((entry, index) => ( - - ))} - - - -
- ); -}; - -export default FCRBarChart; diff --git a/src/components/pages/dashboard/chart/ProductionLineChart.tsx b/src/components/pages/dashboard/chart/ProductionLineChart.tsx deleted file mode 100644 index 470e09c9..00000000 --- a/src/components/pages/dashboard/chart/ProductionLineChart.tsx +++ /dev/null @@ -1,357 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, - ResponsiveContainer, -} from 'recharts'; - -// Sample data in API format -const sampleApiData: ProductionChartItem[] = [ - { - date: '2025-12-01T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 88 }, - { id: 2, name: 'Flock A-001', data: 92 }, - { id: 3, name: 'Flock B-001', data: 90 }, - { id: 4, name: 'Flock B-002', data: 85 }, - ], - }, - { - date: '2025-12-03T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 85 }, - { id: 2, name: 'Flock A-001', data: 95 }, - { id: 3, name: 'Flock B-001', data: 93 }, - { id: 4, name: 'Flock B-002', data: 87 }, - ], - }, - { - date: '2025-12-05T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 82 }, - { id: 2, name: 'Flock A-001', data: 98 }, - { id: 3, name: 'Flock B-001', data: 91 }, - { id: 4, name: 'Flock B-002', data: 84 }, - ], - }, - { - date: '2025-12-07T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 80 }, - { id: 2, name: 'Flock A-001', data: 89 }, - { id: 3, name: 'Flock B-001', data: 88 }, - { id: 4, name: 'Flock B-002', data: 82 }, - ], - }, - { - date: '2025-12-08T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 83 }, - { id: 2, name: 'Flock A-001', data: 92 }, - { id: 3, name: 'Flock B-001', data: 95 }, - { id: 4, name: 'Flock B-002', data: 85 }, - ], - }, - { - date: '2025-12-11T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 81 }, - { id: 2, name: 'Flock A-001', data: 88 }, - { id: 3, name: 'Flock B-001', data: 92 }, - { id: 4, name: 'Flock B-002', data: 83 }, - ], - }, - { - date: '2025-12-13T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 84 }, - { id: 2, name: 'Flock A-001', data: 90 }, - { id: 3, name: 'Flock B-001', data: 89 }, - { id: 4, name: 'Flock B-002', data: 86 }, - ], - }, - { - date: '2025-12-15T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 82 }, - { id: 2, name: 'Flock A-001', data: 94 }, - { id: 3, name: 'Flock B-001', data: 96 }, - { id: 4, name: 'Flock B-002', data: 84 }, - ], - }, - { - date: '2025-12-17T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 80 }, - { id: 2, name: 'Flock A-001', data: 91 }, - { id: 3, name: 'Flock B-001', data: 93 }, - { id: 4, name: 'Flock B-002', data: 82 }, - ], - }, - { - date: '2025-12-19T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 79 }, - { id: 2, name: 'Flock A-001', data: 88 }, - { id: 3, name: 'Flock B-001', data: 90 }, - { id: 4, name: 'Flock B-002', data: 81 }, - ], - }, - { - date: '2025-12-21T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 81 }, - { id: 2, name: 'Flock A-001', data: 97 }, - { id: 3, name: 'Flock B-001', data: 92 }, - { id: 4, name: 'Flock B-002', data: 83 }, - ], - }, - { - date: '2025-12-23T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 83 }, - { id: 2, name: 'Flock A-001', data: 95 }, - { id: 3, name: 'Flock B-001', data: 98 }, - { id: 4, name: 'Flock B-002', data: 85 }, - ], - }, - { - date: '2025-12-25T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 80 }, - { id: 2, name: 'Flock A-001', data: 89 }, - { id: 3, name: 'Flock B-001', data: 94 }, - { id: 4, name: 'Flock B-002', data: 82 }, - ], - }, - { - date: '2025-12-27T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 82 }, - { id: 2, name: 'Flock A-001', data: 93 }, - { id: 3, name: 'Flock B-001', data: 96 }, - { id: 4, name: 'Flock B-002', data: 84 }, - ], - }, - { - date: '2025-12-28T00:00:00Z', - flocks: [ - { id: 1, name: 'Flock A-002', data: 85 }, - { id: 2, name: 'Flock A-001', data: 96 }, - { id: 3, name: 'Flock B-001', data: 95 }, - { id: 4, name: 'Flock B-002', data: 87 }, - ], - }, -]; - -// Helper function to format date based on period -const formatDateByPeriod = ( - dateString: string, - period: 'daily' | 'weekly' | 'monthly' | 'yearly' -): string => { - const date = new Date(dateString); - const monthNames = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'Mei', - 'Jun', - 'Jul', - 'Agu', - 'Sep', - 'Okt', - 'Nov', - 'Des', - ]; - - switch (period) { - case 'daily': - // Format: "1 Des" - return `${date.getDate()} ${monthNames[date.getMonth()]}`; - - case 'weekly': - // Format: "Week 1 Des" - const weekNumber = Math.ceil(date.getDate() / 7); - return `Week ${weekNumber} ${monthNames[date.getMonth()]}`; - - case 'monthly': - // Format: "Des" - return monthNames[date.getMonth()]; - - case 'yearly': - // Format: "2025" - return date.getFullYear().toString(); - - default: - return dateString; - } -}; - -// Type definitions for API data -interface FlockData { - id: number; - name: string; - data: number; -} - -interface ProductionChartItem { - date: string; - flocks: FlockData[]; -} - -interface ProductionChartsData { - production_charts: ProductionChartItem[]; -} - -// Transform API data to Recharts format -const transformProductionData = (apiData: ProductionChartItem[]) => { - return apiData.map((item) => { - const transformed: Record = { - date: item.date.split('T')[0], // Extract YYYY-MM-DD from ISO string - }; - - // Add each flock's data as a property - item.flocks.forEach((flock) => { - transformed[flock.name] = flock.data; - }); - - return transformed; - }); -}; - -interface ProductionLineChartProps { - period?: 'daily' | 'weekly' | 'monthly' | 'yearly'; - data?: ProductionChartItem[]; // Optional API data -} - -const ProductionLineChart = ({ - period = 'daily', - data: apiData, -}: ProductionLineChartProps) => { - // State to track which lines are hidden - const [hiddenLines, setHiddenLines] = useState([]); - - // Use API data if provided, otherwise use sample data - const chartData = apiData - ? transformProductionData(apiData) - : transformProductionData(sampleApiData); - - // Handle legend click to show/hide lines - const handleLegendClick = (dataKey: string) => { - setHiddenLines((prev) => - prev.includes(dataKey) - ? prev.filter((key) => key !== dataKey) - : [...prev, dataKey] - ); - }; - - return ( -
-

- Performa Produksi per Flock -

- - - - formatDateByPeriod(value, period)} - /> - - - formatDateByPeriod(value as string, period) - } - /> - { - if (e.dataKey) handleLegendClick(e.dataKey as string); - }} - style={{ cursor: 'pointer' }} - /> - - - - - - -
- ); -}; - -export default ProductionLineChart; - -// Export types for external use -export type { FlockData, ProductionChartItem, ProductionChartsData }; diff --git a/src/components/pages/dashboard/chart/ProductionStat.tsx b/src/components/pages/dashboard/chart/ProductionStat.tsx deleted file mode 100644 index 7e299223..00000000 --- a/src/components/pages/dashboard/chart/ProductionStat.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import Card from '@/components/Card'; -import { Icon } from '@iconify/react'; -import { DashboardProductionStatisticsData } from '@/types/api/dashboard/dashboard-production'; -import { formatCurrency } from '@/lib/helper'; - -interface ProductionStatProps { - data?: DashboardProductionStatisticsData[]; -} - -const ProductionStat = ({ data }: ProductionStatProps) => { - // Helper function to get icon based on title - const getIcon = (title: string) => { - if (title.toLowerCase().includes('keuangan')) - return 'heroicons:currency-dollar'; - if (title.toLowerCase().includes('penjualan')) - return 'heroicons:arrow-trending-up'; - if (title.toLowerCase().includes('pembelian')) - return 'heroicons:shopping-cart'; - if (title.toLowerCase().includes('overhead')) return 'heroicons:calculator'; - return 'heroicons:chart-bar'; - }; - - // Helper function to get icon background color - const getIconBgColor = (title: string) => { - if (title.toLowerCase().includes('keuangan')) return 'bg-blue-500'; - if (title.toLowerCase().includes('penjualan')) return 'bg-green-500'; - if (title.toLowerCase().includes('pembelian')) return 'bg-orange-500'; - if (title.toLowerCase().includes('overhead')) return 'bg-purple-500'; - return 'bg-gray-500'; - }; - - // Show loading state if no data - if (!data || data.length === 0) { - return ( -
- {[1, 2, 3, 4].map((i) => ( - -
-
-
-
-
-
- ))} -
- ); - } - - return ( -
- {data.map((stat, index) => ( - -
-
-

{stat.title}

-

- {formatCurrency(stat.value)} -

-

- - {stat.change > 0 ? '+' : ''} - {stat.change}% vs{' '} - {stat.period === 'monthly' ? 'bulan lalu' : 'periode lalu'} -

-
-
-
- -
-
-
-
- ))} -
- ); -}; - -export default ProductionStat; diff --git a/src/components/pages/dashboard/chart/StandardLineChart.tsx b/src/components/pages/dashboard/chart/StandardLineChart.tsx deleted file mode 100644 index 18bcabf6..00000000 --- a/src/components/pages/dashboard/chart/StandardLineChart.tsx +++ /dev/null @@ -1,691 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { - LineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - Legend, - ResponsiveContainer, -} from 'recharts'; - -// Type definitions for API data -interface FlockData { - id: number; - name: string; - data: number; -} - -interface StandardData { - name: string; - value: number; -} - -interface StandardChartItem { - week: number; - standards: StandardData[]; - flocks: FlockData[]; -} - -// Sample data in API format -const sampleApiData: StandardChartItem[] = [ - { - week: 18, - standards: [ - { name: 'hen_day', value: 40 }, - { name: 'hen_house', value: 38 }, - { name: 'uniformity', value: 85 }, - { name: 'egg_weight', value: 52 }, - { name: 'egg_mass', value: 20 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 38 }, - { id: 2, name: 'Flock A-002', data: 37 }, - { id: 3, name: 'Flock B-001', data: 39 }, - { id: 4, name: 'Flock B-002', data: 36 }, - ], - }, - { - week: 20, - standards: [ - { name: 'hen_day', value: 45 }, - { name: 'hen_house', value: 43 }, - { name: 'uniformity', value: 86 }, - { name: 'egg_weight', value: 54 }, - { name: 'egg_mass', value: 24 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 43 }, - { id: 2, name: 'Flock A-002', data: 42 }, - { id: 3, name: 'Flock B-001', data: 44 }, - { id: 4, name: 'Flock B-002', data: 41 }, - ], - }, - { - week: 22, - standards: [ - { name: 'hen_day', value: 48 }, - { name: 'hen_house', value: 46 }, - { name: 'uniformity', value: 87 }, - { name: 'egg_weight', value: 55 }, - { name: 'egg_mass', value: 26 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 47 }, - { id: 2, name: 'Flock A-002', data: 46 }, - { id: 3, name: 'Flock B-001', data: 48 }, - { id: 4, name: 'Flock B-002', data: 45 }, - ], - }, - { - week: 24, - standards: [ - { name: 'hen_day', value: 50 }, - { name: 'hen_house', value: 48 }, - { name: 'uniformity', value: 88 }, - { name: 'egg_weight', value: 56 }, - { name: 'egg_mass', value: 28 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 49 }, - { id: 2, name: 'Flock A-002', data: 48 }, - { id: 3, name: 'Flock B-001', data: 50 }, - { id: 4, name: 'Flock B-002', data: 47 }, - ], - }, - { - week: 26, - standards: [ - { name: 'hen_day', value: 52 }, - { name: 'hen_house', value: 50 }, - { name: 'uniformity', value: 89 }, - { name: 'egg_weight', value: 57 }, - { name: 'egg_mass', value: 30 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 50 }, - { id: 2, name: 'Flock A-002', data: 49 }, - { id: 3, name: 'Flock B-001', data: 51 }, - { id: 4, name: 'Flock B-002', data: 48 }, - ], - }, - { - week: 28, - standards: [ - { name: 'hen_day', value: 55 }, - { name: 'hen_house', value: 53 }, - { name: 'uniformity', value: 90 }, - { name: 'egg_weight', value: 58 }, - { name: 'egg_mass', value: 32 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 53 }, - { id: 2, name: 'Flock A-002', data: 52 }, - { id: 3, name: 'Flock B-001', data: 54 }, - { id: 4, name: 'Flock B-002', data: 51 }, - ], - }, - { - week: 30, - standards: [ - { name: 'hen_day', value: 58 }, - { name: 'hen_house', value: 56 }, - { name: 'uniformity', value: 91 }, - { name: 'egg_weight', value: 59 }, - { name: 'egg_mass', value: 34 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 55 }, - { id: 2, name: 'Flock A-002', data: 54 }, - { id: 3, name: 'Flock B-001', data: 56 }, - { id: 4, name: 'Flock B-002', data: 53 }, - ], - }, - { - week: 32, - standards: [ - { name: 'hen_day', value: 60 }, - { name: 'hen_house', value: 58 }, - { name: 'uniformity', value: 92 }, - { name: 'egg_weight', value: 60 }, - { name: 'egg_mass', value: 36 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 58 }, - { id: 2, name: 'Flock A-002', data: 57 }, - { id: 3, name: 'Flock B-001', data: 59 }, - { id: 4, name: 'Flock B-002', data: 56 }, - ], - }, - { - week: 34, - standards: [ - { name: 'hen_day', value: 62 }, - { name: 'hen_house', value: 60 }, - { name: 'uniformity', value: 92 }, - { name: 'egg_weight', value: 61 }, - { name: 'egg_mass', value: 38 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 60 }, - { id: 2, name: 'Flock A-002', data: 59 }, - { id: 3, name: 'Flock B-001', data: 61 }, - { id: 4, name: 'Flock B-002', data: 58 }, - ], - }, - { - week: 36, - standards: [ - { name: 'hen_day', value: 64 }, - { name: 'hen_house', value: 62 }, - { name: 'uniformity', value: 93 }, - { name: 'egg_weight', value: 62 }, - { name: 'egg_mass', value: 40 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 62 }, - { id: 2, name: 'Flock A-002', data: 61 }, - { id: 3, name: 'Flock B-001', data: 63 }, - { id: 4, name: 'Flock B-002', data: 60 }, - ], - }, - { - week: 38, - standards: [ - { name: 'hen_day', value: 66 }, - { name: 'hen_house', value: 64 }, - { name: 'uniformity', value: 93 }, - { name: 'egg_weight', value: 63 }, - { name: 'egg_mass', value: 42 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 64 }, - { id: 2, name: 'Flock A-002', data: 63 }, - { id: 3, name: 'Flock B-001', data: 65 }, - { id: 4, name: 'Flock B-002', data: 62 }, - ], - }, - { - week: 40, - standards: [ - { name: 'hen_day', value: 68 }, - { name: 'hen_house', value: 66 }, - { name: 'uniformity', value: 94 }, - { name: 'egg_weight', value: 64 }, - { name: 'egg_mass', value: 44 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 66 }, - { id: 2, name: 'Flock A-002', data: 65 }, - { id: 3, name: 'Flock B-001', data: 67 }, - { id: 4, name: 'Flock B-002', data: 64 }, - ], - }, - { - week: 42, - standards: [ - { name: 'hen_day', value: 70 }, - { name: 'hen_house', value: 68 }, - { name: 'uniformity', value: 94 }, - { name: 'egg_weight', value: 65 }, - { name: 'egg_mass', value: 46 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 68 }, - { id: 2, name: 'Flock A-002', data: 67 }, - { id: 3, name: 'Flock B-001', data: 69 }, - { id: 4, name: 'Flock B-002', data: 66 }, - ], - }, - { - week: 44, - standards: [ - { name: 'hen_day', value: 72 }, - { name: 'hen_house', value: 70 }, - { name: 'uniformity', value: 95 }, - { name: 'egg_weight', value: 66 }, - { name: 'egg_mass', value: 48 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 70 }, - { id: 2, name: 'Flock A-002', data: 69 }, - { id: 3, name: 'Flock B-001', data: 71 }, - { id: 4, name: 'Flock B-002', data: 68 }, - ], - }, - { - week: 46, - standards: [ - { name: 'hen_day', value: 74 }, - { name: 'hen_house', value: 72 }, - { name: 'uniformity', value: 95 }, - { name: 'egg_weight', value: 67 }, - { name: 'egg_mass', value: 50 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 72 }, - { id: 2, name: 'Flock A-002', data: 71 }, - { id: 3, name: 'Flock B-001', data: 73 }, - { id: 4, name: 'Flock B-002', data: 70 }, - ], - }, - { - week: 48, - standards: [ - { name: 'hen_day', value: 76 }, - { name: 'hen_house', value: 74 }, - { name: 'uniformity', value: 95 }, - { name: 'egg_weight', value: 68 }, - { name: 'egg_mass', value: 52 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 74 }, - { id: 2, name: 'Flock A-002', data: 73 }, - { id: 3, name: 'Flock B-001', data: 75 }, - { id: 4, name: 'Flock B-002', data: 72 }, - ], - }, - { - week: 50, - standards: [ - { name: 'hen_day', value: 78 }, - { name: 'hen_house', value: 76 }, - { name: 'uniformity', value: 96 }, - { name: 'egg_weight', value: 69 }, - { name: 'egg_mass', value: 54 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 76 }, - { id: 2, name: 'Flock A-002', data: 75 }, - { id: 3, name: 'Flock B-001', data: 77 }, - { id: 4, name: 'Flock B-002', data: 74 }, - ], - }, - { - week: 52, - standards: [ - { name: 'hen_day', value: 80 }, - { name: 'hen_house', value: 78 }, - { name: 'uniformity', value: 96 }, - { name: 'egg_weight', value: 70 }, - { name: 'egg_mass', value: 56 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 78 }, - { id: 2, name: 'Flock A-002', data: 77 }, - { id: 3, name: 'Flock B-001', data: 79 }, - { id: 4, name: 'Flock B-002', data: 76 }, - ], - }, - { - week: 54, - standards: [ - { name: 'hen_day', value: 82 }, - { name: 'hen_house', value: 80 }, - { name: 'uniformity', value: 96 }, - { name: 'egg_weight', value: 71 }, - { name: 'egg_mass', value: 58 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 80 }, - { id: 2, name: 'Flock A-002', data: 79 }, - { id: 3, name: 'Flock B-001', data: 81 }, - { id: 4, name: 'Flock B-002', data: 78 }, - ], - }, - { - week: 56, - standards: [ - { name: 'hen_day', value: 84 }, - { name: 'hen_house', value: 82 }, - { name: 'uniformity', value: 97 }, - { name: 'egg_weight', value: 72 }, - { name: 'egg_mass', value: 60 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 82 }, - { id: 2, name: 'Flock A-002', data: 81 }, - { id: 3, name: 'Flock B-001', data: 83 }, - { id: 4, name: 'Flock B-002', data: 80 }, - ], - }, - { - week: 58, - standards: [ - { name: 'hen_day', value: 86 }, - { name: 'hen_house', value: 84 }, - { name: 'uniformity', value: 97 }, - { name: 'egg_weight', value: 73 }, - { name: 'egg_mass', value: 62 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 84 }, - { id: 2, name: 'Flock A-002', data: 83 }, - { id: 3, name: 'Flock B-001', data: 85 }, - { id: 4, name: 'Flock B-002', data: 82 }, - ], - }, - { - week: 60, - standards: [ - { name: 'hen_day', value: 88 }, - { name: 'hen_house', value: 86 }, - { name: 'uniformity', value: 97 }, - { name: 'egg_weight', value: 74 }, - { name: 'egg_mass', value: 64 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 86 }, - { id: 2, name: 'Flock A-002', data: 85 }, - { id: 3, name: 'Flock B-001', data: 87 }, - { id: 4, name: 'Flock B-002', data: 84 }, - ], - }, - { - week: 62, - standards: [ - { name: 'hen_day', value: 90 }, - { name: 'hen_house', value: 88 }, - { name: 'uniformity', value: 98 }, - { name: 'egg_weight', value: 75 }, - { name: 'egg_mass', value: 66 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 88 }, - { id: 2, name: 'Flock A-002', data: 87 }, - { id: 3, name: 'Flock B-001', data: 89 }, - { id: 4, name: 'Flock B-002', data: 86 }, - ], - }, - { - week: 64, - standards: [ - { name: 'hen_day', value: 92 }, - { name: 'hen_house', value: 90 }, - { name: 'uniformity', value: 98 }, - { name: 'egg_weight', value: 76 }, - { name: 'egg_mass', value: 68 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 90 }, - { id: 2, name: 'Flock A-002', data: 89 }, - { id: 3, name: 'Flock B-001', data: 91 }, - { id: 4, name: 'Flock B-002', data: 88 }, - ], - }, - { - week: 66, - standards: [ - { name: 'hen_day', value: 94 }, - { name: 'hen_house', value: 92 }, - { name: 'uniformity', value: 98 }, - { name: 'egg_weight', value: 77 }, - { name: 'egg_mass', value: 70 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 92 }, - { id: 2, name: 'Flock A-002', data: 91 }, - { id: 3, name: 'Flock B-001', data: 93 }, - { id: 4, name: 'Flock B-002', data: 90 }, - ], - }, - { - week: 68, - standards: [ - { name: 'hen_day', value: 95 }, - { name: 'hen_house', value: 93 }, - { name: 'uniformity', value: 98 }, - { name: 'egg_weight', value: 78 }, - { name: 'egg_mass', value: 72 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 93 }, - { id: 2, name: 'Flock A-002', data: 92 }, - { id: 3, name: 'Flock B-001', data: 94 }, - { id: 4, name: 'Flock B-002', data: 91 }, - ], - }, - { - week: 70, - standards: [ - { name: 'hen_day', value: 96 }, - { name: 'hen_house', value: 94 }, - { name: 'uniformity', value: 99 }, - { name: 'egg_weight', value: 79 }, - { name: 'egg_mass', value: 74 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 94 }, - { id: 2, name: 'Flock A-002', data: 93 }, - { id: 3, name: 'Flock B-001', data: 95 }, - { id: 4, name: 'Flock B-002', data: 92 }, - ], - }, - { - week: 72, - standards: [ - { name: 'hen_day', value: 97 }, - { name: 'hen_house', value: 95 }, - { name: 'uniformity', value: 99 }, - { name: 'egg_weight', value: 80 }, - { name: 'egg_mass', value: 76 }, - ], - flocks: [ - { id: 1, name: 'Flock A-001', data: 95 }, - { id: 2, name: 'Flock A-002', data: 94 }, - { id: 3, name: 'Flock B-001', data: 96 }, - { id: 4, name: 'Flock B-002', data: 93 }, - ], - }, -]; - -// Transform API data to Recharts format -const transformStandardData = ( - apiData: StandardChartItem[], - selectedStandards: string[] = [ - 'hen_day', - 'hen_house', - 'uniformity', - 'egg_weight', - 'egg_mass', - ] -) => { - return apiData.map((item) => { - const transformed: Record = { - week: item.week, - }; - - // Add selected standards as properties - selectedStandards.forEach((standardName) => { - const standardData = item.standards.find((s) => s.name === standardName); - if (standardData) { - transformed[standardName] = standardData.value; - } - }); - - // Add each flock's data as a property - item.flocks.forEach((flock) => { - transformed[flock.name] = flock.data; - }); - - return transformed; - }); -}; - -interface StandardLineChartProps { - data?: StandardChartItem[]; - selectedStandards?: string[]; -} - -const StandardLineChart = ({ - data: apiData, - selectedStandards = [ - 'hen_day', - 'hen_house', - 'uniformity', - 'egg_weight', - 'egg_mass', - ], -}: StandardLineChartProps) => { - // State to track which lines are hidden - const [hiddenLines, setHiddenLines] = useState([]); - - // Use API data if provided, otherwise use sample data - const chartData = apiData - ? transformStandardData(apiData, selectedStandards) - : transformStandardData(sampleApiData, selectedStandards); - - // Handle legend click to show/hide lines - const handleLegendClick = (dataKey: string) => { - setHiddenLines((prev) => - prev.includes(dataKey) - ? prev.filter((key) => key !== dataKey) - : [...prev, dataKey] - ); - }; - - // Standard line colors mapping - const standardColors: Record = { - hen_day: '#94a3b8', - hen_house: '#64748b', - uniformity: '#475569', - egg_weight: '#334155', - egg_mass: '#1e293b', - }; - - // Standard names mapping for display - const standardLabels: Record = { - hen_day: 'Hen Day', - hen_house: 'Hen House', - uniformity: 'Uniformity', - egg_weight: 'Egg Weight', - egg_mass: 'Egg Mass', - }; - - return ( -
-

- Perbandingan Henday per Umur -

- - - - - - - value !== undefined ? [`${value}%`, ''] : ['', ''] - } - labelFormatter={(label) => `Minggu ${label}`} - /> - { - if (e.dataKey) handleLegendClick(e.dataKey as string); - }} - style={{ cursor: 'pointer' }} - /> - {/* Dynamic Standard Lines */} - {selectedStandards.map((standardName) => ( - - ))} - {/* Flock Lines */} - - - - - - -
- ); -}; - -export default StandardLineChart; - -// Export types for external use -export type { FlockData, StandardData, StandardChartItem }; diff --git a/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts index 4ed86a48..d62157a8 100644 --- a/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts +++ b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts @@ -1,16 +1,117 @@ +import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; -const dashboardProductionFilterSchema = yup.object({ - startDate: yup.string().optional(), - endDate: yup.string().optional(), - flock: yup.array().optional(), - standard_production_id: yup.array().optional(), - standard_productions: yup.array().optional(), - period: yup.string().optional(), -}); +export type DashboardFilterType = { + startDate: string; + endDate: string; + analysisMode: string; + comparisonType: string | undefined; + location: OptionType | OptionType[]; + lokasiIds: number[] | undefined; + flock: OptionType | OptionType[] | undefined; + flockIds: number[] | undefined; + kandang: OptionType | OptionType[] | undefined; + kandangIds: number[] | undefined; +}; -export type DashboardProductionFilterValues = yup.InferType< - typeof dashboardProductionFilterSchema ->; +// Schema untuk mode OVERVIEW - semua field required +export const DashboardFilterOverviewSchema: yup.ObjectSchema = + yup.object({ + startDate: yup.string().required('Start date is required'), + endDate: yup.string().required('End date is required'), + analysisMode: yup.string().required('Analysis mode is required'), + comparisonType: yup.string().when('analysisMode', { + is: 'COMPARISON', + then: (schema) => schema.required('Compared by is required'), + otherwise: (schema) => schema.optional(), + }), + lokasiIds: yup.array().optional(), + flockIds: yup.array().optional(), + kandangIds: yup.array().optional(), + location: yup + .mixed() + .required('Farm is required') + .test('is-not-empty', 'Farm is required', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + flock: yup + .mixed() + .required('Flock is required') + .test('is-not-empty', 'Flock is required', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + kandang: yup + .mixed() + .required('Kandang is required') + .test('is-not-empty', 'Kandang is required', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + }); -export default dashboardProductionFilterSchema; +// Schema untuk mode COMPARISON - conditional validation +export const DashboardFilterComparisonSchema: yup.ObjectSchema = + yup.object({ + startDate: yup.string().required('Start date is required'), + endDate: yup.string().required('End date is required'), + analysisMode: yup.string().required('Analysis mode is required'), + comparisonType: yup.string().when('analysisMode', { + is: 'COMPARISON', + then: (schema) => schema.required('Compared by is required'), + otherwise: (schema) => schema.optional(), + }), + lokasiIds: yup.array().optional(), + flockIds: yup.array().optional(), + kandangIds: yup.array().optional(), + location: yup + .mixed() + .required('Farm is required') + .test('is-not-empty', 'Farm is required', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + flock: yup.mixed().when('comparisonType', { + is: (value: string) => value === 'FLOCK' || value === 'KANDANG', + then: (schema) => + schema.test('is-required', 'Flock is required', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + otherwise: (schema) => schema.optional(), + }), + kandang: yup.mixed().when('comparisonType', { + is: 'KANDANG', + then: (schema) => + schema.test('is-required', 'Kandang is required', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + otherwise: (schema) => schema.optional(), + }), + }); + +// Helper function untuk mendapatkan schema yang sesuai berdasarkan analysis mode +export const getDashboardFilterSchema = (analysisMode?: string) => { + return analysisMode === 'OVERVIEW' + ? DashboardFilterOverviewSchema + : DashboardFilterComparisonSchema; +}; + +// Default schema +export const DashboardFilterSchema = DashboardFilterComparisonSchema; + +export type DashboardFilterValues = yup.InferType; diff --git a/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx b/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx new file mode 100644 index 00000000..b479eced --- /dev/null +++ b/src/components/pages/dashboard/skeleton/DashboardLineChartSkeleton.tsx @@ -0,0 +1,100 @@ +import { Icon } from '@iconify/react'; +import { DashboardMeta } from '@/types/api/dashboard/dashboard'; + +const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => { + return ( +
+ {/* Header with title skeleton */} +
+ Performance{' '} + +
+ + {/* Chart area with axes skeleton */} +
+ {/* Main chart container */} +
+ {/* Y-axis skeleton (left side) */} +
+ {[1, 2, 3, 4, 5, 6].map((item) => ( +
+ ))} +
+ + {/* Chart content area */} +
+ {/* Empty state centered in chart area */} +
+ {!meta?.filters && ( + <> + {/* Filter icon */} +
+ +
+ + {/* Empty state text */} +

+ No Filters Selected +

+

+ Please choose filters to narrow down your results and make + your search easier. +

+ + )} + {meta?.filters && ( + <> + {/* Filter icon */} +
+ +
+ + {/* Empty state text */} +

+ Data Not Yet Available +

+

+ Please change your filters to get the data. +

+ + )} +
+ + {/* Placeholder for chart height */} +
+ + {/* X-axis skeleton (bottom) */} +
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( +
+ ))} +
+
+
+
+
+ ); +}; + +export default DashboardLineChartSkeleton; diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index 859b19ce..9c84ed4d 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -28,7 +28,7 @@ const ExpenseDetail: React.FC = ({ initialValues }) => { if ( initialValues?.latest_approval && - initialValues?.latest_approval.step_number >= 4 && + initialValues?.latest_approval.step_number >= 5 && initialValues.latest_approval.action !== 'REJECTED' ) { validTabs.push({ diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index ccd57ec3..ea4a0e8d 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -48,6 +48,13 @@ const ExpenseRealizationContent = ({ const realizationDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 657c5e5c..a1ad4643 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -59,34 +59,40 @@ const ExpenseRequestContent = ({ const isLatestApprovalRejectedOrDone = isLatestApprovalRejected || - initialValues?.latest_approval.step_number === 5; + initialValues?.latest_approval.step_number === 6; - const isCurrentApprovalOnManager = + const isCurrentApprovalOnHeadArea = !isLatestApprovalRejected && initialValues?.latest_approval.step_number === 1; - const isCurrentApprovalOnFinance = + const isCurrentApprovalOnUnitVicePresident = !isLatestApprovalRejected && initialValues?.latest_approval.step_number === 2; + const isCurrentApprovalOnFinance = + !isLatestApprovalRejected && + initialValues?.latest_approval.step_number === 3; + const isCurrentApprovalOnRealization = !isLatestApprovalRejected && - initialValues?.latest_approval.step_number === 4; + initialValues?.latest_approval.step_number === 5; const showEditButton = - initialValues?.latest_approval.step_number !== 5 && + initialValues?.latest_approval.step_number !== 6 && (initialValues?.latest_approval.step_number === 1 || initialValues?.latest_approval.step_number === 2 || - initialValues?.latest_approval.step_number === 3); + initialValues?.latest_approval.step_number === 3 || + initialValues?.latest_approval.step_number === 4); const showRejectButton = !isLatestApprovalRejected && (initialValues?.latest_approval.step_number === 1 || - initialValues?.latest_approval.step_number === 2); + initialValues?.latest_approval.step_number === 2 || + initialValues?.latest_approval.step_number === 3); const isExpenseCanBeRealized = !isLatestApprovalRejected && - initialValues?.latest_approval.step_number === 3; + initialValues?.latest_approval.step_number === 4; // Modal hooks const deleteModal = useModal(); @@ -174,8 +180,15 @@ const ExpenseRequestContent = ({ let approveResponse: BaseApiResponse | undefined = undefined; - if (isCurrentApprovalOnManager) { - approveResponse = await ExpenseApi.approveManager( + if (isCurrentApprovalOnHeadArea) { + approveResponse = await ExpenseApi.approveHeadArea( + initialValues.id, + notes + ); + } + + if (isCurrentApprovalOnUnitVicePresident) { + approveResponse = await ExpenseApi.approveUnitVicePresident( initialValues.id, notes ); @@ -207,8 +220,15 @@ const ExpenseRequestContent = ({ let rejectResponse: BaseApiResponse | undefined = undefined; - if (isCurrentApprovalOnManager) { - rejectResponse = await ExpenseApi.rejectManager(initialValues.id, notes); + if (isCurrentApprovalOnHeadArea) { + rejectResponse = await ExpenseApi.rejectHeadArea(initialValues.id, notes); + } + + if (isCurrentApprovalOnUnitVicePresident) { + rejectResponse = await ExpenseApi.rejectUnitVicePresident( + initialValues.id, + notes + ); } if (isCurrentApprovalOnFinance) { @@ -231,6 +251,13 @@ const ExpenseRequestContent = ({ const requestDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; @@ -255,8 +282,8 @@ const ExpenseRequestContent = ({ {/* TODO: apply RBAC */}
- {isCurrentApprovalOnManager && ( - + {isCurrentApprovalOnHeadArea && ( + + + )} + + {isCurrentApprovalOnUnitVicePresident && ( + + )} @@ -304,7 +345,8 @@ const ExpenseRequestContent = ({ {showRejectButton && ( @@ -454,8 +496,8 @@ const ExpenseRequestContent = ({
@@ -1773,16 +1773,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { @@ -1790,16 +1790,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx index 4d60f69a..c5683fff 100644 --- a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx +++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx @@ -41,6 +41,8 @@ import { cn } from '@/lib/helper'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { TRANSFER_TO_LAYING_APPROVAL_LINE } from '@/config/approval-line'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface TransferToLayingFormProps { type?: 'add' | 'edit' | 'detail'; @@ -160,6 +162,7 @@ const TransferToLayingForm = ({ }); const { setValues: formikSetValues, values: formikValues } = formik; + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); const { flockSourceKandangs: flockSourceKandangsValue, flockDestinationKandangs: flockDestinationKandangsValue, @@ -539,7 +542,7 @@ const TransferToLayingForm = ({ @@ -777,6 +780,8 @@ const TransferToLayingForm = ({ /> + + {formErrorMessage && (
Submit diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx index 1eba4ea3..3ddbd6cf 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -31,7 +31,10 @@ import { MarketingReportApi } from '@/services/api/report/marketing-report'; import { MARKETING_TYPE_OPTIONS } from '@/config/constant'; import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; -import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { + DailyMarketingReport, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; import { isResponseError } from '@/lib/api-helper'; const DailyMarketingReportContent = () => { @@ -191,9 +194,10 @@ const DailyMarketingReportContent = () => { const queryString = `?${params.toString()}`; try { - const dailyMarketingsReport = await httpClient< - BaseApiResponse - >(`${MarketingReportApi.basePath}${queryString}`); + const dailyMarketingsReport = + await httpClient( + `${MarketingReportApi.basePath}${queryString}` + ); if (isResponseError(dailyMarketingsReport)) { toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); @@ -202,7 +206,10 @@ const DailyMarketingReportContent = () => { const openPdf = async () => { const dailyMarketingReportPdfBlob = await pdf( - + ).toBlob(); const dailyMarketingReportPdfUrl = URL.createObjectURL( @@ -213,7 +220,10 @@ const DailyMarketingReportContent = () => { const downloadPdf = async () => { const blob = await pdf( - + ).toBlob(); const url = URL.createObjectURL(blob); diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx index 337892b3..86ee29bc 100644 --- a/src/components/pages/report/DailyMarketingReportPDF.tsx +++ b/src/components/pages/report/DailyMarketingReportPDF.tsx @@ -9,11 +9,15 @@ import { View, } from '@react-pdf/renderer'; -import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { + DailyMarketingReport, + SalesSummary, +} from '@/types/api/report/marketing'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; interface DailyMarketingReportPDFProps { data?: DailyMarketingReport; + total?: SalesSummary; } const DailyMarketingReportPDFStyle = StyleSheet.create({ @@ -267,9 +271,12 @@ const DailyMarketingReportPDFStyle = StyleSheet.create({ }, }); -const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { - const rows = data?.rows || []; - const summary = data?.summary; +const DailyMarketingReportPDF = ({ + data, + total, +}: DailyMarketingReportPDFProps) => { + const rows = data || []; + const summary = total; return ( @@ -409,7 +416,7 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { - {formatDate(row.do_date, 'DD/MM/YYYY')} + {formatDate(row.realization_date, 'DD/MM/YYYY')} @@ -429,7 +436,7 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { - {row.sales} + {row.sales.name} @@ -518,6 +525,19 @@ const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { {formatCurrency(summary?.total_sales_amount ?? 0)} + + + Total HPP Per KG: + + + {formatCurrency(summary?.total_hpp_price_per_kg ?? 0)} + + formatDate(props.row.original.do_date, 'DD-MMM-YYYY'), + accessorKey: 'realization_date', + header: 'Tanggal Realisasi', + cell: (props) => + formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), }, { accessorKey: 'aging_days', @@ -84,6 +85,7 @@ const DailyMarketingsTable = ({ { accessorKey: 'sales', header: 'Sales/Marketing', + cell: (props) => props.row.original.sales.name, }, { accessorKey: 'vehicle_number', @@ -106,10 +108,10 @@ const DailyMarketingsTable = ({ cell: (props) => formatNumber(props.row.original.qty), footer: () => { const totalQty = isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.summary.total_qty + ? dailyMarketings?.total?.total_qty : 0; - return formatNumber(totalQty); + return totalQty ? formatNumber(totalQty) : '-'; }, }, { @@ -123,10 +125,10 @@ const DailyMarketingsTable = ({ cell: (props) => formatNumber(props.row.original.total_weight_kg), footer: () => { const totalWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.summary.total_weight_kg + ? dailyMarketings?.total?.total_weight_kg : 0; - return formatNumber(totalWeightKg); + return totalWeightKg ? formatNumber(totalWeightKg) : '-'; }, }, { @@ -138,6 +140,13 @@ const DailyMarketingsTable = ({ accessorKey: 'hpp_price_per_kg', header: 'HPP (Rp)', cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + footer: () => { + const totalHppPricePerKg = isResponseSuccess(dailyMarketings) + ? dailyMarketings?.total?.total_hpp_price_per_kg + : 0; + + return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-'; + }, }, { accessorKey: 'sales_amount', @@ -145,10 +154,10 @@ const DailyMarketingsTable = ({ cell: (props) => formatCurrency(props.row.original.sales_amount), footer: () => { const totalSalesAmount = isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.summary.total_sales_amount + ? dailyMarketings?.total?.total_sales_amount : 0; - return formatCurrency(totalSalesAmount); + return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-'; }, }, ]; @@ -167,7 +176,7 @@ const DailyMarketingsTable = ({ if (!open) { setOpen( isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.rows.length > 0 + ? dailyMarketings.data.length > 0 : false ); } @@ -215,9 +224,7 @@ const DailyMarketingsTable = ({ data={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.data.rows - : [] + isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : [] } columns={dailyMarketingColumns} pageSize={pageSize} @@ -242,7 +249,7 @@ const DailyMarketingsTable = ({ containerClassName: cn({ 'w-full mb-20': isResponseSuccess(dailyMarketings) && - dailyMarketings?.data?.rows.length === 0, + dailyMarketings?.data?.length === 0, }), }} /> diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx new file mode 100644 index 00000000..aaaae985 --- /dev/null +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; +import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; + +const FinanceTabs = () => { + const tabs = [ + { + id: '1', + label: 'Kontrol Pembayaran Customer', + + content: , + }, + { + id: '2', + label: 'Rekapitulasi Hutang Ke Supplier', + + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default FinanceTabs; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx new file mode 100644 index 00000000..88c556de --- /dev/null +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -0,0 +1,424 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; + +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + mainTitle: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 5, + color: '#1f74bf', + }, + supplierTitle: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 8, + color: '#1f74bf', + }, + supplierInfo: { + fontSize: 9, + marginBottom: 5, + color: '#333333', + }, + table: { + borderWidth: 1, + borderColor: '#000000', + marginBottom: 15, + }, + tableRow: { + flexDirection: 'row', + }, + tableHeader: { + backgroundColor: '#F5F5F5', + }, + tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'left', + }, + tableCellNo: { + flex: 0.5, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 7, + }, + tableCellHeader: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', + }, + tableCellHeaderRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + }, + tableCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'right', + }, + tableCellCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + summaryRow: { + backgroundColor: '#F0F0F0', + fontWeight: 'bold', + }, +}); + +interface CustomerPaymentExportPDFParams { + data: CustomerPaymentReport[]; +} + +const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { + return ( + + {params.data.map((customerReport, customerIndex) => ( + + {/* Title and Customer Info */} + + + Laporan > Kontrol Pembayaran Customer + + + {customerReport.customer.name} + + + {customerReport.customer.address || ''} + + {customerReport.summary && ( + + Total Saldo Piutang:{' '} + {formatCurrency( + customerReport.summary.total_accounts_receivable + )} + + )} + + + {/* Table */} + + {/* Table Header */} + + + No + + + Tgl DO/Bayar + + + Tgl Realisasi + + + Aging + + + Referensi + + + No. Polisi + + + Qty + + + Berat (Kg) + + + AVG + + + Harga Awal + + + CN + + + Harga Akhir + + + PPN (%) + + + Total + + + Pembayaran + + + Saldo Piutang + + + Ket + + + Pengambilan + + + Sales + + + + {/* Table Body */} + {customerReport.rows.map((item, index) => ( + + + {index + 1} + + + + {item.do_date ? formatDate(item.do_date, 'DD MMM YY') : '-'} + + + + + {item.realization_date + ? formatDate(item.realization_date, 'DD MMM YY') + : '-'} + + + + + {item.aging_day ? formatNumber(item.aging_day) : '-'} hari + + + + {item.reference || '-'} + + + {item.vehicle_plate || '-'} + + + {formatNumber(item.qty)} + + + {formatNumber(item.weight)} + + + {formatNumber(item.average_weight)} + + + {formatCurrency(item.price)} + + + {formatCurrency(item.credit_note)} + + + {formatCurrency(item.final_price)} + + + {formatNumber(item.ppn)}% + + + {formatCurrency(item.total)} + + + {formatCurrency(item.payment)} + + + {formatCurrency(item.accounts_receivable)} + + + {item.notes || '-'} + + + {item.pickup_info || '-'} + + + {item.sales_marketing || '-'} + + + ))} + + {/* Summary Row */} + {customerReport.summary && ( + + + Total + + + + + + + + + + + + + + + + + + {formatNumber(customerReport.summary.total_qty)} + + + + {formatNumber(customerReport.summary.total_weight)} + + + + + + + + {formatCurrency( + customerReport.summary.total_initial_amount + )} + + + + + {formatCurrency(customerReport.summary.total_credit_note)} + + + + + {formatCurrency(customerReport.summary.total_final_amount)} + + + + + + + + {formatCurrency(customerReport.summary.total_grand_amount)} + + + + + {formatCurrency(customerReport.summary.total_payment)} + + + + + {formatCurrency( + customerReport.summary.total_accounts_receivable + )} + + + + + + + + + + + + + )} + + + ))} + + ); +}; + +export const generateCustomerPaymentPDF = async ( + params: CustomerPaymentExportPDFParams +): Promise => { + const PDFDocument = createPDFDocument(params); + + try { + const blob = await pdf(PDFDocument).toBlob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `laporan-kontrol-pembayaran-customer-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + throw error; + } +}; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx new file mode 100644 index 00000000..d51aa3b7 --- /dev/null +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -0,0 +1,117 @@ +'use client'; + +import * as XLSX from 'xlsx'; +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; + +interface CustomerPaymentExportExcelParams { + data: CustomerPaymentReport[]; +} + +export const generateCustomerPaymentExcel = ( + params: CustomerPaymentExportExcelParams +): void => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = XLSX.utils.book_new(); + + params.data.forEach((customerReport) => { + const customerData = customerReport.rows; + const customerName = customerReport.customer.name || 'Unknown Customer'; + + const excelData: { [key: string]: string | number }[] = customerData.map( + (item, index) => ({ + No: index + 1, + 'Tanggal DO/Bayar': item.do_date + ? formatDate(item.do_date, 'DD MMM YYYY') + : '', + 'Tanggal Realisasi': item.realization_date + ? formatDate(item.realization_date, 'DD MMM YYYY') + : '', + Aging: formatNumber(item.aging_day || 0), + Referensi: item.reference || '', + 'Nomor Polisi': Array.isArray(item.vehicle_plate) + ? item.vehicle_plate.join(', ') + : '', + 'Ekor/Qty': formatNumber(item.qty || 0), + 'Berat (Kg)': formatNumber(item.weight || 0), + AVG: formatNumber(item.average_weight || 0), + 'Harga Awal': formatCurrency(item.price || 0), + CN: formatCurrency(item.credit_note || 0), + 'Harga Akhir': formatCurrency(item.final_price || 0), + 'PPN (%)': formatNumber(item.ppn || 0), + Total: formatCurrency(item.total || 0), + Pembayaran: formatCurrency(item.payment || 0), + 'Saldo Piutang': formatCurrency(item.accounts_receivable || 0), + Keterangan: item.notes || '', + Pengambilan: item.pickup_info || '', + 'Sales/Marketing': item.sales_marketing || '', + }) + ); + + if (customerReport.summary) { + excelData.push({ + No: 'Total', + 'Tanggal DO/Bayar': '', + 'Tanggal Realisasi': '', + Aging: '', + Referensi: '', + 'Nomor Polisi': '', + 'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0), + 'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0), + AVG: '', + 'Harga Awal': formatCurrency( + customerReport.summary.total_initial_amount || 0 + ), + CN: formatCurrency(customerReport.summary.total_credit_note || 0), + 'Harga Akhir': formatCurrency( + customerReport.summary.total_final_amount || 0 + ), + 'PPN (%)': '', + Total: formatCurrency(customerReport.summary.total_grand_amount || 0), + Pembayaran: formatCurrency(customerReport.summary.total_payment || 0), + 'Saldo Piutang': formatCurrency( + customerReport.summary.total_accounts_receivable || 0 + ), + Keterangan: '', + Pengambilan: '', + 'Sales/Marketing': '', + }); + } + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Tanggal DO/Bayar + { wch: 15 }, // Tanggal Realisasi + { wch: 8 }, // Aging + { wch: 12 }, // Referensi + { wch: 15 }, // Nomor Polisi + { wch: 10 }, // Ekor/Qty + { wch: 12 }, // Berat + { wch: 10 }, // AVG + { wch: 15 }, // Harga Awal + { wch: 10 }, // CN + { wch: 15 }, // Harga Akhir + { wch: 10 }, // PPN + { wch: 15 }, // Total + { wch: 15 }, // Pembayaran + { wch: 15 }, // Saldo Piutang + { wch: 20 }, // Keterangan + { wch: 15 }, // Pengambilan + { wch: 20 }, // Sales/Marketing + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + customerName.length > 31 ? customerName.substring(0, 31) : customerName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `laporan-kontrol-pembayaran-customer-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx new file mode 100644 index 00000000..7f6fa45b --- /dev/null +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -0,0 +1,423 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; + +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { DebtSupplier } from '@/types/api/report/debt-supplier'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + mainTitle: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 5, + color: '#1f74bf', + }, + supplierTitle: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 8, + color: '#1f74bf', + }, + supplierInfo: { + fontSize: 9, + marginBottom: 5, + color: '#333333', + }, + table: { + borderWidth: 1, + borderColor: '#000000', + marginBottom: 15, + }, + tableRow: { + flexDirection: 'row', + }, + tableHeader: { + backgroundColor: '#F5F5F5', + }, + tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'left', + }, + tableCellNo: { + flex: 0.5, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 7, + }, + tableCellHeader: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', + }, + tableCellHeaderRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + }, + tableCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'right', + }, + tableCellCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + summaryRow: { + backgroundColor: '#F0F0F0', + fontWeight: 'bold', + }, +}); + +interface DebtSupplierExportPDFParams { + data: DebtSupplier[]; +} + +const createPDFDocument = (params: DebtSupplierExportPDFParams) => { + return ( + + {params.data.map((supplierReport, supplierIndex) => ( + + {/* Title and Supplier Info */} + + + Laporan > Rekapitulasi Hutang ke Supplier + + + {supplierReport.supplier.name} + + + + {/* Table */} + + {/* Table Header */} + + + No + + + No. PR + + + No. PO + + + Tgl Terima + + + Tgl PO + + + Aging + + + Area + + + Gudang + + + Tgl Jatuh Tempo + + + Status Jatuh Tempo + + + Total Harga + + + Pembayaran + + + Hutang + + + Status + + + No. Perjalanan + + + + {/* Table Body */} + {supplierReport.rows.map((item, index) => ( + + + {index + 1} + + + {item.pr_number || '-'} + + + {item.po_number || '-'} + + + + {item.received_date + ? item.received_date != '-' + ? formatDate(item.received_date, 'DD MMM YY') + : '-' + : '-'} + + + + + {item.po_date + ? item.po_date != '-' + ? formatDate(item.po_date, 'DD MMM YY') + : '-' + : '-'} + + + + {formatNumber(item.aging)} Hari + + + {item.area?.name || '-'} + + + {item.warehouse?.name || '-'} + + + + {item.due_date + ? item.due_date != '-' + ? formatDate(item.due_date, 'DD MMM YY') + : '-' + : '-'} + + + + {item.due_status || '-'} + + + {formatCurrency(item.total_price)} + + + {formatCurrency(item.payment_price)} + + + {formatCurrency(item.debt_price)} + + + {item.status || '-'} + + + {item.travel_number || '-'} + + + ))} + + {/* Summary Row */} + {supplierReport.total && ( + + + Total + + + + + + + + + + + + + + + {formatNumber(supplierReport.total.aging)} Hari + + + + + + + + + + + + + + + + {formatCurrency(supplierReport.total.total_price)} + + + + + {formatCurrency(supplierReport.total.payment_price)} + + + + {formatCurrency(supplierReport.total.debt_price)} + + + + + + + + + )} + + + ))} + + ); +}; + +export const generateDebtSupplierPDF = async ( + params: DebtSupplierExportPDFParams +): Promise => { + const PDFDocument = createPDFDocument(params); + + try { + const blob = await pdf(PDFDocument).toBlob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + throw error; + } +}; diff --git a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx new file mode 100644 index 00000000..58b07e30 --- /dev/null +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -0,0 +1,105 @@ +'use client'; + +import * as XLSX from 'xlsx'; +import { formatDate } from '@/lib/helper'; +import { DebtSupplier } from '@/types/api/report/debt-supplier'; + +interface DebtSupplierExportExcelParams { + data: DebtSupplier[]; +} + +export const generateDebtSupplierExcel = ( + params: DebtSupplierExportExcelParams +): void => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = XLSX.utils.book_new(); + + params.data.forEach((supplierReport) => { + const supplierData = supplierReport.rows; + const supplierName = supplierReport.supplier.name || 'Unknown Supplier'; + + const excelData: { [key: string]: string | number }[] = supplierData.map( + (item, index) => ({ + No: index + 1, + 'Nomor PR': item.pr_number || '', + 'Nomor PO': item.po_number || '', + 'Tanggal Terima': item.received_date + ? item.received_date != '-' + ? formatDate(item.received_date, 'MM/DD/YYYY') + : '-' + : '-', + 'Tanggal PO': item.po_date + ? item.po_date != '-' + ? formatDate(item.po_date, 'MM/DD/YYYY') + : '-' + : '-', + 'Aging (Hari)': item.aging || 0, + Area: item.area?.name || '', + Gudang: item.warehouse?.name || '', + 'Tanggal Jatuh Tempo': item.due_date + ? item.due_date != '-' + ? formatDate(item.due_date, 'MM/DD/YYYY') + : '-' + : '-', + 'Status Jatuh Tempo': item.due_status || '', + 'Total Harga': item.total_price || 0, + 'Harga Pembayaran': item.payment_price || 0, + 'Harga Hutang': item.debt_price || 0, + Status: item.status || '', + 'Nomor Perjalanan': item.travel_number || '', + }) + ); + + if (supplierReport.total) { + excelData.push({ + No: 'Total', + 'Nomor PR': '', + 'Nomor PO': '', + 'Tanggal Terima': '', + 'Tanggal PO': '', + 'Aging (Hari)': supplierReport.total.aging || 0, + Area: '', + Gudang: '', + 'Tanggal Jatuh Tempo': '', + 'Status Jatuh Tempo': '', + 'Total Harga': supplierReport.total.total_price || 0, + 'Harga Pembayaran': supplierReport.total.payment_price || 0, + 'Harga Hutang': supplierReport.total.debt_price || 0, + Status: '', + 'Nomor Perjalanan': '', + }); + } + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Nomor PR + { wch: 15 }, // Nomor PO + { wch: 15 }, // Tanggal PR + { wch: 15 }, // Tanggal PO + { wch: 12 }, // Aging + { wch: 15 }, // Area + { wch: 15 }, // Gudang + { wch: 18 }, // Tanggal Jatuh Tempo + { wch: 18 }, // Status Jatuh Tempo + { wch: 15 }, // Total Harga + { wch: 15 }, // Harga Pembayaran + { wch: 15 }, // Harga Hutang + { wch: 12 }, // Status + { wch: 15 }, // Nomor Perjalanan + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + supplierName.length > 31 ? supplierName.substring(0, 31) : supplierName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx new file mode 100644 index 00000000..1d8d1993 --- /dev/null +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -0,0 +1,721 @@ +import { useState, useMemo, useCallback } from 'react'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; +import SelectInput, { + useSelect, + OptionType, +} from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { CustomerApi } from '@/services/api/master-data'; +import { FinanceApi } from '@/services/api/report/finance-report'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + CustomerPaymentReport, + CustomerPaymentSummary, +} from '@/types/api/report/customer-payment'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Pagination from '@/components/Pagination'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; +import Modal from '@/components/Modal'; +import { useModal } from '@/components/Modal'; +import toast from 'react-hot-toast'; +import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; +import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; + +const CustomerPaymentTab = () => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== PAGINATION STATE ===== + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== FILTER STATE ===== + const [filterCustomer, setFilterCustomer] = useState([]); + const [filterSales, setFilterSales] = useState([]); + const [filterStartDate, setFilterStartDate] = useState(''); + const [filterEndDate, setFilterEndDate] = useState(''); + const [filterErrors, setFilterErrors] = useState>({}); + + const filterModal = useModal(); + + const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = + useSelect(CustomerApi.basePath, 'id', 'name', 'search'); + + const salesOptions = useMemo( + () => [ + { value: 'Sales A', label: 'Sales A' }, + { value: 'Sales B', label: 'Sales B' }, + { value: 'Sales C', label: 'Sales C' }, + // TODO: Fetch sales options from API + ], + [] + ); + + const dataTypeOptions = useMemo( + () => [{ value: 'do_date', label: 'Tanggal Jual' }], + [] + ); + + // ===== FILTER HANDLERS ===== + const handleResetFilters = useCallback(() => { + setIsSubmitted(false); + setFilterCustomer([]); + setFilterSales([]); + setFilterStartDate(''); + setFilterEndDate(''); + setFilterErrors({}); + }, []); + + const handleApplyFilters = useCallback(() => { + const errors: Record = {}; + + if (!filterStartDate) { + errors.start_date = 'Tanggal mulai wajib diisi'; + } + if (!filterEndDate) { + errors.end_date = 'Tanggal akhir wajib diisi'; + } + + setFilterErrors(errors); + + if (Object.keys(errors).length === 0) { + setIsSubmitted(true); + setCurrentPage(1); + filterModal.closeModal(); + } + }, [filterModal, filterStartDate, filterEndDate]); + + // ===== DATA FETCHING ===== + const { data: customerPayment, isLoading } = useSWR( + isSubmitted + ? () => { + const params = { + customer_id: + filterCustomer.length > 0 + ? filterCustomer.map((v) => String(v.value)).join(',') + : undefined, + sales: + filterSales.length > 0 + ? filterSales.map((v) => String(v.value)).join(',') + : undefined, + filter_by: 'do_date' as const, + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + page: currentPage, + limit: pageSize, + }; + + return ['customer-payment-report', params]; + } + : null, + ([, params]) => + FinanceApi.getCustomerPaymentReport( + params.customer_id, + params.sales, + params.filter_by, + params.start_date, + params.end_date, + params.page, + params.limit + ) + ); + + const data: CustomerPaymentReport[] = useMemo( + () => + isResponseSuccess(customerPayment) + ? (customerPayment?.data as unknown as CustomerPaymentReport[]) || [] + : [], + [customerPayment] + ); + + const meta = + isResponseSuccess(customerPayment) && customerPayment?.meta + ? customerPayment.meta + : null; + + // ===== EXPORT DATA FETCHER ===== + const customerPaymentExport = useCallback(async (): Promise< + CustomerPaymentReport[] | null + > => { + const params = { + customer_id: + filterCustomer.length > 0 + ? filterCustomer.map((v) => String(v.value)).join(',') + : undefined, + sales: + filterSales.length > 0 + ? filterSales.map((v) => String(v.value)).join(',') + : undefined, + filter_by: 'do_date' as const, + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + limit: 100, + page: 1, + }; + + const response = await FinanceApi.getCustomerPaymentReport( + params.customer_id, + params.sales, + params.filter_by, + params.start_date, + params.end_date, + params.page, + params.limit + ); + + return isResponseSuccess(response) + ? (response.data as unknown as CustomerPaymentReport[]) + : null; + }, [filterCustomer, filterSales, filterStartDate, filterEndDate]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await customerPaymentExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + generateCustomerPaymentExcel({ data: allDataForExport }); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [customerPaymentExport]); + + const handleExportPdf = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await customerPaymentExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateCustomerPaymentPDF({ data: allDataForExport }); + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [customerPaymentExport]); + + // ===== PAGINATION HANDLERS ===== + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handleRowChange = (pageSize: number) => { + setPageSize(pageSize); + }; + + const handleNextPage = () => { + if (meta && currentPage < meta.total_pages) { + setCurrentPage(currentPage + 1); + } + }; + + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const getTableColumns = ( + summary: CustomerPaymentSummary + ): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
Total
, + }, + { + id: 'do_date_or_payment_date', + header: 'Tanggal DO/Bayar', + accessorKey: 'do_date', + cell: (props) => { + const value = props.row.original.do_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'realization_date', + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: (props) => { + const value = props.row.original.realization_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'aging', + header: 'Aging', + accessorKey: 'aging_day', + cell: (props) => { + const value = props.row.original.aging_day; + return ( +
+ {value ? formatNumber(value) : '-'} hari +
+ ); + }, + }, + { + id: 'reference', + header: 'Referensi', + accessorKey: 'reference', + cell: (props) => { + const value = props.row.original.reference; + return value || '-'; + }, + }, + { + id: 'vehicle_plate', + header: 'Nomor Polisi', + accessorKey: 'vehicle_plate', + cell: (props) => { + const value = props.row.original.vehicle_plate; + return value || '-'; + }, + }, + { + id: 'qty', + header: 'Ekor/Qty', + accessorKey: 'qty', + cell: (props) => { + const value = props.row.original.qty; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summary.total_qty) || '-'} +
+ ), + }, + { + id: 'weight', + header: 'Berat (Kg)', + accessorKey: 'weight', + cell: (props) => { + const value = props.row.original.weight; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summary.total_weight) || '-'} +
+ ), + }, + { + id: 'average_weight', + header: 'AVG', + accessorKey: 'average_weight', + cell: (props) => { + const value = props.row.original.average_weight; + return
{formatNumber(value)}
; + }, + footer: () => ( +
-
+ ), + }, + { + id: 'price', + header: 'Harga Awal', + accessorKey: 'price', + cell: (props) => { + const value = props.row.original.price; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_initial_amount) || '-'} +
+ ), + }, + { + id: 'credit_note', + header: 'CN', + accessorKey: 'credit_note', + cell: (props) => { + const value = props.row.original.credit_note; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_credit_note) || '-'} +
+ ), + }, + { + id: 'final_price', + header: 'Harga Akhir', + accessorKey: 'final_price', + cell: (props) => { + const value = props.row.original.final_price; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_final_amount) || '-'} +
+ ), + }, + { + id: 'ppn', + header: 'PPN (%)', + accessorKey: 'ppn', + cell: (props) => { + const value = props.row.original.ppn; + return
{formatNumber(value)}%
; + }, + footer: () => ( +
-
+ ), + }, + { + id: 'total', + header: 'Total', + accessorKey: 'total', + cell: (props) => { + const value = props.row.original.total; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_grand_amount) || '-'} +
+ ), + }, + { + id: 'payment', + header: 'Pembayaran', + accessorKey: 'payment', + cell: (props) => { + const value = props.row.original.payment; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_payment) || '-'} +
+ ), + }, + { + id: 'accounts_receivable', + header: 'Saldo Piutang', + accessorKey: 'accounts_receivable', + cell: (props) => { + const value = props.row.original.accounts_receivable; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summary.total_accounts_receivable) || '-'} +
+ ), + }, + { + id: 'notes', + header: 'Keterangan', + accessorKey: 'notes', + cell: (props) => { + const value = props.row.original.notes; + return value || '-'; + }, + }, + { + id: 'pickup_info', + header: 'Pengambilan', + accessorKey: 'pickup_info', + cell: (props) => { + const value = props.row.original.pickup_info; + return value || '-'; + }, + }, + { + id: 'sales_marketing', + header: 'Sales/Marketing', + accessorKey: 'sales_marketing', + cell: (props) => { + const value = props.row.original.sales_marketing; + return value || '-'; + }, + }, + ]; + return tableColumns; + }; + + return ( +
+ +
+ + + + + Export + + } + align='end' + > + + + + + +
+ + {/* Filter Modal */} + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+
+ { + setFilterStartDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, start_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.start_date && ( +

+ {filterErrors.start_date} +

+ )} +
+ +
+ { + setFilterEndDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, end_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.end_date && ( +

+ {filterErrors.end_date} +

+ )} +
+
+ +
+ { + setFilterCustomer( + Array.isArray(val) ? val : val ? [val] : [] + ); + }} + isLoading={isLoadingCustomers} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ +
+ { + setFilterSales(Array.isArray(val) ? val : val ? [val] : []); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ +
+ +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+ + {!isSubmitted ? ( +
+ Silakan klik tombol Filter untuk mengatur filter dan menampilkan + data. +
+ ) : isLoading ? ( +
+ +
+ ) : data.length === 0 ? ( +
+ Tidak ada data yang dapat ditampilkan... +
+ ) : ( + data.map((customerReport) => { + const summary = customerReport.summary || { + total_qty: 0, + total_weight: 0, + total_initial_amount: 0, + total_credit_note: 0, + total_final_amount: 0, + total_ppn: 0, + total_grand_amount: 0, + total_payment: 0, + total_accounts_receivable: 0, + }; + + const totalAccountsReceivable = summary.total_accounts_receivable; + const tableColumns = getTableColumns(summary); + + return ( + +
Kandang AktifKandang {!kandangData && 'Aktif'} :{initialValue?.active_house_count} Kandang + {!kandangData + ? `${initialValue?.active_house_count} Kandang` + : kandangData?.kandang?.name} +
Status Pembayaran Penjualan: {formatCurrency( - initialValues?.latest_approval.step_number === 4 || - initialValues?.latest_approval.step_number === 5 + initialValues?.latest_approval.step_number === 5 || + initialValues?.latest_approval.step_number === 6 ? (initialValues?.total_realisasi ?? 0) : (initialValues?.total_pengajuan ?? 0) )} diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index a70b6454..a7fcb3e9 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -39,6 +39,10 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { case 5: expenseStatusPillBadgeColor = 'green'; break; + + case 6: + expenseStatusPillBadgeColor = 'green'; + break; } if (isLatestApprovalRejected) { diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 1f3e9df5..fdfd9cc3 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -55,15 +55,16 @@ const RowOptionsMenu = ({ deleteClickHandler: () => void; }) => { const showEditButton = - props.row.original.latest_approval.step_number !== 5 && + props.row.original.latest_approval.step_number !== 6 && (props.row.original.latest_approval.step_number === 1 || props.row.original.latest_approval.step_number === 2 || - props.row.original.latest_approval.step_number === 3); + props.row.original.latest_approval.step_number === 3 || + props.row.original.latest_approval.step_number === 4); // TODO: apply RBAC const showRealizationButton = props.row.original.latest_approval.action !== 'REJECTED' && - props.row.original.latest_approval.step_number === 3; + props.row.original.latest_approval.step_number === 4; return ( @@ -193,7 +194,7 @@ const ExpensesTable = () => { parseInt(item) ); - const isAllSelectedRowLatestApprovalOnManager = useMemo(() => { + const isAllSelectedRowLatestApprovalOnHeadArea = useMemo(() => { return selectedRowIds.every((rowId) => { if (!isResponseSuccess(expenses)) return false; @@ -202,11 +203,28 @@ const ExpensesTable = () => { const isLatestApprovalRejected = expenseItem?.latest_approval.action === 'REJECTED'; - const isCurrentApprovalOnManager = + const isCurrentApprovalOnHeadArea = !isLatestApprovalRejected && expenseItem?.latest_approval.step_number === 1; - return isCurrentApprovalOnManager; + return isCurrentApprovalOnHeadArea; + }); + }, [expenses, selectedRowIds]); + + const isAllSelectedRowLatestApprovalOnUnitVicePresident = useMemo(() => { + return selectedRowIds.every((rowId) => { + if (!isResponseSuccess(expenses)) return false; + + const expenseItem = expenses.data.find((item) => item.id === rowId); + + const isLatestApprovalRejected = + expenseItem?.latest_approval.action === 'REJECTED'; + + const isCurrentApprovalOnUnitVicePresident = + !isLatestApprovalRejected && + expenseItem?.latest_approval.step_number === 2; + + return isCurrentApprovalOnUnitVicePresident; }); }, [expenses, selectedRowIds]); @@ -221,7 +239,7 @@ const ExpensesTable = () => { const isCurrentApprovalOnFinance = !isLatestApprovalRejected && - expenseItem?.latest_approval.step_number === 2; + expenseItem?.latest_approval.step_number === 3; return isCurrentApprovalOnFinance; }); @@ -238,7 +256,7 @@ const ExpensesTable = () => { const isCurrentApprovalOnRealization = !isLatestApprovalRejected && - expenseItem?.latest_approval.step_number === 4; + expenseItem?.latest_approval.step_number === 5; return isCurrentApprovalOnRealization; }); @@ -397,7 +415,7 @@ const ExpensesTable = () => { ) => { return ( row.original.latest_approval.action !== 'REJECTED' && - row.original.latest_approval.step_number !== 5 + row.original.latest_approval.step_number !== 6 ); }; @@ -441,8 +459,13 @@ const ExpensesTable = () => { let bulkApproveResponse: BaseApiResponse | undefined = undefined; - if (isAllSelectedRowLatestApprovalOnManager) { - bulkApproveResponse = await ExpenseApi.bulkApproveManager( + if (isAllSelectedRowLatestApprovalOnHeadArea) { + bulkApproveResponse = await ExpenseApi.bulkApproveHeadArea( + selectedRowIds, + notes + ); + } else if (isAllSelectedRowLatestApprovalOnUnitVicePresident) { + bulkApproveResponse = await ExpenseApi.bulkApproveUnitVicePresident( selectedRowIds, notes ); @@ -478,8 +501,13 @@ const ExpensesTable = () => { let bulkRejectResponse: BaseApiResponse | undefined = undefined; - if (isAllSelectedRowLatestApprovalOnManager) { - bulkRejectResponse = await ExpenseApi.bulkRejectManager( + if (isAllSelectedRowLatestApprovalOnHeadArea) { + bulkRejectResponse = await ExpenseApi.bulkRejectHeadArea( + selectedRowIds, + notes + ); + } else if (isAllSelectedRowLatestApprovalOnUnitVicePresident) { + bulkRejectResponse = await ExpenseApi.bulkRejectUnitVicePresident( selectedRowIds, notes ); @@ -594,16 +622,31 @@ const ExpensesTable = () => { {selectedRowIds.length > 0 && ( <> - + + + + + @@ -622,7 +665,8 @@ const ExpensesTable = () => { @@ -631,7 +675,8 @@ const ExpensesTable = () => { color='error' onClick={bulkRejectClickHandler} disabled={ - !isAllSelectedRowLatestApprovalOnManager && + !isAllSelectedRowLatestApprovalOnHeadArea && + !isAllSelectedRowLatestApprovalOnUnitVicePresident && !isAllSelectedRowLatestApprovalOnFinance } className='w-full sm:w-fit' diff --git a/src/components/pages/expense/RealizationStatusBadge.tsx b/src/components/pages/expense/RealizationStatusBadge.tsx index e042c022..720c1d03 100644 --- a/src/components/pages/expense/RealizationStatusBadge.tsx +++ b/src/components/pages/expense/RealizationStatusBadge.tsx @@ -9,7 +9,7 @@ interface RealizationStatusBadgeProps { const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => { const isLatestApprovalRejected = approval?.action === 'REJECTED'; - const isExpenseRealized = approval?.step_number && approval.step_number >= 4; + const isExpenseRealized = approval?.step_number && approval.step_number >= 5; const realizationStatus = isExpenseRealized ? 'Sudah Realisasi' diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx index 6526b1c1..ed5aea3e 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -17,6 +17,7 @@ import DropFileInput from '@/components/input/DropFileInput'; import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable'; import ExpenseRealizationKandangDetailExpense from '@/components/pages/expense/form/ExpenseRealizationKandangDetailExpense'; import RequirePermission from '@/components/helper/RequirePermission'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { CreateExpenseRealizationPayload, @@ -35,6 +36,7 @@ import { LocationApi, SupplierApi } from '@/services/api/master-data'; import { Supplier } from '@/types/api/master-data/supplier'; import { ACCEPTED_FILE_TYPE } from '@/config/constant'; import { cn } from '@/lib/helper'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; interface ExpenseRealizationFormProps { type?: 'add' | 'edit' | 'detail'; @@ -132,6 +134,7 @@ const ExpenseRealizationForm = ({ }); const { setValues: formikSetValues } = formik; + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); const { setInputValue: setLocationInputValue, @@ -223,6 +226,13 @@ const ExpenseRealizationForm = ({ const realizationDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; @@ -256,7 +266,7 @@ const ExpenseRealizationForm = ({ @@ -365,6 +375,8 @@ const ExpenseRealizationForm = ({ /> + + {expenseFormErrorMessage && (
Submit diff --git a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts index 71357361..cd34e5a3 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts @@ -7,18 +7,19 @@ type ExpenseFormSchemaType = { category?: { value: 'BOP' | 'NON-BOP'; label: 'BOP' | 'NON-BOP'; - }; + } | null; location?: { value: number; label: string; - }; + } | null; location_id: number; transaction_date?: string; kandangs?: { id?: number; name?: string }[]; supplier?: { value: number; label: string; - }; + } | null; + supplier_id: number; existing_documents?: { id: number; name: string; url: string }[]; deleted_documents?: number[]; documents?: File[]; @@ -28,7 +29,8 @@ type ExpenseFormSchemaType = { nonstock?: { value: number; label: string; - }; + } | null; + nonstock_id?: number; quantity?: number; price?: number; notes?: string; @@ -41,16 +43,24 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = category: Yup.object({ value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(), label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(), - }).required('Kategori wajib diisi!'), + }) + .nullable() + .optional(), location: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), - }).required('Lokasi wajib diisi!'), + }) + .nullable() + .optional(), - location_id: Yup.number().min(1).required('Lokasi wajib diisi!'), + location_id: Yup.number() + .required('Lokasi wajib diisi!') + .min(1, 'Lokasi wajib diisi!') + .typeError('Lokasi wajib diisi!'), transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'), + kandangs: Yup.array() .of( Yup.object({ @@ -63,15 +73,24 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = supplier: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), - }).required('Vendor wajib diisi!'), + }) + .nullable() + .optional(), - existing_documents: Yup.array().of( - Yup.object({ - id: Yup.number().required(), - name: Yup.string().required(), - url: Yup.string().required(), - }) - ), + supplier_id: Yup.number() + .required('Vendor wajib diisi!') + .min(1, 'Vendor wajib diisi!') + .typeError('Vendor wajib diisi!'), + + existing_documents: Yup.array() + .of( + Yup.object({ + id: Yup.number().required(), + name: Yup.string().required(), + url: Yup.string().required(), + }) + ) + .optional(), deleted_documents: Yup.array().of(Yup.number().required()).optional(), @@ -87,9 +106,17 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = nonstock: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), - }).required('Nonstock wajib diisi!'), - quantity: Yup.number().required('Total kuantitas wajib diisi!'), - price: Yup.number().required('Harga satuan wajib diisi!'), + }).nullable(), + nonstock_id: Yup.number() + .required('Nonstock wajib diisi!') + .min(1, 'Nonstock wajib diisi!') + .typeError('Nonstock wajib diisi!'), + quantity: Yup.number() + .required('Total kuantitas wajib diisi!') + .typeError('Total kuantitas wajib diisi!'), + price: Yup.number() + .required('Harga satuan wajib diisi!') + .typeError('Harga satuan wajib diisi!'), notes: Yup.string(), }) ) @@ -104,7 +131,16 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = export const UpdateExpenseRequestFormSchema = ExpenseRequestFormSchema; export const UploadRequestDocumentsFormSchema = Yup.object({ - documents: Yup.array().of(Yup.mixed().required()).required(), + documents: Yup.array() + .of( + Yup.mixed() + .required() + .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => { + if (!value || !(value instanceof File)) return true; + return value.size <= 5 * 1024 * 1024; + }) + ) + .required(), }); export type ExpenseRequestFormValues = Yup.InferType< @@ -124,13 +160,13 @@ export const getExpenseFormInitialValues = ( value: initialValues.category, label: initialValues.category, } - : undefined, + : null, location: initialValues?.location ? { value: initialValues.location.id, label: initialValues.location.name, } - : undefined, + : null, location_id: Number(initialValues?.location.id || 0), transaction_date: initialValues?.transaction_date ? formatDate(initialValues.transaction_date, 'YYYY-MM-DD') @@ -144,7 +180,8 @@ export const getExpenseFormInitialValues = ( value: initialValues.supplier.id, label: initialValues.supplier.name, } - : undefined, + : null, + supplier_id: initialValues?.supplier?.id ?? 0, existing_documents: initialValues?.documents?.map((doc) => { const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path; return { @@ -164,12 +201,25 @@ export const getExpenseFormInitialValues = ( value: expenseItem.nonstock.id, label: expenseItem.nonstock.name, }, + nonstock_id: expenseItem.nonstock.id, quantity: expenseItem.qty, price: expenseItem.price, notes: expenseItem.note, })) : [], })) - : [], + : [ + { + cost_items: [ + { + nonstock: null, + nonstock_id: 0, + quantity: undefined, + price: undefined, + notes: '', + }, + ], + }, + ], }; }; diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index 60e55397..a41290b8 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -37,6 +37,8 @@ import { cn, sleep } from '@/lib/helper'; import { LocationApi, SupplierApi } from '@/services/api/master-data'; import { ACCEPTED_FILE_TYPE } from '@/config/constant'; import { Supplier } from '@/types/api/master-data/supplier'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface ExpenseFormProps { type?: 'add' | 'edit' | 'detail'; @@ -55,6 +57,7 @@ const ExpenseRequestForm = ({ const rejectModal = useModal(); const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const createExpenseHandler = useCallback( async (payload: CreateExpensePayload) => { @@ -201,7 +204,8 @@ const ExpenseRequestForm = ({ { cost_items: [ { - nonstock: undefined, + nonstock: null, + nonstock_id: 0, quantity: undefined, price: undefined, notes: '', @@ -223,7 +227,8 @@ const ExpenseRequestForm = ({ { cost_items: [ { - nonstock: undefined, + nonstock: null, + nonstock_id: 0, quantity: undefined, price: undefined, notes: '', @@ -248,7 +253,8 @@ const ExpenseRequestForm = ({ kandang_id: kandangItem.id, cost_items: existingExpenseNonstock?.cost_items || [ { - nonstock: undefined, + nonstock: null, + nonstock_id: 0, quantity: undefined, price: undefined, notes: '', @@ -263,10 +269,20 @@ const ExpenseRequestForm = ({ const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldTouched('supplier', true); formik.setFieldValue('supplier', val); + + const supplierId = Array.isArray(val) ? val[0]?.value : val?.value; + formik.setFieldValue('supplier_id', supplierId ?? 0); }; const requestDocumentsChangeHandler = (val: File[]) => { formik.setFieldTouched('documents', true); + + const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024); + if (invalidFiles.length > 0) { + toast.error('Ukuran dokumen maksimal 5 MB!'); + return; + } + formik.setFieldValue('documents', val); }; @@ -322,6 +338,22 @@ const ExpenseRequestForm = ({ router.push('/expense'); }; + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + useEffect(() => { formikSetValues(getExpenseFormInitialValues(initialValues)); }, [formikSetValues, getExpenseFormInitialValues, initialValues]); @@ -347,10 +379,27 @@ const ExpenseRequestForm = ({ + {expenseFormErrorMessage && ( +
+ + {expenseFormErrorMessage} +
+ )} + + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )}
)} - {expenseFormErrorMessage && ( -
- - {expenseFormErrorMessage} -
- )} - {type !== 'detail' && (
Submit diff --git a/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx b/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx index e219870e..41eb40f8 100644 --- a/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx +++ b/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx @@ -25,7 +25,7 @@ interface ExpenseRequestKandangDetailExpenseProps { location?: { value: number; label: string; - }; + } | null; className?: { wrapper?: string; }; @@ -59,13 +59,20 @@ const ExpenseRequestKandangDetailExpense: React.FC< `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, val ); + + const nonstockId = Array.isArray(val) ? val[0]?.value : val?.value; + formik.setFieldValue( + `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`, + nonstockId ?? 0 + ); }; const addExpenseItemHandler = (kandangExpenseIdx: number) => { const newExpensesValue = [ ...formik.values.expense_nonstocks[kandangExpenseIdx].cost_items, { - nonstock: undefined, + nonstock: null, + nonstock_id: 0, price: undefined, quantity: undefined, notes: '', diff --git a/src/components/pages/expense/pdf/ExpensePDF.tsx b/src/components/pages/expense/pdf/ExpensePDF.tsx index ef1c7d8b..f82f6639 100644 --- a/src/components/pages/expense/pdf/ExpensePDF.tsx +++ b/src/components/pages/expense/pdf/ExpensePDF.tsx @@ -198,7 +198,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { expense?.latest_approval?.action === 'REJECTED'; const isExpenseRealized = expense?.latest_approval?.step_number && - expense?.latest_approval.step_number >= 4; + expense?.latest_approval.step_number >= 5; const realizationStatus = isExpenseRealized ? 'Sudah Realisasi' @@ -242,8 +242,8 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { { label: 'Nominal Biaya', value: formatCurrency( - expense?.latest_approval.step_number === 4 || - expense?.latest_approval.step_number === 5 + expense?.latest_approval.step_number === 5 || + expense?.latest_approval.step_number === 6 ? (expense?.total_realisasi ?? 0) : (expense?.total_pengajuan ?? 0) ), diff --git a/src/components/pages/finance/add/FormFinanceAdd.tsx b/src/components/pages/finance/add/FormFinanceAdd.tsx index c835740e..a94fabd9 100644 --- a/src/components/pages/finance/add/FormFinanceAdd.tsx +++ b/src/components/pages/finance/add/FormFinanceAdd.tsx @@ -1,7 +1,7 @@ 'use client'; import Button from '@/components/Button'; -import Card from '@/components/Card'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { FormHeader } from '@/components/helper/form/FormHeader'; import DateInput from '@/components/input/DateInput'; import NumberInput from '@/components/input/NumberInput'; @@ -21,6 +21,7 @@ import { } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatTitleCase } from '@/lib/helper'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { FinanceApi } from '@/services/api/finance'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { @@ -104,6 +105,9 @@ const FormFinanceAdd = ({ }, }); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + // ===== Options ===== const { options: partyOptions, @@ -180,7 +184,7 @@ const FormFinanceAdd = ({ title={`${type === 'add' ? 'Tambah' : 'Ubah'} Data Keuangan`} backUrl='/finance' /> - + +
+
{type !== 'detail' && (
@@ -405,11 +411,7 @@ const InventoryAdjustmentForm = ({ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={ - !formik.isValid || - formik.isSubmitting || - formik.values.product == undefined - } + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index 51c20d8e..2fbca835 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -48,8 +48,8 @@ import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import RequirePermission from '@/components/helper/RequirePermission'; -import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); @@ -219,7 +219,6 @@ const MarketingForm = ({ const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>( 'add' ); - const [formErrorList, setFormErrorList] = useState([]); const [deliveryOrderValues, setDeliveryOrderValues] = useState< DeliveryOrderProductFormValues[] >( @@ -561,22 +560,8 @@ const MarketingForm = ({ ); }, [memoSalesOrder]); - const handleValidateForm = async () => { - const errors = await formik.validateForm(); - - if (Object.keys(errors).length > 0) { - // Parse and display errors - const errorMessages = getUniqueFormikErrors(errors); - setFormErrorList(errorMessages); - return; // Stop submission - } - }; - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleValidateForm(); - formik.handleSubmit(); - }; + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); return ( <> @@ -686,13 +671,7 @@ const MarketingForm = ({
- {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} + {/* Form Actions */}
diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index 84f9e443..25a20982 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -16,8 +16,8 @@ import Badge from '@/components/Badge'; import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm'; import * as Yup from 'yup'; import { isResponseSuccess } from '@/lib/api-helper'; -import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; const DeliveryOrderProductForm = ({ formState, @@ -42,7 +42,6 @@ const DeliveryOrderProductForm = ({ null ); const [currentInput, setCurrentInput] = useState(''); - const [formErrorList, setFormErrorList] = useState([]); const salesOrder = salesOrders.find( (item) => item.id === initialValues?.marketing_product_id @@ -168,21 +167,8 @@ const DeliveryOrderProductForm = ({ } }, [initialValues]); - const handleValidateForm = () => { - formik.validateForm(); - const formErrorList = getUniqueFormikErrors(formik.errors); - setFormErrorList(formErrorList); - if (formErrorList.length > 0) { - return; - } - }; - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleBlurField(currentInput); - handleValidateForm(); - formik.handleSubmit(e); - }; + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); return ( <> @@ -388,12 +374,7 @@ const DeliveryOrderProductForm = ({ />
- {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} +
- {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} +
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/bank/form/BankForm.tsx b/src/components/pages/master-data/bank/form/BankForm.tsx index ac5cc531..13c85422 100644 --- a/src/components/pages/master-data/bank/form/BankForm.tsx +++ b/src/components/pages/master-data/bank/form/BankForm.tsx @@ -25,6 +25,8 @@ import { } from '@/types/api/master-data/bank'; import { BankApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; interface BankFormProps { type?: 'add' | 'edit' | 'detail'; @@ -124,6 +126,9 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -145,7 +150,7 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => { @@ -247,6 +252,8 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => {
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/customer/form/CustomerForm.tsx b/src/components/pages/master-data/customer/form/CustomerForm.tsx index fd3cea6f..0a629b36 100644 --- a/src/components/pages/master-data/customer/form/CustomerForm.tsx +++ b/src/components/pages/master-data/customer/form/CustomerForm.tsx @@ -28,6 +28,8 @@ import useSWR from 'swr'; import { UserApi } from '@/services/api/user'; import { TYPE_OPTIONS } from '@/config/constant'; import RequirePermission from '@/components/helper/RequirePermission'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface CustomerFormProps { formType?: 'add' | 'edit' | 'detail'; @@ -191,6 +193,9 @@ const CustomerForm = ({ formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + // Render return ( <> @@ -213,7 +218,7 @@ const CustomerForm = ({ @@ -358,6 +363,8 @@ const CustomerForm = ({
)} + + {formType !== 'detail' && (
Submit diff --git a/src/components/pages/master-data/fcr/form/FcrForm.tsx b/src/components/pages/master-data/fcr/form/FcrForm.tsx index f30ec7e9..807e7e45 100644 --- a/src/components/pages/master-data/fcr/form/FcrForm.tsx +++ b/src/components/pages/master-data/fcr/form/FcrForm.tsx @@ -26,6 +26,8 @@ import { } from '@/types/api/master-data/fcr'; import { FcrApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; interface FcrFormProps { type?: 'add' | 'edit' | 'detail'; @@ -158,6 +160,9 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -179,7 +184,7 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { @@ -294,6 +299,8 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { )}
+ +
{type !== 'add' && (
@@ -349,7 +356,7 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/flock/form/FlockForm.tsx b/src/components/pages/master-data/flock/form/FlockForm.tsx index 5db61656..51ed4325 100644 --- a/src/components/pages/master-data/flock/form/FlockForm.tsx +++ b/src/components/pages/master-data/flock/form/FlockForm.tsx @@ -17,6 +17,8 @@ import TextInput from '@/components/input/TextInput'; import { cn } from '@/lib/helper'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; +import AlertErrorList from '@/components/helper/form/FormErrors'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; interface FlockCustomProps { formType?: 'add' | 'edit' | 'detail'; @@ -86,6 +88,9 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => { formikSetValues(formikInitialValue); }, [formikSetValues, formikInitialValue]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + // Render return ( <> @@ -107,7 +112,7 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => { @@ -168,6 +173,8 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
)} + + {formType !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/kandang/form/KandangForm.tsx b/src/components/pages/master-data/kandang/form/KandangForm.tsx index 81911ab0..ffea5718 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.tsx +++ b/src/components/pages/master-data/kandang/form/KandangForm.tsx @@ -29,6 +29,8 @@ import { LocationApi, KandangApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; import { UserApi } from '@/services/api/user'; import NumberInput from '@/components/input/NumberInput'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface KandangFormProps { type?: 'add' | 'edit' | 'detail'; @@ -198,6 +200,9 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -219,7 +224,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { @@ -324,6 +329,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/location/form/LocationForm.tsx b/src/components/pages/master-data/location/form/LocationForm.tsx index 68a10527..9f77cf86 100644 --- a/src/components/pages/master-data/location/form/LocationForm.tsx +++ b/src/components/pages/master-data/location/form/LocationForm.tsx @@ -27,6 +27,8 @@ import { } from '@/types/api/master-data/location'; import { AreaApi, LocationApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface LocationFormProps { type?: 'add' | 'edit' | 'detail'; @@ -160,6 +162,9 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -181,7 +186,7 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => { @@ -268,6 +273,8 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index af72f22f..7d8b8784 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -29,6 +29,8 @@ import { NonstockApi, SupplierApi, UomApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; import { flags } from '@/types/api/api-general'; import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface NonstockFormProps { type?: 'add' | 'edit' | 'detail'; @@ -213,6 +215,9 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -234,7 +239,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { @@ -337,6 +342,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx index d241a3dd..1e61879c 100644 --- a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx @@ -11,7 +11,6 @@ import TextInput from '@/components/input/TextInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; -import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { @@ -27,6 +26,7 @@ import { } from '@/types/api/master-data/product-category'; import { ProductCategoryApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; interface ProductCategoryFormProps { type?: 'add' | 'edit' | 'detail'; @@ -41,7 +41,6 @@ const ProductCategoryForm = ({ const deleteModal = useModal(); const [formErrorMessage, setFormErrorMessage] = useState(''); - const [formErrorList, setFormErrorList] = useState([]); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const createProductCategoryHandler = useCallback( @@ -132,21 +131,8 @@ const ProductCategoryForm = ({ formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); - const handleValidateForm = async () => { - const errors = await formik.validateForm(); - - if (Object.keys(errors).length > 0) { - const errorMessages = getUniqueFormikErrors(errors); - setFormErrorList(errorMessages); - return; - } - }; - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleValidateForm(); - formik.handleSubmit(e); - }; + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); return ( <> @@ -184,13 +170,7 @@ const ProductCategoryForm = ({
)} - {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} +
{ const deleteModal = useModal(); const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); - const [formErrorList, setFormErrorList] = useState([]); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const createProductHandler = useCallback( @@ -204,21 +204,8 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); - const handleValidateForm = async () => { - const errors = await formik.validateForm(); - - if (Object.keys(errors).length > 0) { - const errorMessages = getUniqueFormikErrors(errors); - setFormErrorList(errorMessages); - return; - } - }; - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleValidateForm(); - formik.handleSubmit(e); - }; + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); return ( <> @@ -254,13 +241,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
)} - {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} +
{ switch (formType) { case 'add': @@ -723,7 +727,8 @@ const ProductionStandardForm = ({ router.push('/master-data/production-standard'); }; - // ===== Function ===== + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); return ( <> @@ -1210,9 +1215,26 @@ const ProductionStandardForm = ({ return null; }} /> + + + + {productionStandardFormErrorMessage && ( + +
+ + {productionStandardFormErrorMessage} +
+ setProductionStandardFormErrorMessage('')} + className='ms-auto' + /> +
+ )} + {formType === 'detail' && (
@@ -1293,19 +1315,6 @@ const ProductionStandardForm = ({
)} - {productionStandardFormErrorMessage && ( - -
- - {productionStandardFormErrorMessage} -
- setProductionStandardFormErrorMessage('')} - className='ms-auto' - /> -
- )}
@@ -221,7 +226,7 @@ const SupplierForm = ({
@@ -444,6 +449,8 @@ const SupplierForm = ({
)} + + {formType !== 'detail' && (
Submit diff --git a/src/components/pages/master-data/uom/form/UomForm.tsx b/src/components/pages/master-data/uom/form/UomForm.tsx index 50576eef..366f9682 100644 --- a/src/components/pages/master-data/uom/form/UomForm.tsx +++ b/src/components/pages/master-data/uom/form/UomForm.tsx @@ -25,6 +25,8 @@ import { } from '@/types/api/master-data/uom'; import { UomApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface UomFormProps { type?: 'add' | 'edit' | 'detail'; @@ -118,6 +120,9 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -139,7 +144,7 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => { @@ -199,6 +204,8 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => {
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx index 227af3c6..0fb55a2a 100644 --- a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx +++ b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx @@ -33,6 +33,8 @@ import { } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface WarehouseFormProps { type?: 'add' | 'edit' | 'detail'; @@ -323,6 +325,9 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { formikSetValues(formikInitialValues); }, [formikSetValues, formikInitialValues]); + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + return ( <>
@@ -344,7 +349,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { @@ -474,6 +479,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
)} + + {type !== 'detail' && (
{ type='submit' color='primary' isLoading={formik.isSubmitting} - disabled={!formik.isValid || formik.isSubmitting} + disabled={formik.isSubmitting} className='px-4' > Submit diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index f6888c3d..5ca0e789 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -19,7 +19,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Kandang } from '@/types/api/master-data/kandang'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { Icon } from '@iconify/react'; -import { CellContext, SortingState } from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; @@ -27,84 +27,6 @@ import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; -const RowOptionsMenu = ({ - type = 'dropdown', - props, - deleteClickHandler, -}: { - type: 'dropdown' | 'collapse'; - props: CellContext; - deleteClickHandler: () => void; -}) => { - return ( -
-
- - - - {props.row.original.approval.step_name === 'Aktif' && ( - - - - )} - {props.row.original.approval.step_name === 'Pengajuan' && ( - - - - )} - - - -
-
- ); -}; - const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const { state: tableFilterState, @@ -149,8 +71,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); const [periodInputValue, setPeriodInputValue] = useState(null); const [sorting, setSorting] = useState([]); - const [selectedProjectFlock, setSelectedProjectFlock] = - useState(); const deleteModal = useModal(); const confirmModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( @@ -221,10 +141,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { : []; // ====== HANDLER ====== - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -292,12 +208,146 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const canApprove = useMemo(() => { if (!selectedSingleRow || isApproveLoading) return false; - const isPengajuan = selectedSingleRow.approval.step_number == 1; - const isNotRejected = selectedSingleRow.approval.action != 'REJECTED'; + const isPengajuan = selectedSingleRow.approval?.step_number == 1; + const isNotRejected = selectedSingleRow.approval?.action != 'REJECTED'; return isPengajuan && isNotRejected; }, [selectedSingleRow, isApproveLoading]); + // ====== COLUMNS ====== + const columns = useMemo[]>( + () => [ + { + id: 'select', + header: ({ table }) => { + const allRows = table.getRowModel().rows; + const selectableRows = allRows; + + const allSelected = + selectableRows.every((row) => row.getIsSelected()) && + selectableRows.length != 0; + + const someSelected = + selectableRows.some((row) => row.getIsSelected()) && !allSelected; + + const toggleSelectableRows = () => { + const shouldSelect = !allSelected; + selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); + }; + + return ( +
+ +
+ ); + }, + cell: ({ row }) => { + return ( + + ); + }, + }, + + { + accessorKey: 'flock_name', + header: 'Flock', + }, + { + accessorKey: 'area.name', + header: 'Area', + }, + { + accessorKey: 'location.name', + header: 'Lokasi', + }, + { + accessorKey: 'fcr.name', + header: 'FCR', + }, + { + accessorKey: 'category', + header: 'Kategori', + }, + { + accessorKey: 'approval.step_name', + header: 'Status', + cell: (props) => { + const approval = props.row.original.approval; + + return ( + + + {approval?.step_name} + + ); + }, + }, + { + header: 'Kandang', + cell: (props) => { + const kandang = props.row.original.kandangs; + if (kandang) { + const kandangNames = kandang.map((k: Kandang) => k.name); + return ( +
+ {kandangNames.length > 0 + ? kandangNames.join(', ') + : 'Tidak ada'} +
+ ); + } else { + return '-'; + } + }, + }, + { + accessorKey: 'period', + header: 'Periode', + }, + { + accessorKey: 'created_at', + header: 'Dibuat pada', + cell: (props) => + formatDate(props.row.original.created_at, 'MMM DD, YYYY'), + }, + ], + [] + ); + return ( <>
@@ -320,7 +370,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
void }) => { type='number' label='Periode' placeholder='Masukan periode' - value={periodInputValue ?? ''} + value={periodInputValue?.toString() ?? ''} onChange={(e) => { setPeriodInputValue(parseInt(e.target.value)); updateFilter('periodFilter', e.target.value); }} /> -
data={isResponseSuccess(projectFlocks) ? projectFlocks?.data : []} - columns={[ - { - id: 'select', - header: ({ table }) => { - const allRows = table.getRowModel().rows; - const selectableRows = allRows; - - const allSelected = - selectableRows.every((row) => row.getIsSelected()) && - selectableRows.length != 0; - - const someSelected = - selectableRows.some((row) => row.getIsSelected()) && - !allSelected; - - const toggleSelectableRows = () => { - const shouldSelect = !allSelected; - selectableRows.forEach((row) => - row.toggleSelected(shouldSelect) - ); - }; - - return ( -
- -
- ); - }, - cell: ({ row }) => { - return ( - - ); - }, - }, - - { - accessorKey: 'flock_name', - header: 'Flock', - }, - { - accessorKey: 'area.name', - header: 'Area', - }, - { - accessorKey: 'location.name', - header: 'Lokasi', - }, - { - accessorKey: 'fcr.name', - header: 'FCR', - }, - { - accessorKey: 'category', - header: 'Kategori', - }, - { - accessorKey: 'approval.step_name', - header: 'Status', - cell: (props) => { - const approval = props.row.original.approval; - - return ( - - - {approval.step_name} - - ); - }, - }, - { - header: 'Kandang', - cell: (props) => { - const kandang = props.row.original.kandangs; - if (kandang) { - const kandangNames = kandang.map((k: Kandang) => k.name); - return ( -
- {kandangNames.length > 0 - ? kandangNames.join(', ') - : 'Tidak ada'} -
- ); - } else { - return '-'; - } - }, - }, - { - accessorKey: 'period', - header: 'Periode', - }, - { - accessorKey: 'created_at', - header: 'Dibuat pada', - cell: (props) => - formatDate(props.row.original.created_at, 'MMM DD, YYYY'), - }, - ]} + columns={columns} pageSize={tableFilterState.pageSize} page={ isResponseSuccess(projectFlocks) ? projectFlocks?.meta?.page : 0 @@ -545,7 +453,12 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ? projectFlocks?.meta?.total_results : 0 } - onPageChange={setPage} + onPageChange={(page) => { + setPage(page); + }} + onPageSizeChange={(pageSize) => { + setPageSize(pageSize); + }} isLoading={isLoading} sorting={sorting} setSorting={setSorting} @@ -553,9 +466,9 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setRowSelection={setRowSelection} className={{ containerClassName: cn({ - 'mb-20': + 'mb-40': isResponseSuccess(projectFlocks) && - projectFlocks?.data?.length === 0, + projectFlocks?.data?.length > 0, }), tableWrapperClassName: 'overflow-x-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!', diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 9835c244..4a998c83 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -209,20 +209,6 @@ const ProjectFlockDetail = ({
- {/*
- History -
-
- -
*/} - {/* BARIS 1 */}
{projectFlock?.fcr?.name}
+
+ {' '} + Standard +
+
+ {projectFlock?.production_standard?.name ?? '-'} +
+ {/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
{' '} diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 7e90c94b..745a6b1e 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -6,7 +6,6 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { getUniqueFormikErrors } from '@/lib/formik-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { AreaApi, @@ -47,6 +46,7 @@ import { Nonstock } from '@/types/api/master-data/nonstock'; import { useUiStore } from '@/stores/ui/ui.store'; import RequirePermission from '@/components/helper/RequirePermission'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; interface ProjectFlockFormProps { formType?: 'add' | 'edit' | 'detail'; @@ -66,7 +66,6 @@ const ProjectFlockForm = ({ const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] = useState(''); - const [formErrorList, setFormErrorList] = useState([]); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); @@ -642,16 +641,8 @@ const ProjectFlockForm = ({ return !isNonstockAlreadyInBudgets; }); - const handleValidateForm = async () => { - const errors = await formik.validateForm(); - - if (Object.keys(errors).length > 0) { - // Parse and display errors - const errorMessages = getUniqueFormikErrors(errors); - setFormErrorList(errorMessages); - return; // Stop submission - } - }; + // ===== Formik Error List ===== + const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); return ( <> @@ -712,11 +703,7 @@ const ProjectFlockForm = ({ { - e.preventDefault(); - handleValidateForm(); - formik.handleSubmit(e); - }} + onSubmit={handleFormSubmit} onReset={formik.handleReset} > {/* Form Informasi Umum */} @@ -1082,13 +1069,7 @@ const ProjectFlockForm = ({
- {/* Error List Alert */} - {formErrorList.length > 0 && ( - setFormErrorList([])} - /> - )} +
{formType !== 'detail' && ( diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4a9d6c13..4966172c 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1737,16 +1737,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Egg Mass - {initialValues.egg_mesh && - initialValues.egg_mesh > 0 - ? formatNumber(initialValues.egg_mesh) + {initialValues.egg_mass && + initialValues.egg_mass > 0 + ? formatNumber(initialValues.egg_mass) : '-'} - {initialValues.egg_mesh_std && - initialValues.egg_mesh_std > 0 - ? formatNumber(initialValues.egg_mesh_std) + {initialValues.egg_mass_std && + initialValues.egg_mass_std > 0 + ? formatNumber(initialValues.egg_mass_std) : '-'}
Hen Day - {initialValues.hand_day && - initialValues.hand_day > 0 - ? formatNumber(initialValues.hand_day) + {initialValues.hen_day && + initialValues.hen_day > 0 + ? formatNumber(initialValues.hen_day) : '-'} - {initialValues.hand_day_std !== undefined && - initialValues.hand_day_std > 0 - ? `${initialValues.hand_day_std}%` + {initialValues.hen_day_std !== undefined && + initialValues.hen_day_std > 0 + ? `${initialValues.hen_day_std}%` : '-'}
Hen House - {initialValues.hand_house && - initialValues.hand_house > 0 - ? formatNumber(initialValues.hand_house) + {initialValues.hen_house && + initialValues.hen_house > 0 + ? formatNumber(initialValues.hen_house) : '-'} - {initialValues.hand_house_std !== undefined && - initialValues.hand_house_std > 0 - ? `${initialValues.hand_house_std}%` + {initialValues.hen_house_std !== undefined && + initialValues.hen_house_std > 0 + ? `${initialValues.hen_house_std}%` : '-'}
0} + className={{ + containerClassName: 'w-full', + tableWrapperClassName: 'overflow-x-auto mt-4', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + 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', + paginationClassName: 'hidden', + }} + /> + + ); + }) + )} + + {meta && data.length > 0 && ( +
+ +
+ )} + + ); +}; + +export default CustomerPaymentTab; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx new file mode 100644 index 00000000..5a72ea3c --- /dev/null +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -0,0 +1,634 @@ +import Button from '@/components/Button'; +import Card from '@/components/Card'; +import Dropdown from '@/components/Dropdown'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import Modal, { useModal } from '@/components/Modal'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { SupplierApi } from '@/services/api/master-data'; +import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; +import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX'; +import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; +import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; +import { useCallback, useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; +import useSWR from 'swr'; +import Pagination from '@/components/Pagination'; +import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; + +const DebtSupplierTab = () => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== PAGINATION STATE ===== + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== FILTER STATE ===== + const [filterSupplier, setFilterSupplier] = useState([]); + const [filterStartDate, setFilterStartDate] = useState(''); + const [filterEndDate, setFilterEndDate] = useState(''); + const [filterDateType, setFilterDateType] = useState(); + const [filterErrors, setFilterErrors] = useState>({}); + + const filterModal = useModal(); + + const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = + useSelect(SupplierApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + + const dataTypeOptions = useMemo( + () => [ + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'po_date', label: 'Tanggal PO' }, + ], + [] + ); + + // ===== FILTER HANDLERS ===== + const handleResetFilters = useCallback(() => { + setIsSubmitted(false); + setFilterSupplier([]); + setFilterStartDate(''); + setFilterEndDate(''); + setFilterErrors({}); + }, []); + + const handleApplyFilters = useCallback(() => { + const errors: Record = {}; + + if (!filterStartDate) { + errors.start_date = 'Tanggal mulai wajib diisi'; + } + if (!filterEndDate) { + errors.end_date = 'Tanggal akhir wajib diisi'; + } + + setFilterErrors(errors); + + if (Object.keys(errors).length === 0) { + setIsSubmitted(true); + setCurrentPage(1); + filterModal.closeModal(); + } + }, [filterModal, filterStartDate, filterEndDate]); + + // ===== DATA FETCHING ===== + const { data: debtSupplier, isLoading } = useSWR( + isSubmitted + ? () => { + const params = { + supplier_ids: + filterSupplier.length > 0 + ? filterSupplier.map((v) => String(v.value)).join(',') + : undefined, + filter_by: filterDateType?.value, + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + page: currentPage, + limit: pageSize, + }; + + return ['debt-supplier-report', params]; + } + : null, + ([, params]) => + DebtSupplierApi.getDebtSupplierReport( + params.supplier_ids, + params.filter_by?.toString(), + params.start_date, + params.end_date, + params.page, + params.limit + ) + ); + + const data: DebtSupplier[] = useMemo( + () => + isResponseSuccess(debtSupplier) + ? (debtSupplier?.data as unknown as DebtSupplier[]) || [] + : [], + [debtSupplier] + ); + const meta = + isResponseSuccess(debtSupplier) && debtSupplier?.meta + ? debtSupplier.meta + : null; + + // ===== EXPORT DATA FETCHER ===== + const debtSupplierExport = useCallback(async (): Promise< + DebtSupplier[] | null + > => { + const params = { + supplier_ids: + filterSupplier.length > 0 + ? filterSupplier.map((v) => String(v.value)).join(',') + : undefined, + filter_by: filterDateType?.value?.toString(), + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + date_type: filterDateType ? filterDateType.value : undefined, + limit: 100, + page: 1, + }; + + const response = await DebtSupplierApi.getDebtSupplierReport( + params.supplier_ids, + params.filter_by, + params.start_date, + params.end_date, + params.page, + params.limit + ); + + return isResponseSuccess(response) + ? (response.data as unknown as DebtSupplier[]) + : null; + }, [filterSupplier, filterStartDate, filterEndDate]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await debtSupplierExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + generateDebtSupplierExcel({ data: allDataForExport }); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [debtSupplierExport]); + + const handleExportPdf = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await debtSupplierExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateDebtSupplierPDF({ data: allDataForExport }); + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [debtSupplierExport]); + + // ===== PAGINATION HANDLERS ===== + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handleRowChange = (pageSize: number) => { + setPageSize(pageSize); + }; + + const handleNextPage = () => { + if (meta && currentPage < meta.total_pages) { + setCurrentPage(currentPage + 1); + } + }; + + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const getTableColumns = (supplier: DebtSupplier): ColumnDef[] => [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + id: 'pr_number', + header: 'Nomor PR', + accessorKey: 'pr_number', + cell: (props) => { + const value = props.row.original.pr_number; + return value || '-'; + }, + }, + { + id: 'po_number', + header: 'Nomor PO', + accessorKey: 'po_number', + cell: (props) => { + const value = props.row.original.po_number; + return value || '-'; + }, + }, + { + id: 'received_date', + header: 'Tanggal Terima', + accessorKey: 'received_date', + cell: (props) => { + const value = props.row.original.received_date; + return value + ? value != '-' + ? formatDate(value, 'DD MMM YYYY') + : '-' + : '-'; + }, + }, + { + id: 'po_date', + header: 'Tanggal PO', + accessorKey: 'po_date', + cell: (props) => { + const value = props.row.original.po_date; + return value + ? value != '-' + ? formatDate(value, 'DD MMM YYYY') + : '-' + : '-'; + }, + }, + { + id: 'aging', + header: 'Aging', + accessorKey: 'aging', + cell: (props) => { + const value = props.row.original.aging; + return
{formatNumber(value)} Hari
; + }, + footer: () => { + const value = supplier.total.aging; + return
{formatNumber(value)} Hari
; + }, + }, + { + id: 'area', + header: 'Area', + accessorKey: 'area', + cell: (props) => { + const value = props.row.original.area?.name; + return value || '-'; + }, + }, + { + id: 'warehouse', + header: 'Gudang', + accessorKey: 'warehouse', + cell: (props) => { + const value = props.row.original.warehouse?.name; + return value || '-'; + }, + }, + { + id: 'due_date', + header: 'Tanggal Jatuh Tempo', + accessorKey: 'due_date', + cell: (props) => { + const value = props.row.original.due_date; + return value + ? value != '-' + ? formatDate(value, 'DD MMM YYYY') + : '-' + : '-'; + }, + }, + { + id: 'due_status', + header: 'Status Jatuh Tempo', + accessorKey: 'due_status', + cell: (props) => { + const value = props.row.original.due_status; + return value || '-'; + }, + }, + { + id: 'total_price', + header: 'Total Harga', + accessorKey: 'total_price', + cell: (props) => { + const value = props.row.original.total_price; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + footer: () => { + const value = supplier.total.total_price; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'payment_price', + header: 'Harga Pembayaran', + accessorKey: 'payment_price', + cell: (props) => { + const value = props.row.original.payment_price; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + footer: () => { + const value = supplier.total.payment_price; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'debt_price', + header: 'Harga Hutang', + accessorKey: 'debt_price', + cell: (props) => { + const value = props.row.original.debt_price; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + footer: () => { + const value = supplier.total.debt_price; + return ( +
+ {formatCurrency(value)} +
+ ); + }, + }, + { + id: 'status', + header: 'Status', + accessorKey: 'status', + cell: (props) => { + const value = props.row.original.status; + return value || '-'; + }, + }, + { + id: 'travel_number', + header: 'Nomor Perjalanan', + accessorKey: 'travel_number', + cell: (props) => { + const value = props.row.original.travel_number; + return value || '-'; + }, + }, + ]; + return ( + <> +
+ +
+ + + + + Export + + } + align='end' + > + + + + + +
+
+ + {!isSubmitted ? ( +
+ Silakan klik tombol Filter untuk mengatur filter dan menampilkan + data. +
+ ) : isLoading ? ( +
+ +
+ ) : data.length === 0 ? ( +
+ Tidak ada data yang dapat ditampilkan... +
+ ) : ( + data.map((supplierReport) => { + return ( + +
0} + className={{ + containerClassName: 'w-full', + tableWrapperClassName: 'overflow-x-auto mt-4', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + 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', + paginationClassName: 'hidden', + }} + /> + + ); + }) + )} + + {meta && data.length > 0 && ( +
+ +
+ )} + + {/* Filter Modal */} + +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+
+ { + setFilterStartDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, start_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.start_date && ( +

+ {filterErrors.start_date} +

+ )} +
+ +
+ { + setFilterEndDate(e.target.value); + setFilterErrors((prev) => ({ ...prev, end_date: '' })); + }} + className={{ wrapper: 'w-full' }} + /> + {filterErrors.end_date && ( +

+ {filterErrors.end_date} +

+ )} +
+
+ +
+ { + setFilterSupplier( + Array.isArray(val) ? val : val ? [val] : [] + ); + }} + isLoading={isLoadingSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ +
+ { + setFilterDateType(val ? (val as OptionType) : undefined); + }} + className={{ wrapper: 'w-full' }} + isClearable + /> +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+ + ); +}; + +export default DebtSupplierTab; diff --git a/src/config/approval-line.ts b/src/config/approval-line.ts index fad098eb..4914d258 100644 --- a/src/config/approval-line.ts +++ b/src/config/approval-line.ts @@ -74,7 +74,23 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [ }, { step_number: 2, - step_name: 'Disetujui', + step_name: 'Approval Head Area', + }, + { + step_number: 3, + step_name: 'Approval Business Unit Vice President', + }, + { + step_number: 4, + step_name: 'Approval Finance', + }, + { + step_number: 5, + step_name: 'Realisasi', + }, + { + step_number: 6, + step_name: 'Selesai', }, ] as const; @@ -130,18 +146,22 @@ export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [ }, { step_number: 2, - step_name: 'Approval Manager', + step_name: 'Approval Head Area', }, { step_number: 3, - step_name: 'Approval Finance', + step_name: 'Approval Business Unit Vice President', }, { step_number: 4, - step_name: 'Realisasi', + step_name: 'Approval Finance', }, { step_number: 5, + step_name: 'Realisasi', + }, + { + step_number: 6, step_name: 'Selesai', }, ] as const; diff --git a/src/config/constant.ts b/src/config/constant.ts index f7f2255e..364f1824 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -60,6 +60,12 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ // TODO: add permission // permission: ['lti.daily_checklist.list'], }, + { + text: 'Konfigurasi', + link: '/daily-checklist/master-data/configuration', + // TODO: add permission + // permission: ['lti.daily_checklist.list'], + }, ], }, ], @@ -127,6 +133,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/report', icon: 'mdi:chart-box-outline', submenu: [ + { + text: 'Keuangan', + link: '/report/finance', + }, { text: 'Logistik & Persediaan', link: '/report/logistic-stock', diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index ca720f28..9a0c9d2e 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -20,6 +20,7 @@ export const ROUTE_PERMISSIONS: Record = { '/daily-checklist/reports/': ['lti.dashboard.list'], '/daily-checklist/master-data/employee/': ['lti.dashboard.list'], '/daily-checklist/master-data/activity/': ['lti.dashboard.list'], + '/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], // Production // Production - Project Flock @@ -117,6 +118,10 @@ export const ROUTE_PERMISSIONS: Record = { '/report/expense/': ['lti.repport.expense.list'], '/report/marketing/': ['lti.repport.delivery.list'], '/report/production-result/': ['lti.repport.production_result.list'], + '/report/finance/': [ + 'lti.repport.finance.list', + 'lti.repport.debtsupplier.list', + ], // Inventory '/inventory/adjustment/': ['lti.inventory.list'], diff --git a/src/dummy/dashboard/dashboard.production.dummy.json b/src/dummy/dashboard/dashboard.production.dummy.json deleted file mode 100644 index bb6e6af6..00000000 --- a/src/dummy/dashboard/dashboard.production.dummy.json +++ /dev/null @@ -1,1801 +0,0 @@ -{ - "statistics_data": [ - { - "title": "Total Keuangan", - "value": 2850000000, - "change": 12.5, - "period": "monthly", - "changeType": "increase" - }, - { - "title": "Penjualan", - "value": 3200000, - "change": 8.3, - "period": "monthly", - "changeType": "increase" - }, - { - "title": "Pembelian", - "value": 1850000000, - "change": -3.2, - "period": "monthly", - "changeType": "decrease" - }, - { - "title": "Biaya Overhead", - "value": 160000000, - "change": -1.5, - "period": "monthly", - "changeType": "decrease" - } - ], - "production_charts": [ - { - "date": "2025-12-01T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 88 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 92 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 90 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 85 - } - ] - }, - { - "date": "2025-12-03T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 85 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 95 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 93 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 87 - } - ] - }, - { - "date": "2025-12-05T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 82 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 98 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 91 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 84 - } - ] - }, - { - "date": "2025-12-07T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 80 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 89 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 88 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 82 - } - ] - }, - { - "date": "2025-12-08T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 83 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 92 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 95 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 85 - } - ] - }, - { - "date": "2025-12-11T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 81 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 88 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 92 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 83 - } - ] - }, - { - "date": "2025-12-13T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 84 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 90 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 89 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 86 - } - ] - }, - { - "date": "2025-12-15T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 82 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 94 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 96 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 84 - } - ] - }, - { - "date": "2025-12-17T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 80 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 91 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 93 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 82 - } - ] - }, - { - "date": "2025-12-19T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 79 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 88 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 90 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 81 - } - ] - }, - { - "date": "2025-12-21T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 81 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 97 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 92 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 83 - } - ] - }, - { - "date": "2025-12-23T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 83 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 95 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 98 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 85 - } - ] - }, - { - "date": "2025-12-25T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 80 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 89 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 94 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 82 - } - ] - }, - { - "date": "2025-12-27T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 82 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 93 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 96 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 84 - } - ] - }, - { - "date": "2025-12-28T00:00:00Z", - "flocks": [ - { - "id": 1, - "name": "Flock A-002", - "data": 85 - }, - { - "id": 2, - "name": "Flock A-001", - "data": 96 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 95 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 87 - } - ] - } - ], - "standard_productions": [ - { - "week": 18, - "standards": [ - { - "name": "hen_day", - "value": 40 - }, - { - "name": "hen_house", - "value": 38 - }, - { - "name": "uniformity", - "value": 85 - }, - { - "name": "egg_weight", - "value": 52 - }, - { - "name": "egg_mass", - "value": 20 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 38 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 37 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 39 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 36 - } - ] - }, - { - "week": 20, - "standards": [ - { - "name": "hen_day", - "value": 45 - }, - { - "name": "hen_house", - "value": 43 - }, - { - "name": "uniformity", - "value": 86 - }, - { - "name": "egg_weight", - "value": 54 - }, - { - "name": "egg_mass", - "value": 24 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 43 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 42 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 44 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 41 - } - ] - }, - { - "week": 22, - "standards": [ - { - "name": "hen_day", - "value": 48 - }, - { - "name": "hen_house", - "value": 46 - }, - { - "name": "uniformity", - "value": 87 - }, - { - "name": "egg_weight", - "value": 55 - }, - { - "name": "egg_mass", - "value": 26 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 47 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 46 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 48 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 45 - } - ] - }, - { - "week": 24, - "standards": [ - { - "name": "hen_day", - "value": 50 - }, - { - "name": "hen_house", - "value": 48 - }, - { - "name": "uniformity", - "value": 88 - }, - { - "name": "egg_weight", - "value": 56 - }, - { - "name": "egg_mass", - "value": 28 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 49 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 48 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 50 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 47 - } - ] - }, - { - "week": 26, - "standards": [ - { - "name": "hen_day", - "value": 52 - }, - { - "name": "hen_house", - "value": 50 - }, - { - "name": "uniformity", - "value": 89 - }, - { - "name": "egg_weight", - "value": 57 - }, - { - "name": "egg_mass", - "value": 30 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 50 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 49 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 51 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 48 - } - ] - }, - { - "week": 28, - "standards": [ - { - "name": "hen_day", - "value": 55 - }, - { - "name": "hen_house", - "value": 53 - }, - { - "name": "uniformity", - "value": 90 - }, - { - "name": "egg_weight", - "value": 58 - }, - { - "name": "egg_mass", - "value": 32 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 53 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 52 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 54 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 51 - } - ] - }, - { - "week": 30, - "standards": [ - { - "name": "hen_day", - "value": 58 - }, - { - "name": "hen_house", - "value": 56 - }, - { - "name": "uniformity", - "value": 91 - }, - { - "name": "egg_weight", - "value": 59 - }, - { - "name": "egg_mass", - "value": 34 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 55 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 54 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 56 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 53 - } - ] - }, - { - "week": 32, - "standards": [ - { - "name": "hen_day", - "value": 60 - }, - { - "name": "hen_house", - "value": 58 - }, - { - "name": "uniformity", - "value": 92 - }, - { - "name": "egg_weight", - "value": 60 - }, - { - "name": "egg_mass", - "value": 36 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 58 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 57 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 59 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 56 - } - ] - }, - { - "week": 34, - "standards": [ - { - "name": "hen_day", - "value": 62 - }, - { - "name": "hen_house", - "value": 60 - }, - { - "name": "uniformity", - "value": 92 - }, - { - "name": "egg_weight", - "value": 61 - }, - { - "name": "egg_mass", - "value": 38 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 60 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 59 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 61 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 58 - } - ] - }, - { - "week": 36, - "standards": [ - { - "name": "hen_day", - "value": 64 - }, - { - "name": "hen_house", - "value": 62 - }, - { - "name": "uniformity", - "value": 93 - }, - { - "name": "egg_weight", - "value": 62 - }, - { - "name": "egg_mass", - "value": 40 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 62 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 61 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 63 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 60 - } - ] - }, - { - "week": 38, - "standards": [ - { - "name": "hen_day", - "value": 66 - }, - { - "name": "hen_house", - "value": 64 - }, - { - "name": "uniformity", - "value": 93 - }, - { - "name": "egg_weight", - "value": 63 - }, - { - "name": "egg_mass", - "value": 42 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 64 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 63 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 65 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 62 - } - ] - }, - { - "week": 40, - "standards": [ - { - "name": "hen_day", - "value": 68 - }, - { - "name": "hen_house", - "value": 66 - }, - { - "name": "uniformity", - "value": 94 - }, - { - "name": "egg_weight", - "value": 64 - }, - { - "name": "egg_mass", - "value": 44 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 66 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 65 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 67 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 64 - } - ] - }, - { - "week": 42, - "standards": [ - { - "name": "hen_day", - "value": 70 - }, - { - "name": "hen_house", - "value": 68 - }, - { - "name": "uniformity", - "value": 94 - }, - { - "name": "egg_weight", - "value": 65 - }, - { - "name": "egg_mass", - "value": 46 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 68 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 67 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 69 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 66 - } - ] - }, - { - "week": 44, - "standards": [ - { - "name": "hen_day", - "value": 72 - }, - { - "name": "hen_house", - "value": 70 - }, - { - "name": "uniformity", - "value": 95 - }, - { - "name": "egg_weight", - "value": 66 - }, - { - "name": "egg_mass", - "value": 48 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 70 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 69 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 71 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 68 - } - ] - }, - { - "week": 46, - "standards": [ - { - "name": "hen_day", - "value": 74 - }, - { - "name": "hen_house", - "value": 72 - }, - { - "name": "uniformity", - "value": 95 - }, - { - "name": "egg_weight", - "value": 67 - }, - { - "name": "egg_mass", - "value": 50 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 72 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 71 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 73 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 70 - } - ] - }, - { - "week": 48, - "standards": [ - { - "name": "hen_day", - "value": 76 - }, - { - "name": "hen_house", - "value": 74 - }, - { - "name": "uniformity", - "value": 95 - }, - { - "name": "egg_weight", - "value": 68 - }, - { - "name": "egg_mass", - "value": 52 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 74 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 73 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 75 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 72 - } - ] - }, - { - "week": 50, - "standards": [ - { - "name": "hen_day", - "value": 78 - }, - { - "name": "hen_house", - "value": 76 - }, - { - "name": "uniformity", - "value": 96 - }, - { - "name": "egg_weight", - "value": 69 - }, - { - "name": "egg_mass", - "value": 54 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 76 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 75 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 77 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 74 - } - ] - }, - { - "week": 52, - "standards": [ - { - "name": "hen_day", - "value": 80 - }, - { - "name": "hen_house", - "value": 78 - }, - { - "name": "uniformity", - "value": 96 - }, - { - "name": "egg_weight", - "value": 70 - }, - { - "name": "egg_mass", - "value": 56 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 78 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 77 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 79 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 76 - } - ] - }, - { - "week": 54, - "standards": [ - { - "name": "hen_day", - "value": 82 - }, - { - "name": "hen_house", - "value": 80 - }, - { - "name": "uniformity", - "value": 96 - }, - { - "name": "egg_weight", - "value": 71 - }, - { - "name": "egg_mass", - "value": 58 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 80 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 79 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 81 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 78 - } - ] - }, - { - "week": 56, - "standards": [ - { - "name": "hen_day", - "value": 84 - }, - { - "name": "hen_house", - "value": 82 - }, - { - "name": "uniformity", - "value": 97 - }, - { - "name": "egg_weight", - "value": 72 - }, - { - "name": "egg_mass", - "value": 60 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 82 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 81 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 83 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 80 - } - ] - }, - { - "week": 58, - "standards": [ - { - "name": "hen_day", - "value": 86 - }, - { - "name": "hen_house", - "value": 84 - }, - { - "name": "uniformity", - "value": 97 - }, - { - "name": "egg_weight", - "value": 73 - }, - { - "name": "egg_mass", - "value": 62 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 84 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 83 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 85 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 82 - } - ] - }, - { - "week": 60, - "standards": [ - { - "name": "hen_day", - "value": 88 - }, - { - "name": "hen_house", - "value": 86 - }, - { - "name": "uniformity", - "value": 97 - }, - { - "name": "egg_weight", - "value": 74 - }, - { - "name": "egg_mass", - "value": 64 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 86 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 85 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 87 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 84 - } - ] - }, - { - "week": 62, - "standards": [ - { - "name": "hen_day", - "value": 90 - }, - { - "name": "hen_house", - "value": 88 - }, - { - "name": "uniformity", - "value": 98 - }, - { - "name": "egg_weight", - "value": 75 - }, - { - "name": "egg_mass", - "value": 66 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 88 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 87 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 89 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 86 - } - ] - }, - { - "week": 64, - "standards": [ - { - "name": "hen_day", - "value": 92 - }, - { - "name": "hen_house", - "value": 90 - }, - { - "name": "uniformity", - "value": 98 - }, - { - "name": "egg_weight", - "value": 76 - }, - { - "name": "egg_mass", - "value": 68 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 90 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 89 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 91 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 88 - } - ] - }, - { - "week": 66, - "standards": [ - { - "name": "hen_day", - "value": 94 - }, - { - "name": "hen_house", - "value": 92 - }, - { - "name": "uniformity", - "value": 98 - }, - { - "name": "egg_weight", - "value": 77 - }, - { - "name": "egg_mass", - "value": 70 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 92 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 91 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 93 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 90 - } - ] - }, - { - "week": 68, - "standards": [ - { - "name": "hen_day", - "value": 95 - }, - { - "name": "hen_house", - "value": 93 - }, - { - "name": "uniformity", - "value": 98 - }, - { - "name": "egg_weight", - "value": 78 - }, - { - "name": "egg_mass", - "value": 72 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 93 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 92 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 94 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 91 - } - ] - }, - { - "week": 70, - "standards": [ - { - "name": "hen_day", - "value": 96 - }, - { - "name": "hen_house", - "value": 94 - }, - { - "name": "uniformity", - "value": 99 - }, - { - "name": "egg_weight", - "value": 79 - }, - { - "name": "egg_mass", - "value": 74 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 94 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 93 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 95 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 92 - } - ] - }, - { - "week": 72, - "standards": [ - { - "name": "hen_day", - "value": 97 - }, - { - "name": "hen_house", - "value": 95 - }, - { - "name": "uniformity", - "value": 99 - }, - { - "name": "egg_weight", - "value": 80 - }, - { - "name": "egg_mass", - "value": 76 - } - ], - "flocks": [ - { - "id": 1, - "name": "Flock A-001", - "data": 95 - }, - { - "id": 2, - "name": "Flock A-002", - "data": 94 - }, - { - "id": 3, - "name": "Flock B-001", - "data": 96 - }, - { - "id": 4, - "name": "Flock B-002", - "data": 93 - } - ] - } - ], - "egg_weights": [ - { - "flock": { - "id": 1, - "name": "Flock A-001" - }, - "weight": 62 - }, - { - "flock": { - "id": 2, - "name": "Flock A-002" - }, - "weight": 61 - }, - { - "flock": { - "id": 3, - "name": "Flock B-001" - }, - "weight": 63 - }, - { - "flock": { - "id": 4, - "name": "Flock B-002" - }, - "weight": 60 - }, - { - "flock": { - "id": 5, - "name": "Flock C-001" - }, - "weight": 62 - } - ], - "fcr_data": [ - { - "flock": { - "id": 1, - "name": "Flock A-001" - }, - "fcr": 2.1 - }, - { - "flock": { - "id": 2, - "name": "Flock A-002" - }, - "fcr": 2.3 - }, - { - "flock": { - "id": 3, - "name": "Flock B-001" - }, - "fcr": 2 - }, - { - "flock": { - "id": 4, - "name": "Flock B-002" - }, - "fcr": 2.4 - }, - { - "flock": { - "id": 5, - "name": "Flock C-001" - }, - "fcr": 2.2 - } - ] -} diff --git a/src/dummy/dashboard/dashboard.production.dummy.ts b/src/dummy/dashboard/dashboard.production.dummy.ts deleted file mode 100644 index b663f28c..00000000 --- a/src/dummy/dashboard/dashboard.production.dummy.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Dummy data for DashboardProduction - * Generated from: dashboard.production.dummy.json - * - * This file is auto-generated. Do not edit manually. - */ - -import { - DashboardProductionStatisticsData, - DashboardProductionProductionChartsFlocks, - DashboardProductionProductionCharts, - DashboardProductionStandardProductionsStandards, - DashboardProductionStandardProductions, - DashboardProductionFcrDataFlock, - DashboardProductionEggWeights, - DashboardProductionFcrData, - DashboardProduction, -} from '../../types/api/dashboard/dashboard-production'; -import { BaseApiResponse } from '@/types/api/api-general'; -import dummyData from './dashboard.production.dummy.json'; - -/** - * Get dummy DashboardProduction data - * @returns Promise with BaseApiResponse containing DashboardProduction - */ -export async function getDummySingle(): Promise< - BaseApiResponse -> { - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - code: 200, - status: 'success', - message: 'Data retrieved successfully', - data: dummyData as unknown as DashboardProduction, - }); - }, 500); - }); -} diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index ae704e92..7bd0be83 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -30,7 +30,7 @@ import { KandangApi } from '@/services/api/master-data'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import useSWR from 'swr'; -import { BaseApiResponse } from '@/types/api/api-general'; +import { BaseApiResponse, Document } from '@/types/api/api-general'; import { AxiosError } from 'axios'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { PhaseApi } from '@/services/api/daily-checklist/phase'; @@ -39,6 +39,9 @@ import { Employee } from '@/types/api/daily-checklist/employee'; import { PhaseActivityApi } from '@/services/api/daily-checklist/phase-activity'; import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity'; import DebouncedTextArea from '@/components/input/DebouncedTextArea'; +import DropFileInput from '@/components/input/DropFileInput'; +import Link from 'next/link'; +import { Icon } from '@iconify/react'; // Static categories const CATEGORIES = [ @@ -148,6 +151,10 @@ export function DailyChecklistContent() { const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); + const [existingDocuments, setExistingDocuments] = useState([]); + const [documents, setDocuments] = useState([]); + const [deletedDocumentIds, setDeletedDocumentIds] = useState([]); + // Format date for display const formatDateForDisplay = (dateStr: string) => { if (!dateStr) return 'Pilih tanggal'; @@ -340,6 +347,9 @@ export function DailyChecklistContent() { return; } + // set existing document + setExistingDocuments(existingDailyChecklist?.data.document_urls || []); + // Build assignments map const assignmentMap: { [taskId: string]: { @@ -729,7 +739,11 @@ export function DailyChecklistContent() { setLoading(true); try { - const submitRes = await DailyChecklistApi.submit(dailyChecklistId); + const submitRes = await DailyChecklistApi.submit( + dailyChecklistId, + documents, + deletedDocumentIds + ); if (isResponseError(submitRes)) { console.error('Error submitting:', submitRes.message); @@ -750,6 +764,19 @@ export function DailyChecklistContent() { const handleSaveDraft = async () => { if (!dailyChecklistId) return; + const uploadImageRes = await DailyChecklistApi.uploadImage( + Number(dailyChecklistId), + 'DRAFT', + documents, + deletedDocumentIds + ); + + if (isResponseError(uploadImageRes)) { + console.error('Error saving draft:', uploadImageRes.message); + toast.error('Gagal menyimpan draft'); + return; + } + toast.success('Draft tersimpan otomatis'); }; @@ -1263,6 +1290,94 @@ export function DailyChecklistContent() { )} + {dailyChecklistId && + selectedPhaseIds.length > 0 && + selectedEmployees.length > 0 && ( + <> + {existingDocuments.length > 0 && ( +
+

+ Dokumen yang telah diupload +

+ {existingDocuments.map( + (existingDocument, existingDocumentIdx) => ( +
+ + {existingDocument.name}{' '} + + + + +
+ ) + )} +
+ )} + + { + setDocuments(files); + }} + onDelete={(deletedFileIdx: number) => { + const newRequestDocuments = [...documents]; + + newRequestDocuments?.splice(deletedFileIdx, 1); + + setDocuments(newRequestDocuments); + }} + className={{ + wrapper: 'mt-6', + inputWrapper: 'flex items-center', + label: 'font-semibold text-gray-900', + }} + /> + + )} + {/* Action Buttons */} {dailyChecklistId && selectedPhaseIds.length > 0 && diff --git a/src/figma-make/components/pages/dashboard/Dashboard.tsx b/src/figma-make/components/pages/dashboard/Dashboard.tsx index 5d866ffc..f6d12d79 100644 --- a/src/figma-make/components/pages/dashboard/Dashboard.tsx +++ b/src/figma-make/components/pages/dashboard/Dashboard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Card, CardContent, @@ -15,7 +15,6 @@ import { SelectTrigger, SelectValue, } from '@/figma-make/components/base/select'; -import { Input } from '@/figma-make/components/base/input'; import { Badge } from '@/figma-make/components/base/badge'; import { Calendar as CalendarIcon, @@ -35,53 +34,17 @@ import { ResponsiveContainer, Cell, } from 'recharts'; -import { supabase, isSupabaseConfigured } from '@/figma-make/lib/supabase'; import { toast } from 'sonner'; - -interface EmployeePerformance { - employee_id: string; - employee_name: string; - kandang_id: string; - kandang_name: string; - total_activities_in_category: number; // Total aktivitas di kategori - completed_activities: number; // Aktivitas yang sudah di-check - completion_rate: number; - last_activity_date: string | null; - color: string; // Color based on kandang -} - -interface Kandang { - id: string; - name: string; -} - -interface Category { - id: string; - name: string; -} - -interface ChecklistKandang { - id: string; - date: string; - kandang_id: string; - category: string; - kandang: { - id: string; - name: string; - } | null; -} - -interface AssignmentEmployee { - id: string; - task_id: string; - employee_id: string; - checked: boolean; - updated_at: string; - employee: { - id: string; - name: string; - } | null; -} +import useSWR from 'swr'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checklist'; +import { AxiosError } from 'axios'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; +import { KandangApi } from '@/services/api/master-data'; +import { useSelect } from '@/components/input/SelectInput'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { formatDate } from '@/lib/helper'; const KANDANG_COLORS = [ '#0069e0', // Blue (primary) @@ -102,312 +65,65 @@ const CATEGORY_LABELS: { [key: string]: string } = { }; export function Dashboard() { - const [loading, setLoading] = useState(false); - const [employeePerformance, setEmployeePerformance] = useState< - EmployeePerformance[] - >([]); - - // Master data - const [kandangList, setKandangList] = useState([]); - const [categoryList, setCategoryList] = useState([]); - // Filters const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const [kandangFilter, setKandangFilter] = useState('ALL'); const [categoryFilter, setCategoryFilter] = useState('ALL'); - // Color mapping for kandang - const [kandangColorMap, setKandangColorMap] = useState<{ - [key: string]: string; - }>({}); - - useEffect(() => { - fetchMasterData(); - }, []); - - useEffect(() => { - // Only fetch when date filters are set - if (dateFrom && dateTo) { - fetchEmployeePerformance(); - } else { - setEmployeePerformance([]); + const { + data: summaryResponse, + isLoading: isLoadingSummary, + mutate: refreshSummary, + } = useSWR< + BaseApiResponse, + AxiosError, + SWRHttpKey + >( + dateFrom && dateTo + ? `${DailyChecklistApi.basePath}/summary?date_from=${dateFrom}&date_to=${dateTo}&kandang_id=${kandangFilter === 'ALL' ? '' : kandangFilter}&category=${categoryFilter === 'ALL' ? '' : categoryFilter}` + : '', + httpClientFetcher, + { + keepPreviousData: true, } - }, [dateFrom, dateTo, kandangFilter, categoryFilter]); + ); - const fetchMasterData = async () => { - if (!isSupabaseConfigured()) return; - - try { - // Fetch kandang - const { data: kandangData, error: kandangError } = await supabase - .from('kandang') - .select('id, name') - .order('name', { ascending: true }); - - if (kandangError) { - console.error('Error fetching kandang:', kandangError); - } else { - setKandangList(kandangData || []); - - // Create color mapping - const colorMap: { [key: string]: string } = {}; - (kandangData || []).forEach((k, index) => { - colorMap[k.id] = KANDANG_COLORS[index % KANDANG_COLORS.length]; - }); - setKandangColorMap(colorMap); - } - - // Set categories from CATEGORY_LABELS (hardcoded list) - const categories: Category[] = Object.keys(CATEGORY_LABELS).map((id) => ({ - id, - name: CATEGORY_LABELS[id], - })); - setCategoryList(categories); - } catch (error) { - console.error('Error fetching master data:', error); - } - }; - - const fetchEmployeePerformance = async () => { - if (!isSupabaseConfigured() || !dateFrom || !dateTo) { - return; - } - - try { - setLoading(true); - - // Step 1: Get all checklists in date range + filters - let checklistQuery = supabase - .from('daily_checklists') - .select( - ` - id, - date, - kandang_id, - category, - kandang:kandang_id ( - id, - name - ) - ` - ) - .gte('date', dateFrom) - .lte('date', dateTo); - - if (kandangFilter !== 'ALL') { - checklistQuery = checklistQuery.eq('kandang_id', kandangFilter); - } - - if (categoryFilter !== 'ALL') { - checklistQuery = checklistQuery.eq('category', categoryFilter); - } - - const { data: checklists, error: checklistError } = await checklistQuery; - - if (checklistError) { - console.error('Error fetching checklists:', checklistError); - toast.error('Gagal memuat data checklist'); - return; - } - - if (!checklists || checklists.length === 0) { - setEmployeePerformance([]); - return; - } - - const checklistsData = checklists as unknown as ChecklistKandang[]; - - // Step 2: Get all tasks from these checklists - const checklistIds = checklistsData.map((c) => c.id); - const { data: tasks, error: tasksError } = await supabase - .from('daily_checklist_activity_tasks') - .select('id, checklist_id') - .in('checklist_id', checklistIds); - - if (tasksError) { - console.error('Error fetching tasks:', tasksError); - return; - } - - if (!tasks || tasks.length === 0) { - setEmployeePerformance([]); - return; - } - - const taskIds = tasks.map((t) => t.id); - - // Step 3: Get all assignments for these tasks - const { data: assignments, error: assignmentsError } = await supabase - .from('daily_checklist_activity_task_assignments') - .select( - ` - id, - task_id, - employee_id, - checked, - updated_at, - employee:employee_id ( - id, - name - ) - ` - ) - .in('task_id', taskIds); - - if (assignmentsError) { - console.error('Error fetching assignments:', assignmentsError); - return; - } - - if (!assignments || assignments.length === 0) { - setEmployeePerformance([]); - return; - } - - const assignmentsData = assignments as unknown as AssignmentEmployee[]; - - // Step 4: Calculate total activities in selected category (if filtered) - let totalActivitiesInCategory = 0; - - if (categoryFilter !== 'ALL') { - // Get total activities from master data for this category - const { data: phases } = await supabase - .from('phases') - .select('id') - .eq('category_id', categoryFilter); - - if (phases && phases.length > 0) { - const phaseIds = phases.map((p) => p.id); - const { count } = await supabase - .from('activities') - .select('*', { count: 'exact', head: true }) - .in('phase_id', phaseIds); - - totalActivitiesInCategory = count || 0; - } - } - - // Step 5: Group by employee and calculate performance - const employeeMap = new Map< - string, - { - employee_id: string; - employee_name: string; - kandang_id: string; - kandang_name: string; - completed_count: number; - total_count: number; - last_activity_date: string | null; - } - >(); - - assignmentsData.forEach((assignment) => { - const task = tasks.find((t) => t.id === assignment.task_id); - if (!task) return; - - const checklist = checklistsData.find( - (c) => c.id === task.checklist_id - ); - if (!checklist) return; - - const employeeId = assignment.employee_id; - const employeeName = assignment.employee?.name || 'Unknown'; - const kandangId = checklist.kandang_id; - const kandangName = checklist.kandang?.name || 'Unknown'; - - if (!employeeMap.has(employeeId)) { - employeeMap.set(employeeId, { - employee_id: employeeId, - employee_name: employeeName, - kandang_id: kandangId, - kandang_name: kandangName, - completed_count: 0, - total_count: 0, - last_activity_date: null, - }); - } - - const empData = employeeMap.get(employeeId)!; - empData.total_count += 1; - - if (assignment.checked) { - empData.completed_count += 1; - } - - // Update last activity date - if (assignment.updated_at) { - if ( - !empData.last_activity_date || - assignment.updated_at > empData.last_activity_date - ) { - empData.last_activity_date = assignment.updated_at; - } - } - }); - - // Step 6: Convert to array and add calculated fields - const performanceData: EmployeePerformance[] = Array.from( - employeeMap.values() - ).map((emp) => { - // Use total activities in category if category is selected, otherwise use employee's assigned count - const totalActivities = - categoryFilter !== 'ALL' && totalActivitiesInCategory > 0 - ? totalActivitiesInCategory - : emp.total_count; - - return { - employee_id: emp.employee_id, - employee_name: emp.employee_name, - kandang_id: emp.kandang_id, - kandang_name: emp.kandang_name, - total_activities_in_category: totalActivities, - completed_activities: emp.completed_count, - completion_rate: - totalActivities > 0 - ? Math.round((emp.completed_count / totalActivities) * 100) - : 0, - last_activity_date: emp.last_activity_date, - color: kandangColorMap[emp.kandang_id] || '#0069e0', - }; - }); - - // Sort by employee name - performanceData.sort((a, b) => - a.employee_name.localeCompare(b.employee_name) - ); - - setEmployeePerformance(performanceData); - } catch (error) { - console.error('Error fetching employee performance:', error); - toast.error('Terjadi kesalahan saat memuat data'); - } finally { - setLoading(false); - } - }; - - const formatDate = (dateString: string | null) => { - if (!dateString) return '-'; - const date = new Date(dateString); - return date.toLocaleDateString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', + const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect(KandangApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', }); - }; - const hasFilters = dateFrom && dateTo; + const kandangColorMap: { [key: string]: string } = {}; + (kandangOptions || []).forEach((k, index) => { + kandangColorMap[k.value] = KANDANG_COLORS[index % KANDANG_COLORS.length]; + }); - // Prepare chart data - const chartData = employeePerformance.map((emp) => ({ + const employeePerformance = isResponseSuccess(summaryResponse) + ? summaryResponse.data?.tracking_abk.map((abk) => { + return { + ...abk, + color: kandangColorMap[abk.kandang_id] || '#0069e0', + }; + }) + : []; + + const chartData = employeePerformance?.map((emp) => ({ name: emp.employee_name, - completed: emp.completed_activities, - remaining: emp.total_activities_in_category - emp.completed_activities, - total: emp.total_activities_in_category, + completed: emp.activity_done, + remaining: emp.activity_left, + total: emp.total_activity, color: emp.color, kandang: emp.kandang_name, })); + const hasFilters = dateFrom && dateTo; + + if (summaryResponse && isResponseError(summaryResponse)) { + toast.error('Gagal memuat data: ' + summaryResponse.message); + } + return (
@@ -457,9 +173,12 @@ export function Dashboard() { Semua Kandang - {kandangList.map((kandang) => ( - - {kandang.name} + {kandangOptions.map((kandang) => ( + + {kandang.label} ))} @@ -482,9 +201,9 @@ export function Dashboard() { Semua Kategori - {categoryList.map((category) => ( - - {category.name} + {Object.keys(CATEGORY_LABELS).map((category) => ( + + {CATEGORY_LABELS[category]} ))} @@ -523,11 +242,11 @@ export function Dashboard() { melihat performance ABK.

- ) : loading ? ( + ) : isLoadingSummary ? (
Memuat data...
- ) : employeePerformance.length === 0 ? ( + ) : employeePerformance && employeePerformance.length === 0 ? (

@@ -582,7 +301,7 @@ export function Dashboard() { fill='#10B981' radius={[0, 0, 0, 0]} > - {chartData.map((entry, index) => ( + {chartData?.map((entry, index) => ( - {chartData.map((entry, index) => ( + {chartData?.map((entry, index) => ( {/* Employee Tracking Table */} - {hasFilters && employeePerformance.length > 0 && ( - - - Tracking ABK -

- Detail performance masing-masing ABK -

-
- -
-

- - - - - - - - - - - - - {employeePerformance.map((emp, index) => ( - - - - - - - - + {hasFilters && + employeePerformance && + employeePerformance.length > 0 && ( + + + Tracking ABK +

+ Detail performance masing-masing ABK +

+
+ +
+
- Nama ABK - - Kandang - - Total Aktivitas - - Aktivitas Selesai - - Aktivitas Tersisa - - Completion Rate - - Last Activity -
- {emp.employee_name} - - - {emp.kandang_name} - - - {emp.total_activities_in_category} - - {emp.completed_activities} - - {emp.total_activities_in_category - - emp.completed_activities} - -
-
-
-
- - {emp.completion_rate}% - -
-
- {formatDate(emp.last_activity_date)} -
+ + + + + + + + + - ))} - -
+ Nama ABK + + Kandang + + Total Aktivitas + + Aktivitas Selesai + + Aktivitas Tersisa + + Completion Rate + + Last Activity +
-
- - - )} + + + {employeePerformance?.map((emp, index) => ( + + + {emp.employee_name} + + + + {emp.kandang_name} + + + + {emp.total_activity} + + + {emp.activity_done} + + + {emp.activity_left} + + +
+
+
+
+ + {emp.completion_rate}% + +
+ + + {formatDate(emp.last_activity, 'DD MMM YYYY')} + + + ))} + + +
+ + + )}
); diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index 54e4c93f..a867c29d 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -20,6 +20,9 @@ import { toast } from 'sonner'; import { useRouter, useSearchParams } from 'next/navigation'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { isResponseError } from '@/lib/api-helper'; +import Link from 'next/link'; +import { Icon } from '@iconify/react'; +import { Document } from '@/types/api/api-general'; interface ChecklistDetailRow { checklist_id: string; @@ -125,6 +128,7 @@ export function DetailDailyChecklistContent() { const [employees, setEmployees] = useState<{ id: string; name: string }[]>( [] ); + const [documents, setDocuments] = useState([]); // Modals const [showApproveModal, setShowApproveModal] = useState(false); @@ -160,6 +164,8 @@ export function DetailDailyChecklistContent() { const rawDetailChecklist = checklistDataRes?.data; + setDocuments(rawDetailChecklist?.document_urls || []); + const checklistData = { id: rawDetailChecklist?.id, date: rawDetailChecklist?.date, @@ -842,6 +848,37 @@ export function DetailDailyChecklistContent() { Tidak ada data aktivitas
)} + + {documents.length > 0 && ( +
+

+ Dokumen yang telah diupload +

+ +
    + {documents.map((existingDocument, existingDocumentIdx) => ( +
  • +
    + + {existingDocument.name}{' '} + + +
    +
  • + ))} +
+
+ )}
diff --git a/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx b/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx index a5b8ac3d..36774ce2 100644 --- a/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx +++ b/src/figma-make/components/pages/master-data/activity/MasterAktivitasContent.tsx @@ -328,6 +328,7 @@ export function MasterAktivitasContent() { return; } + refreshPhases(); refreshPhaseActivities(); toast.success('Aktivitas berhasil ditambahkan'); } else { @@ -349,6 +350,7 @@ export function MasterAktivitasContent() { return; } + refreshPhases(); refreshPhaseActivities(); toast.success('Aktivitas berhasil diubah'); } @@ -387,6 +389,7 @@ export function MasterAktivitasContent() { return; } + refreshPhases(); refreshPhaseActivities(); toast.success('Aktivitas berhasil dihapus'); setShowActivityDeleteConfirm(false); diff --git a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx new file mode 100644 index 00000000..1358d6ba --- /dev/null +++ b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx @@ -0,0 +1,564 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react'; +import { Card, CardContent } from '@/figma-make/components/base/card'; +import { Button } from '@/figma-make/components/base/button'; +import { Label } from '@/figma-make/components/base/label'; +import { Input } from '@/figma-make/components/base/input'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/figma-make/components/base/dialog'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/figma-make/components/base/alert-dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/figma-make/components/base/dropdown-menu'; +import { toast } from 'sonner'; +import useSWR from 'swr'; +import Table from '@/components/Table'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatDate } from '@/lib/helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ColumnDef } from '@tanstack/react-table'; +import { DailyChecklistConfiguration } from '@/types/api/daily-checklist/configuration'; +import { DailyChecklistConfigurationApi } from '@/services/api/daily-checklist/configuration'; +import { DatePicker } from '@/figma-make/components/base/date-picker'; + +export function MasterConfigurationContent() { + const { + state: tableFilterState, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, + }); + + const { + data: dailyChecklistConfigurations, + isLoading: isLoadingDailyChecklistConfigurations, + mutate: refreshDailyChecklistConfigurations, + } = useSWR( + `${DailyChecklistConfigurationApi.basePath}${getTableFilterQueryString()}`, + DailyChecklistConfigurationApi.getAllFetcher, + { + keepPreviousData: true, + } + ); + + const [showModal, setShowModal] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [configurationToDelete, setConfigurationToDelete] = useState< + number | null + >(null); + const [loading, setLoading] = useState(false); + const [modalMode, setModalMode] = useState<'create' | 'edit'>('create'); + const [configurationForm, setConfigurationForm] = useState({ + id: 0, + date: '', + percentage_threshold_bad: '', + percentage_threshold_enough: '', + }); + + const configurationColumns: ColumnDef[] = [ + { + id: 'date', + header: 'Tanggal', + accessorKey: 'date', + enableSorting: false, + cell: ({ row }) => formatDate(row.original.date, 'DD MMM YYYY'), + }, + { + id: 'percentage_threshold_bad', + header: 'Threshold Bad', + accessorKey: 'percentage_threshold_bad', + enableSorting: false, + cell: ({ row }) => `${row.original.percentage_threshold_bad}%`, + }, + { + id: 'percentage_threshold_enough', + header: 'Threshold Enough', + accessorKey: 'percentage_threshold_enough', + enableSorting: false, + cell: ({ row }) => `${row.original.percentage_threshold_enough}%`, + }, + { + id: 'action', + header: 'Aksi', + accessorKey: 'action', + enableSorting: false, + cell: ({ row }) => ( + + + + + + handleEdit(row.original)}> + + Edit + + handleDeleteClick(row.original.id)} + className='text-red-600' + > + + Hapus + + + + ), + }, + ]; + + const handleAdd = () => { + setModalMode('create'); + setConfigurationForm({ + id: 0, + date: '', + percentage_threshold_bad: '', + percentage_threshold_enough: '', + }); + setShowModal(true); + }; + + const handleEdit = (configuration: DailyChecklistConfiguration) => { + setModalMode('edit'); + setConfigurationForm({ + id: configuration.id, + date: configuration.date, + percentage_threshold_bad: String(configuration.percentage_threshold_bad), + percentage_threshold_enough: String( + configuration.percentage_threshold_enough + ), + }); + setShowModal(true); + }; + + const handleSave = async () => { + if ( + !configurationForm.date.trim() || + Number(configurationForm.percentage_threshold_bad) === 0 || + Number(configurationForm.percentage_threshold_enough) === 0 + ) { + toast.error('Tanggal dan persentase harus diisi'); + return; + } + + setLoading(true); + + try { + if (modalMode === 'create') { + const createConfigurationResponse = + await DailyChecklistConfigurationApi.create({ + date: formatDate(configurationForm.date, 'YYYY-MM-DD'), + percentage_threshold_bad: Number( + configurationForm.percentage_threshold_bad + ), + percentage_threshold_enough: Number( + configurationForm.percentage_threshold_enough + ), + }); + + if (isResponseError(createConfigurationResponse)) { + console.error( + 'Error creating configuration:', + createConfigurationResponse.message + ); + toast.error('Gagal menambahkan konfigurasi'); + return; + } + + refreshDailyChecklistConfigurations(); + toast.success('Konfigurasi berhasil ditambahkan'); + } else { + const updateConfigurationResponse = + await DailyChecklistConfigurationApi.update(configurationForm.id, { + date: formatDate(configurationForm.date, 'YYYY-MM-DD'), + percentage_threshold_bad: Number( + configurationForm.percentage_threshold_bad + ), + percentage_threshold_enough: Number( + configurationForm.percentage_threshold_enough + ), + }); + + if (isResponseError(updateConfigurationResponse)) { + console.error( + 'Error updating configuration:', + updateConfigurationResponse.message + ); + toast.error('Gagal mengubah konfigurasi'); + return; + } + + refreshDailyChecklistConfigurations(); + toast.success('Konfigurasi berhasil diubah'); + } + + setShowModal(false); + setConfigurationForm({ + id: 0, + date: '', + percentage_threshold_bad: '', + percentage_threshold_enough: '', + }); + } catch (error) { + console.error('Error saving configuration:', error); + toast.error('Terjadi kesalahan saat menyimpan konfigurasi'); + } finally { + setLoading(false); + } + }; + + const handleDeleteClick = (configurationId: number) => { + setConfigurationToDelete(configurationId); + setShowDeleteConfirm(true); + }; + + const handleConfirmDelete = async () => { + if (!configurationToDelete) return; + + setLoading(true); + + try { + const deleteConfigurationResponse = + await DailyChecklistConfigurationApi.delete(configurationToDelete); + + if (isResponseError(deleteConfigurationResponse)) { + console.error( + 'Error deleting configuration:', + deleteConfigurationResponse.message + ); + toast.error('Gagal menghapus konfigurasi'); + return; + } + + refreshDailyChecklistConfigurations(); + toast.success('Konfigurasi berhasil dihapus'); + setShowDeleteConfirm(false); + setConfigurationToDelete(null); + } catch (error) { + console.error('Error deleting employee:', error); + toast.error('Terjadi kesalahan saat menghapus konfigurasi'); + } finally { + setLoading(false); + } + }; + + const handleExport = (format: string) => { + toast.success(`Data berhasil diekspor ke ${format}`); + }; + + if (isLoadingDailyChecklistConfigurations && !dailyChecklistConfigurations) { + return ( +
+
+
+

+ Master Konfigurasi +

+

+ Master Data • Konfigurasi +

+
+ + + Memuat data... + + +
+
+ ); + } + + const formatDateForDisplay = (dateStr: string) => { + if (!dateStr) return 'Pilih tanggal'; + const [year, month, day] = dateStr.split('-'); + const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); + return date.toLocaleDateString('id-ID', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }; + + return ( +
+
+ {/* Page Title */} +
+

+ Master Konfigurasi +

+

+ Master Data • Konfigurasi +

+
+ + {/* Main Card */} + + + {/* Single Toolbar Row */} +
+
+ +
+
+ + {/* Table */} + + data={ + isResponseSuccess(dailyChecklistConfigurations) + ? dailyChecklistConfigurations?.data + : [] + } + columns={configurationColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(dailyChecklistConfigurations) + ? dailyChecklistConfigurations?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(dailyChecklistConfigurations) + ? dailyChecklistConfigurations?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingDailyChecklistConfigurations} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(dailyChecklistConfigurations) && + dailyChecklistConfigurations?.data?.length === 0, + }), + tableWrapperClassName: + 'overflow-x-auto border border-solid border-base-content/10 rounded-none', + headerRowClassName: 'bg-gray-50/50', + headerColumnClassName: + 'text-left py-3.5 px-6 text-sm font-semibold text-gray-700', + paginationClassName: 'px-4', + }} + /> +
+
+
+ + {/* Add/Edit Modal */} + + + + + {modalMode === 'create' + ? 'Tambah Konfigurasi' + : 'Edit Konfigurasi'} + + + {modalMode === 'create' + ? 'Masukkan detail konfigurasi baru' + : 'Ubah detail konfigurasi'} + + +
+
+ +
+ + setConfigurationForm({ + ...configurationForm, + date: e, + }) + } + disabled={loading} + placeholder='Pilih tanggal' + formatDisplay={formatDateForDisplay} + /> +
+
+ +
+ +
+ +
+ + +
+ + + {'<='} + + + setConfigurationForm({ + ...configurationForm, + percentage_threshold_bad: e.target.value, + }) + } + placeholder='Kurang' + className='w-20' + disabled={loading} + max={100} + /> +
+
+ +
+ + +
+ + + {'<='} + + + setConfigurationForm({ + ...configurationForm, + percentage_threshold_enough: e.target.value, + }) + } + placeholder='Cukup' + className='w-20' + disabled={loading} + min={Number(configurationForm.percentage_threshold_bad) + 1} + max={100} + /> +
+
+ +
+ + +
+ + + {'<='} + + +
+
+
+ + + + +
+
+ + {/* Delete Confirmation */} + + + + Hapus konfigurasi? + + Data konfigurasi akan dihapus secara permanen. + + + + Batal + + {loading ? 'Menghapus...' : 'Hapus'} + + + + +
+ ); +} diff --git a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx index c9562971..f8b67e7a 100644 --- a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx +++ b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx @@ -283,10 +283,6 @@ export function MasterEmployeeContent() { } }; - const handleExport = (format: string) => { - toast.success(`Data berhasil diekspor ke ${format}`); - }; - if (isLoadingEmployees && !employees) { return (
@@ -390,27 +386,6 @@ export function MasterEmployeeContent() { {/* RIGHT: Export + Add */}
- - - - - - handleExport('CSV')}> - Export CSV - - handleExport('Excel')}> - Export Excel - - - -
diff --git a/src/figma-make/lib/info.tsx b/src/figma-make/lib/info.tsx deleted file mode 100644 index 3127f912..00000000 --- a/src/figma-make/lib/info.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: delete this file later - -/* AUTOGENERATED FILE - DO NOT EDIT CONTENTS */ - -export const projectId = 'xxx'; -export const publicAnonKey = 'xxx'; diff --git a/src/figma-make/lib/supabase.ts b/src/figma-make/lib/supabase.ts deleted file mode 100644 index 0b693389..00000000 --- a/src/figma-make/lib/supabase.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js'; -import { projectId, publicAnonKey } from '@/figma-make/lib/info'; - -// ============================================ -// 🔍 SUPABASE ENVIRONMENT DEBUG CHECK -// ============================================ - -/** - * Get environment variable from multiple sources - * Checks in order: __ENV__, window.__ENV__, process.env, import.meta.env - */ -function getEnv(key: string): string | undefined { - let value: string | undefined; - let source: string | undefined; - - // Check globalThis.__ENV__ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((globalThis as any).__ENV__?.[key]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (globalThis as any).__ENV__[key]; - source = 'globalThis.__ENV__'; - } - // Check window.__ENV__ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - else if (typeof window !== 'undefined' && (window as any).__ENV__?.[key]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (window as any).__ENV__[key]; - source = 'window.__ENV__'; - } - // Check process.env - // eslint-disable-next-line @typescript-eslint/no-explicit-any - else if ((globalThis as any).process?.env?.[key]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (globalThis as any).process.env[key]; - source = 'process.env'; - } - // Check import.meta.env (if available) - else if ( - typeof import.meta !== 'undefined' && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (import.meta as any)?.env?.[key] - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value = (import.meta as any).env[key]; - source = 'import.meta.env'; - } - - if (value && source) { - console.log(`✅ ${key} loaded from: ${source}`); - } - - return value; -} - -// Try to read from environment variables first -let supabaseUrl = getEnv('VITE_SUPABASE_URL'); -let supabaseAnonKey = getEnv('VITE_SUPABASE_ANON_KEY'); - -// Fallback to Figma Make autogenerated credentials -if (!supabaseUrl || !supabaseAnonKey) { - console.log( - '📋 Using Figma Make autogenerated Supabase credentials from /utils/supabase/info.tsx' - ); - supabaseUrl = `https://${projectId}.supabase.co`; - supabaseAnonKey = publicAnonKey; -} - -// Helper function to mask sensitive data -const maskString = (str: string | undefined): string => { - if (!str) return 'undefined'; - if (str.length <= 20) return str.substring(0, 10) + '...'; - return str.substring(0, 20) + '...' + `(${str.length - 20} chars masked)`; -}; - -// Debug logging -console.group('🔍 Supabase Environment Check'); -console.log('projectId (from info.tsx):', projectId); -console.log('SUPABASE_URL present?', !!supabaseUrl); -console.log('SUPABASE_KEY present?', !!supabaseAnonKey); -console.log('SUPABASE_URL value:', maskString(supabaseUrl)); -console.log('SUPABASE_KEY value:', maskString(supabaseAnonKey)); -console.groupEnd(); - -// Check if Supabase is configured -export const isSupabaseConfigured = () => { - return !!(supabaseUrl && supabaseAnonKey); -}; - -// Create Supabase client or throw error -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let supabase: SupabaseClient; - -if (isSupabaseConfigured()) { - console.log('✅ Creating real Supabase client...'); - supabase = createClient(supabaseUrl!, supabaseAnonKey!); - console.log('✅ Supabase client created successfully!'); -} else { - const errorMessage = ` -❌ SUPABASE CONFIGURATION ERROR ❌ - -Missing required environment variables: -- VITE_SUPABASE_URL: ${!!supabaseUrl ? '✅ Present' : '❌ Missing'} -- VITE_SUPABASE_ANON_KEY: ${!!supabaseAnonKey ? '✅ Present' : '❌ Missing'} - -Please set Supabase environment variables in: -→ Figma Make Supabase integration settings -→ Deployment settings/environment configuration - -The app checked the following sources: -- globalThis.__ENV__ -- window.__ENV__ -- process.env -- import.meta.env - -None of these sources contained the required variables. - `.trim(); - - console.error(errorMessage); - throw new Error(errorMessage); -} - -export { supabase }; - -// Database types -export interface Database { - public: { - Tables: { - kandang: { - Row: { - id: string; - name: string; - created_at?: string; - }; - Insert: { - id?: string; - name: string; - created_at?: string; - }; - Update: { - id?: string; - name?: string; - created_at?: string; - }; - }; - employees: { - Row: { - id: string; - name: string; - kandang_id: string; - is_active: boolean; - created_at?: string; - }; - Insert: { - id?: string; - name: string; - kandang_id: string; - is_active?: boolean; - created_at?: string; - }; - Update: { - id?: string; - name?: string; - kandang_id?: string; - is_active?: boolean; - created_at?: string; - }; - }; - phases: { - Row: { - id: string; - name: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - name: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - name?: string; - created_at?: string; - updated_at?: string; - }; - }; - phase_activities: { - Row: { - id: string; - phase_id: string; - name: string; - description?: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - phase_id: string; - name: string; - description?: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - phase_id?: string; - name?: string; - description?: string; - created_at?: string; - updated_at?: string; - }; - }; - checklists: { - Row: { - id: string; - name: string; - description?: string; - phase_id: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - name: string; - description?: string; - phase_id: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - name?: string; - description?: string; - phase_id?: string; - created_at?: string; - updated_at?: string; - }; - }; - daily_checklists: { - Row: { - id: string; - date: string; - kandang_id: string; - checklist_id: string; - category: string; - status: string; - name?: string; - total_score?: number; - document_path?: string; - reject_reason?: string; - created_by: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - date: string; - kandang_id: string; - checklist_id: string; - category: string; - status?: string; - name?: string; - total_score?: number; - document_path?: string; - reject_reason?: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - date?: string; - kandang_id?: string; - checklist_id?: string; - category?: string; - status?: string; - name?: string; - total_score?: number; - document_path?: string; - reject_reason?: string; - created_at?: string; - updated_at?: string; - }; - }; - daily_checklist_tasks: { - Row: { - id: string; - checklist_id: string; - activity_id: string; - notes?: string; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - checklist_id: string; - activity_id: string; - notes?: string; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - checklist_id?: string; - activity_id?: string; - notes?: string; - created_at?: string; - updated_at?: string; - }; - }; - task_assignees: { - Row: { - id: string; - task_id: string; - employee_id: string; - is_completed: boolean; - created_at?: string; - updated_at?: string; - }; - Insert: { - id?: string; - task_id: string; - employee_id: string; - is_completed?: boolean; - created_at?: string; - updated_at?: string; - }; - Update: { - id?: string; - task_id?: string; - employee_id?: string; - is_completed?: boolean; - created_at?: string; - updated_at?: string; - }; - }; - }; - }; -} diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index c1f0db62..892fc88e 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -37,6 +37,23 @@ export class ClosingApiService extends BaseApiService { } } + async getPenjualanByKandang( + closingId: number, + kandangId: number + ): Promise | undefined> { + try { + const path = `${this.basePath}/${closingId}/${kandangId}/penjualan`; + return await httpClient>(path, { + method: 'GET', + }); + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + async getAllIncomingSapronakFetcher( endpoint: string ): Promise> { @@ -92,10 +109,11 @@ export class ClosingApiService extends BaseApiService { } async getPerhitunganSapronak( - id: number + id: number, + projectKandangId?: number ): Promise | undefined> { try { - const path = `${this.basePath}/${id}/perhitungan_sapronak`; + const path = `${this.basePath}/${id}${projectKandangId ? `/${projectKandangId}` : ''}/perhitungan_sapronak`; return await httpClient>( path, { @@ -161,6 +179,23 @@ export class ClosingApiService extends BaseApiService { return undefined; } } + + async getHppEkspedisiByKandang( + closingId: number, + kandangId: number + ): Promise | undefined> { + try { + const path = `${this.basePath}/${closingId}/${kandangId}/expedition-hpp`; + return await httpClient>(path, { + method: 'GET', + }); + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } } export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/services/api/daily-checklist/configuration.ts b/src/services/api/daily-checklist/configuration.ts new file mode 100644 index 00000000..27331aee --- /dev/null +++ b/src/services/api/daily-checklist/configuration.ts @@ -0,0 +1,19 @@ +import { BaseApiService } from '@/services/api/base'; +import { + CreateDailyChecklistConfigurationPayload, + DailyChecklistConfiguration, + UpdateDailyChecklistConfigurationPayload, +} from '@/types/api/daily-checklist/configuration'; + +export class DailyChecklistConfigurationApiService extends BaseApiService< + DailyChecklistConfiguration, + CreateDailyChecklistConfigurationPayload, + UpdateDailyChecklistConfigurationPayload +> { + constructor(basePath: string = '/master-data/config-checklists') { + super(basePath); + } +} + +export const DailyChecklistConfigurationApi = + new DailyChecklistConfigurationApiService('/master-data/config-checklists'); diff --git a/src/services/api/daily-checklist/daily-checklist.ts b/src/services/api/daily-checklist/daily-checklist.ts index 48b789a8..b8f72201 100644 --- a/src/services/api/daily-checklist/daily-checklist.ts +++ b/src/services/api/daily-checklist/daily-checklist.ts @@ -1,13 +1,18 @@ import axios from 'axios'; +import * as XLSX from 'xlsx'; import { BaseApiService } from '@/services/api/base'; -import { httpClient } from '@/services/http/client'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { CreateDailyChecklistPayload, DailyChecklist, + DailyChecklistReport, DetailDailyChecklist, } from '@/types/api/daily-checklist/daily-checklist'; +import { isResponseError } from '@/lib/api-helper'; +import { toast } from 'sonner'; +import { formatDate } from '@/lib/helper'; export class DailyChecklistApiService extends BaseApiService< DailyChecklist, @@ -134,15 +139,26 @@ export class DailyChecklistApiService extends BaseApiService< } } - async submit(id: string) { + async submit( + id: string, + files: File[] = [], + deletedDocumentIds: number[] = [] + ) { try { + const formData = new FormData(); + + formData.append('status', 'SUBMITTED'); + + formData.append('reject_reason', ''); + + files.forEach((file) => formData.append(`documents`, file)); + + formData.append('deleted_document_ids', deletedDocumentIds.join(',')); + const submitPath = `${this.basePath}/${id}`; const submitRes = await httpClient(submitPath, { method: 'PATCH', - body: { - status: 'SUBMITTED', - reject_reason: '', - }, + body: formData, }); return submitRes; @@ -156,13 +172,16 @@ export class DailyChecklistApiService extends BaseApiService< async approve(id: string) { try { + const formData = new FormData(); + + formData.append('status', 'APPROVED'); + + formData.append('reject_reason', ''); + const approvePath = `${this.basePath}/${id}`; const approveRes = await httpClient(approvePath, { method: 'PATCH', - body: { - status: 'APPROVED', - reject_reason: '', - }, + body: formData, }); return approveRes; @@ -176,13 +195,16 @@ export class DailyChecklistApiService extends BaseApiService< async reject(id: string, rejectReason: string) { try { + const formData = new FormData(); + + formData.append('status', 'REJECTED'); + + formData.append('reject_reason', rejectReason); + const rejectPath = `${this.basePath}/${id}`; const rejectRes = await httpClient(rejectPath, { method: 'PATCH', - body: { - status: 'REJECTED', - reject_reason: rejectReason, - }, + body: formData, }); return rejectRes; @@ -193,6 +215,111 @@ export class DailyChecklistApiService extends BaseApiService< return undefined; } } + + async uploadImage( + id: number, + status: string, + files: File[], + deletedDocumentIds: number[] = [] + ) { + try { + const formData = new FormData(); + + formData.append('status', status); + + files.forEach((file) => formData.append(`documents`, file)); + + formData.append('deleted_document_ids', deletedDocumentIds.join(',')); + + const uploadImagePath = `${this.basePath}/${id}`; + const uploadImageRes = await httpClient( + uploadImagePath, + { + method: 'PATCH', + body: formData, + } + ); + + return uploadImageRes; + } catch (error) { + if (axios.isAxiosError(error)) { + return error.response?.data; + } + return undefined; + } + } + + async exportDailyChecklistReportToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('limit', '2000'); + + const queryString = `?${params.toString()}`; + + try { + const dailyMarketingsReport = await httpClientFetcher< + BaseApiResponse + >(`${this.basePath}/report${queryString}`); + + if (isResponseError(dailyMarketingsReport)) { + toast.error('Gagal melakukan export daily checklist! Coba lagi.'); + return; + } + + const currentMonthMaxDay = new Date( + Number(params.get('tahun')), + Number(params.get('bulan')), + 0 + ).getDate(); + + const rows = dailyMarketingsReport.data; + + const formattedRows = []; + + for (let i = 0; i < rows.length; i++) { + const formattedData: Record = { + Area: rows[i].area.name, + Farm: rows[i].farm.name, + Kandang: rows[i].kandang.name, + ABK: rows[i].abk.name, + Phase: rows[i].phase, + }; + + // Add day + for (let j = 1; j <= currentMonthMaxDay; j++) { + formattedData[`Day ${j}`] = rows[i].daily_activities[`${j}`]; + } + + // add summary + formattedData['Total Checklist'] = rows[i].summary.total_checklist; + formattedData['Jumlah Hari Efektif'] = + rows[i].summary.jumlah_hari_efektif; + formattedData['ABK %'] = rows[i].summary.abk_percentage; + formattedData['Kandang %'] = rows[i].summary.kandang_percentage; + formattedData['Kategori Kurang'] = rows[i].summary.kategori.kurang; + formattedData['Kategori Cukup'] = rows[i].summary.kategori.cukup; + formattedData['Kategori Baik'] = rows[i].summary.kategori.baik; + + formattedRows.push(formattedData); + } + + const ws = XLSX.utils.json_to_sheet(formattedRows); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet( + wb, + ws, + `Daily Checklist ${params.get('tahun')}-${params.get('bulan')?.slice(0, 3)}` + ); + + // triggers download in browser + XLSX.writeFile( + wb, + `laporan-daily-checklist-${params.get('tahun')}-${params.get('bulan')}.xlsx` + ); + } catch (error) { + toast.error('Gagal melakukan export daily checklist! Coba lagi.'); + } + } } export const DailyChecklistApi = new DailyChecklistApiService( diff --git a/src/services/api/dashboard.ts b/src/services/api/dashboard.ts index d45ebbde..5cfc9b8f 100644 --- a/src/services/api/dashboard.ts +++ b/src/services/api/dashboard.ts @@ -1,13 +1,9 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; -import { DashboardProduction } from '@/types/api/dashboard/dashboard-production'; -import { getDummySingle } from '@/dummy/dashboard/dashboard.production.dummy'; +import { Dashboard } from '@/types/api/dashboard/dashboard'; +import { httpClientFetcher } from '@/services/http/client'; -class DashboardService extends BaseApiService< - DashboardProduction, - unknown, - unknown -> { +class DashboardService extends BaseApiService { constructor(basePath: string) { super(basePath); } @@ -16,19 +12,14 @@ class DashboardService extends BaseApiService< * Fetch dashboard production data * @param endpoint - The endpoint URL with query parameters * @returns Promise with BaseApiResponse containing DashboardProduction - * - * Note: Currently using dummy data. When real API is ready, - * uncomment the line below and remove getDummySingle() call: - * return await this.customRequest>(endpoint); */ async getDashboardProductionFetcher( endpoint: string - ): Promise> { - // For now, we're using dummy data regardless of the endpoint - // The endpoint parameter is kept for future API integration - console.log('Fetching dashboard data with endpoint:', endpoint); - return await getDummySingle(); + ): Promise | undefined> { + return await httpClientFetcher>( + `${endpoint ? endpoint : this.basePath}` + ); } } -export const DashboardApi = new DashboardService('/dashboard'); +export const DashboardApi = new DashboardService('/dashboards'); diff --git a/src/services/api/expense.ts b/src/services/api/expense.ts index 70e0e339..2a2fb1a7 100644 --- a/src/services/api/expense.ts +++ b/src/services/api/expense.ts @@ -169,13 +169,13 @@ export class ExpenseApiService extends BaseApiService< } } - async approveManager( + async approveHeadArea( id: number, notes?: string ): Promise | undefined> { try { const approveRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, { method: 'POST', body: { @@ -196,13 +196,67 @@ export class ExpenseApiService extends BaseApiService< } } - async bulkApproveManager( + async bulkApproveHeadArea( ids: number[], notes?: string ): Promise | undefined> { try { const bulkApproveRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, + { + method: 'POST', + body: { + action: 'APPROVED', + approvable_ids: ids, + notes: notes, + }, + } + ); + + return bulkApproveRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async approveUnitVicePresident( + id: number, + notes?: string + ): Promise | undefined> { + try { + const approveRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, + { + method: 'POST', + body: { + action: 'APPROVED', + approvable_ids: [id], + notes: notes, + }, + } + ); + + return approveRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async bulkApproveUnitVicePresident( + ids: number[], + notes?: string + ): Promise | undefined> { + try { + const bulkApproveRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, { method: 'POST', body: { @@ -277,13 +331,13 @@ export class ExpenseApiService extends BaseApiService< } } - async rejectManager( + async rejectHeadArea( id: number, notes?: string ): Promise | undefined> { try { const rejectRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, { method: 'POST', body: { @@ -304,13 +358,67 @@ export class ExpenseApiService extends BaseApiService< } } - async bulkRejectManager( + async bulkRejectHeadArea( ids: number[], notes?: string ): Promise | undefined> { try { const bulkRejectRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, + { + method: 'POST', + body: { + action: 'REJECTED', + approvable_ids: ids, + notes: notes, + }, + } + ); + + return bulkRejectRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async rejectUnitVicePresident( + id: number, + notes?: string + ): Promise | undefined> { + try { + const rejectRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, + { + method: 'POST', + body: { + action: 'REJECTED', + approvable_ids: [id], + notes: notes, + }, + } + ); + + return rejectRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async bulkRejectUnitVicePresident( + ids: number[], + notes?: string + ): Promise | undefined> { + try { + const bulkRejectRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, { method: 'POST', body: { diff --git a/src/services/api/report/debt-supplier.ts b/src/services/api/report/debt-supplier.ts new file mode 100644 index 00000000..dad46d18 --- /dev/null +++ b/src/services/api/report/debt-supplier.ts @@ -0,0 +1,39 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DebtSupplier } from '@/types/api/report/debt-supplier'; + +export class DebtSupplierApiService extends BaseApiService< + DebtSupplier, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getDebtSupplierReport( + supplier_ids?: string, + filter_by?: string, + start_date?: string, + end_date?: string, + page?: number, + limit?: number + ): Promise | undefined> { + return await this.customRequest>( + `debt-supplier`, + { + method: 'GET', + params: { + supplier_ids: supplier_ids, + filter_by: filter_by, + start_date: start_date, + end_date: end_date, + page: page, + limit: limit, + }, + } + ); + } +} + +export const DebtSupplierApi = new DebtSupplierApiService('reports'); diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts new file mode 100644 index 00000000..e8ec52c8 --- /dev/null +++ b/src/services/api/report/finance-report.ts @@ -0,0 +1,46 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; +import { DebtSupplier } from '@/types/api/report/debt-supplier'; + +export class FinanceApiService extends BaseApiService< + CustomerPaymentReport, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getCustomerPaymentReport( + customer_id?: string, + sales?: string, + filter_by?: 'do_date', + start_date?: string, + end_date?: string, + page?: number, + limit?: number + ): Promise | undefined> { + return await this.customRequest>( + `customer-payment`, + { + method: 'GET', + params: { + customer_id: customer_id, + sales: sales, + filter_by: filter_by, + start_date: start_date, + end_date: end_date, + page: page, + limit: limit, + }, + } + ); + } +} + +// export const FinanceApi = new FinanceApiService('reports'); + +export const FinanceApi = new FinanceApiService( + 'http://localhost:4010/api/reports/finance' +); diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts index 5d81605e..f55336ac 100644 --- a/src/services/api/report/marketing-report.ts +++ b/src/services/api/report/marketing-report.ts @@ -2,11 +2,14 @@ import * as XLSX from 'xlsx'; import toast from 'react-hot-toast'; import { BaseApiService } from '@/services/api/base'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; -import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { + DailyMarketingReport, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { formatDate, sleep } from '@/lib/helper'; +import { formatDate } from '@/lib/helper'; export class MarketingReportApiService extends BaseApiService< DailyMarketingReport, @@ -19,10 +22,8 @@ export class MarketingReportApiService extends BaseApiService< async getAllDailyMarketingFetcher( endpoint: string - ): Promise> { - return await httpClientFetcher>( - endpoint - ); + ): Promise { + return await httpClientFetcher(endpoint); } async exportDailyMarketingToExcel(initialQueryString: string) { @@ -42,16 +43,19 @@ export class MarketingReportApiService extends BaseApiService< return; } - const rows = dailyMarketingsReport.data.rows; + const rows = dailyMarketingsReport.data; const formattedRows = []; for (let i = 0; i < rows.length; i++) { formattedRows.push({ ...rows[i], - created_user: rows[i].created_user.name, - created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'), - updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'), + // created_user: rows[i].created_user.name, + // created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'), + // updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'), + so_date: formatDate(rows[i].so_date, 'YYYY-MM-DD'), + realization_date: formatDate(rows[i].realization_date, 'YYYY-MM-DD'), + sales: rows[i].sales.name, warehouse: rows[i].warehouse.name, customer: rows[i].customer.name, product: rows[i].product.name, diff --git a/src/services/hooks/useFormikErrorList.ts b/src/services/hooks/useFormikErrorList.ts new file mode 100644 index 00000000..9d299322 --- /dev/null +++ b/src/services/hooks/useFormikErrorList.ts @@ -0,0 +1,62 @@ +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import { FormikProps } from 'formik'; +import { useState } from 'react'; + +interface UseFormikErrorListOptions { + onBeforeSubmit?: (e: React.FormEvent) => boolean | void; + onAfterValidation?: () => void | Promise; +} + +export const useFormikErrorList = ( + formik: FormikProps, + options?: UseFormikErrorListOptions +) => { + const [formErrorList, setFormErrorList] = useState([]); + + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return false; + } + + return true; + }; + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Call onBeforeSubmit callback + if (options?.onBeforeSubmit) { + const shouldContinue = options.onBeforeSubmit(e); + if (shouldContinue === false) { + return; // Cancel submit + } + } + + // Validate form + const isValid = await handleValidateForm(); + + // Call onAfterValidation callback if validation passed + if (options?.onAfterValidation) { + await options.onAfterValidation(); + } + + // Submit form + formik.handleSubmit(); + }; + + const close = () => { + setFormErrorList([]); + }; + + return { + formErrorList, + setFormErrorList, + close, + handleValidateForm, + handleFormSubmit, + }; +}; diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index 5b9f04e3..d3deb616 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -116,3 +116,10 @@ export type BaseGroupedApproval = { export type Approvals = BaseApiResponse; export type GroupedApprovals = BaseApiResponse; + +export type Document = { + id: number; + name: string; + size: number; + url: string; +}; diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 95d1526d..56406ada 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -63,6 +63,7 @@ export type BaseClosing = { location_id: number; location_name: string; project_category: 'GROWING' | 'LAYING'; + project_type?: 'GROWING' | 'LAYING'; // berubah dari BE? period: number; closing_date?: string; shed_label: string; @@ -185,7 +186,6 @@ export type ClosingSapronakCalculation = { doc: ClosingSapronakCalculationItem; ovk: ClosingSapronakCalculationItem; pakan: ClosingSapronakCalculationItem; - pullet: ClosingSapronakCalculationItem; }; // ====== OVERHEAD ====== diff --git a/src/types/api/daily-checklist/configuration.d.ts b/src/types/api/daily-checklist/configuration.d.ts new file mode 100644 index 00000000..7e94c69b --- /dev/null +++ b/src/types/api/daily-checklist/configuration.d.ts @@ -0,0 +1,22 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseConfiguration = { + id: number; + date: string; + percentage_threshold_bad: number; + percentage_threshold_enough: number; +}; + +export type DailyChecklistConfiguration = BaseMetadata & BaseConfiguration; + +export type CreateDailyChecklistConfigurationPayload = { + date: string; + percentage_threshold_bad: number; + percentage_threshold_enough: number; +}; + +export type UpdateDailyChecklistConfigurationPayload = { + date: string; + percentage_threshold_bad: number; + percentage_threshold_enough: number; +}; diff --git a/src/types/api/daily-checklist/daily-checklist.d.ts b/src/types/api/daily-checklist/daily-checklist.d.ts index 9f01ae1f..5e5a3fe8 100644 --- a/src/types/api/daily-checklist/daily-checklist.d.ts +++ b/src/types/api/daily-checklist/daily-checklist.d.ts @@ -1,7 +1,10 @@ -import { BaseMetadata } from '@/types/api/api-general'; +import { BaseMetadata, Document } from '@/types/api/api-general'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { Phase } from '@/types/api/daily-checklist/phase'; import { PhaseActivity } from '@/types/api/daily-checklist/phase-activity'; +import { BaseArea } from '@/types/api/master-data/area'; +import { BaseLocation } from '@/types/api/master-data/location'; +import { BaseEmployee } from '@/types/api/master-data/employee'; export type BaseDailyChecklist = { id: number; @@ -46,6 +49,7 @@ export type DetailDailyChecklist = BaseDailyChecklist & { id: number; name: string; }[]; + document_urls: Document[]; }; export type CreateDailyChecklistPayload = { @@ -54,3 +58,49 @@ export type CreateDailyChecklistPayload = { category: string; status: string; }; + +export type PerformanceOverviewItem = { + employee_id: number; + employee_name: string; + total_activity: number; + activity_done: number; + activity_left: number; + kandang: Pick; +}; + +export type TrackingAbkItem = { + employee_id: number; + employee_name: string; + kandang_id: number; + kandang_name: string; + total_activity: number; + activity_done: number; + activity_left: number; + completion_rate: number; + last_activity: string; +}; + +export type DailyChecklistSummary = { + performance_overview: PerformanceOverviewItem[]; + tracking_abk: TrackingAbkItem[]; +}; + +export type DailyChecklistReport = { + area: Pick; + farm: Pick; + kandang: Pick; + abk: Pick; + phase: string; + daily_activities: Record; + summary: { + total_checklist: number; + jumlah_hari_efektif: number; + abk_percentage: number; + kandang_percentage: number; + kategori: { + kurang: number; + cukup: number; + baik: number; + }; + }; +}; diff --git a/src/types/api/dashboard/dashboard-production.d.ts b/src/types/api/dashboard/dashboard-production.d.ts deleted file mode 100644 index 5d873806..00000000 --- a/src/types/api/dashboard/dashboard-production.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -export interface DashboardProduction { - statistics_data: DashboardProductionStatisticsData[]; - production_charts: DashboardProductionProductionCharts[]; - standard_productions: DashboardProductionStandardProductions[]; - egg_weights: DashboardProductionEggWeights[]; - fcr_data: DashboardProductionFcrData[]; -} - -export interface DashboardProductionFcrData { - flock: DashboardProductionFcrDataFlock; - fcr: number; -} - -export interface DashboardProductionEggWeights { - flock: DashboardProductionFcrDataFlock; - weight: number; -} - -export interface DashboardProductionStandardProductions { - week: number; - standards: DashboardProductionStandardProductionsStandards[]; - flocks: DashboardProductionProductionChartsFlocks[]; -} - -export interface DashboardProductionProductionCharts { - date: string; - flocks: DashboardProductionProductionChartsFlocks[]; -} - -export interface DashboardProductionStatisticsData { - title: string; - value: number; - change: number; - period: string; - changeType: string; -} - -export interface DashboardProductionFcrDataFlock { - id: number; - name: string; -} - -export interface DashboardProductionStandardProductionsStandards { - name: string; - value: number; -} - -export interface DashboardProductionProductionChartsFlocks { - id: number; - name: string; - data: number; -} diff --git a/src/types/api/dashboard/dashboard.d.ts b/src/types/api/dashboard/dashboard.d.ts new file mode 100644 index 00000000..ec3dafdb --- /dev/null +++ b/src/types/api/dashboard/dashboard.d.ts @@ -0,0 +1,61 @@ +import { SuccessApiResponse } from '@/types/api/api-general'; + +export interface Dashboard { + statistics_data: DashboardStatisticsData[]; + charts: DashboardComparisonCharts | DashboardOverviewCharts; +} + +export interface DashboardComparisonCharts { + location: DashboardCharts; + flock: DashboardCharts; + kandang: DashboardCharts; +} + +export interface DashboardOverviewCharts { + body_weight: DashboardCharts; + performance: DashboardCharts; + fcr: DashboardCharts; + quality_control: DashboardCharts; + deplesi: DashboardCharts; +} + +export interface DashboardCharts { + series: DashboardChartsSeries[]; + dataset: DashboardChartsDataset[]; +} + +export interface DashboardStatisticsData { + label: string; + value: number; + percent_last_month: number; +} + +export interface DashboardChartsDataset { + week: number; + // Index signature to support dynamic keys (series IDs) in comparison mode + [key: string | number]: number | undefined; +} + +export interface DashboardChartsSeries { + id: string | number; + label: string; + unit: string; +} + +export interface DashboardFilter { + start_date: string; + end_date: string; + analysis_mode: 'OVERVIEW' | 'COMPARISON'; + location_ids: number[]; + comparison_type?: string | undefined; + flock_ids: number[]; + kandang_ids: number[]; +} + +export interface DashboardMeta { + page: number; + limit: number; + total_pages: number; + total_results: number; + filters: DashboardFilter; +} diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 1aeb2005..66cc39ed 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -5,6 +5,7 @@ import { Kandang } from '@/types/api/master-data/kandang'; import { Location } from '@/types/api/master-data/location'; import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; import { Nonstock } from '@/types/api/master-data/nonstock'; +import { ProductionStandard } from '@/types/api/master-data/production-standard'; export type BaseProjectFlock = { id: number; diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 9cf9a625..1728516a 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -8,15 +8,15 @@ export type ProductionMetrics = { fcr_value: number; fcr_std?: number; total_chick_qty: number; - hand_day?: number; - hand_house?: number; + hen_day?: number; + hen_house?: number; feed_intake?: number; - egg_mesh?: number; - egg_weight?: number; - hand_day_std?: number; - hand_house_std?: number; feed_intake_std?: number; - egg_mesh_std?: number; + egg_mass?: number; + egg_weight?: number; + hen_day_std?: number; + hen_house_std?: number; + egg_mass_std?: number; egg_weight_std?: number; daily_gain?: number; avg_daily_gain?: number; diff --git a/src/types/api/report/customer-payment.d.ts b/src/types/api/report/customer-payment.d.ts new file mode 100644 index 00000000..bfa059c9 --- /dev/null +++ b/src/types/api/report/customer-payment.d.ts @@ -0,0 +1,42 @@ +import { BaseCustomer } from '@/types/api/master-data/customer'; +import { BaseMetadata } from '@/types/api/api-general'; + +export type CustomerPaymentRow = { + id: number; + do_date: string; + realization_date: string; + aging_day: number | null; + reference: string; + vehicle_plate: string[]; + qty: number; + weight: number; + average_weight: number; + price: number; + credit_note: number; + final_price: number; + ppn: number; + total: number; + payment: number; + accounts_receivable: number; + notes: string; + pickup_info: string; + sales_marketing: string; +}; + +export type CustomerPaymentSummary = { + total_qty: number; + total_weight: number; + total_initial_amount: number; + total_credit_note: number; + total_final_amount: number; + total_ppn: number; + total_grand_amount: number; + total_payment: number; + total_accounts_receivable: number; +}; + +export type CustomerPaymentReport = BaseMetadata & { + customer: BaseCustomer; + rows: CustomerPaymentRow[]; + summary: CustomerPaymentSummary; +}; diff --git a/src/types/api/report/debt-supplier.d.ts b/src/types/api/report/debt-supplier.d.ts new file mode 100644 index 00000000..46849599 --- /dev/null +++ b/src/types/api/report/debt-supplier.d.ts @@ -0,0 +1,35 @@ +import { Area } from '@/types/api/master-data/area'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Warehouse } from '@/types/api/master-data/warehouse'; + +export interface DebtSupplier { + supplier: Supplier; + initial_balance: number; + rows: DebtRow[]; + total: DebtTotal; +} + +export interface DebtTotal { + aging: number; + total_price: number; + payment_price: number; + debt_price: number; +} + +export interface DebtRow { + pr_number: string; + po_number: string; + po_date: string; + received_date: string; + aging: number; + area: Area; + warehouse: Warehouse; + due_date: string; + due_status: string; + total_price: number; + payment_price: number; + debt_price: number; + status: string; + travel_number: string; + balance: number; +} diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts index d1e81f77..4a0ab306 100644 --- a/src/types/api/report/marketing.d.ts +++ b/src/types/api/report/marketing.d.ts @@ -1,4 +1,4 @@ -import { BaseMetadata } from '@/types/api/api-general'; +import { BaseApiResponse, BaseMetadata } from '@/types/api/api-general'; import { BaseCustomer, Customer } from '@/types/api/master-data/customer'; import { BaseWarehouseArea, @@ -9,16 +9,17 @@ import { import { Location } from '@/types/api/master-data/location'; import { Area } from '@/types/api/master-data/area'; import { BaseProduct } from '@/types/api/master-data/product'; +import { BaseUser } from '@/types/api/user'; export type BaseDailyMarketingRow = { - no: number; - so_date: string; // e.g. "01-Dec-2025" - do_date: string; // e.g. "08-Dec-2025" + id: number; + so_date: string; + realization_date: string; aging_days: number; warehouse: BaseWarehouseArea | BaseWarehouseLocation | BaseWarehouseKandang; customer: BaseCustomer; - sales: string; + sales: BaseUser; product: BaseProduct; do_number: string; @@ -43,12 +44,13 @@ export interface SalesSummary { total_weight_kg: number; total_sales_amount: number; total_hpp_amount: number; + total_hpp_price_per_kg: number; } -export type DailyMarketingReport = { - rows: DailyMarketingRow[]; - summary: SalesSummary; -}; +export type DailyMarketingReport = DailyMarketingRow[]; + +export type DailyMarketingReportResponse = + BaseApiResponse & { total: SalesSummary }; export type MarketingReportFilters = { area_id?: number;