diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index f3a78d9d..f271700b 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -7,22 +7,37 @@ 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( + closingId ? `sales-${closingId}` : null, + () => ClosingApi.getPenjualan(Number(closingId)) + ); const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( closingId ? `hpp-ekspedisi-${closingId}` : null, @@ -44,8 +59,12 @@ const ClosingDetailPage = () => { return; } - const isLoading = isLoadingClosing || isLoadingHppEkspedisi; - // const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi; + const isLoading = + isLoadingClosing || + isLoadingSales || + isLoadingHppEkspedisi || + isLoadingProject || + isLoadingKandang; return (
@@ -55,12 +74,18 @@ const ClosingDetailPage = () => { )}
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/helper/form/FormErrors.tsx b/src/components/helper/form/FormErrors.tsx index a351227f..4b97d033 100644 --- a/src/components/helper/form/FormErrors.tsx +++ b/src/components/helper/form/FormErrors.tsx @@ -1,6 +1,7 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; +import { useState } from 'react'; /** * Alert Unique Error List @@ -14,8 +15,10 @@ const AlertErrorList = ({ formErrorList: string[]; onClose: () => 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/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 657c5e5c..82c58341 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) { @@ -255,8 +275,8 @@ const ExpenseRequestContent = ({ {/* TODO: apply RBAC */}
- {isCurrentApprovalOnManager && ( - + {isCurrentApprovalOnHeadArea && ( + + + )} + + {isCurrentApprovalOnUnitVicePresident && ( + + )} @@ -304,7 +338,8 @@ const ExpenseRequestContent = ({ {showRejectButton && ( @@ -454,8 +489,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/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..e224d0f0 --- /dev/null +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -0,0 +1,425 @@ +'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 || ''} + + + NPWP: {customerReport.customer_npwp || '-'} + + {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') + : '-'} + + + + {formatNumber(item.aging)} 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..3cc4d67a --- /dev/null +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -0,0 +1,115 @@ +'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 || 0), + Referensi: item.reference || '', + 'Nomor Polisi': item.vehicle_plate || '', + '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..42d27847 --- /dev/null +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -0,0 +1,363 @@ +'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 > Hutang Supplier + + + {supplierReport.supplier.name} + + + + {/* Table */} + + {/* Table Header */} + + + No + + + No. PR + + + No. PO + + + Tgl PR + + + Tgl PO + + + Aging + + + Area + + + Gudang + + + Tgl Jatuh Tempo + + + Status JT + + + Total Harga + + + Pembayaran + + + Hutang + + + Status + + + No. Perjalanan + + + + {/* Table Body */} + {supplierReport.rows.map((item, index) => ( + + + {index + 1} + + + {item.pr_number || '-'} + + + {item.po_number || '-'} + + + + {item.pr_date ? formatDate(item.pr_date, 'DD MMM YY') : '-'} + + + + + {item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-'} + + + + {formatNumber(item.aging)} Hari + + + {item.area?.name || '-'} + + + {item.warehouse?.name || '-'} + + + + {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..820ba7ec --- /dev/null +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as XLSX from 'xlsx'; +import { formatDate, formatCurrency, formatNumber } 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 PR': item.pr_date + ? formatDate(item.pr_date, 'DD MMM YYYY') + : '', + 'Tanggal PO': item.po_date + ? formatDate(item.po_date, 'DD MMM YYYY') + : '', + 'Aging (Hari)': formatNumber(item.aging || 0), + Area: item.area?.name || '', + Gudang: item.warehouse?.name || '', + 'Tanggal Jatuh Tempo': item.due_date + ? formatDate(item.due_date, 'DD MMM YYYY') + : '', + 'Status Jatuh Tempo': item.due_status || '', + 'Total Harga': formatCurrency(item.total_price || 0), + 'Harga Pembayaran': formatCurrency(item.payment_price || 0), + 'Harga Hutang': formatCurrency(item.debt_price || 0), + Status: item.status || '', + 'Nomor Perjalanan': item.travel_number || '', + }) + ); + + if (supplierReport.total) { + excelData.push({ + No: 'Total', + 'Nomor PR': '', + 'Nomor PO': '', + 'Tanggal PR': '', + 'Tanggal PO': '', + 'Aging (Hari)': formatNumber(supplierReport.total.aging || 0), + Area: '', + Gudang: '', + 'Tanggal Jatuh Tempo': '', + 'Status Jatuh Tempo': '', + 'Total Harga': formatCurrency(supplierReport.total.total_price || 0), + 'Harga Pembayaran': formatCurrency( + supplierReport.total.payment_price || 0 + ), + 'Harga Hutang': formatCurrency(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..f57f7335 --- /dev/null +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -0,0 +1,717 @@ +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', + cell: (props) => { + const value = props.row.original.aging; + return
{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/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/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/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..c8df2120 --- /dev/null +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -0,0 +1,608 @@ +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 { FinanceApi } from '@/services/api/report/finance-report'; +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'; + +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 [filterDataType, setFilterDataType] = useState(); + const [filterErrors, setFilterErrors] = useState>({}); + + const filterModal = useModal(); + + const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = + useSelect(SupplierApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + + const dataTypeOptions = useMemo( + () => [ + { value: 'do_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: filterDataType?.value || 'do_date', + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + page: currentPage, + limit: pageSize, + }; + + return ['debt-supplier-report', params]; + } + : null, + ([, params]) => + FinanceApi.getDebtSupplierReport( + params.supplier_ids, + params.filter_by?.toString(), + params.start_date, + params.end_date, + params.page, + params.limit + ) + ); + // const { data: debtSupplier, isLoading } = useSWR(FinanceApi.basePath, () => + // FinanceApi.getDebtSupplierReport() + // ); + + 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: 'do_date' as const, + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + date_type: filterDataType ? filterDataType.value : undefined, + limit: 100, + page: 1, + }; + + const response = await FinanceApi.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: 'pr_date', + header: 'Tanggal PR', + accessorKey: 'pr_date', + cell: (props) => { + const value = props.row.original.pr_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'po_date', + header: 'Tanggal PO', + accessorKey: 'po_date', + cell: (props) => { + const value = props.row.original.po_date; + return 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 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' }} + /> +
+ +
+ { + setFilterDataType(val ? (val as OptionType) : undefined); + }} + className={{ wrapper: 'w-full' }} + /> +
+
+ + {/* 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 e4409748..364f1824 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -133,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 9ebc566b..9a0c9d2e 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -118,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/services/api/closing.ts b/src/services/api/closing.ts index c1f0db62..8ea2faa5 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -92,10 +92,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, { 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/finance-report.ts b/src/services/api/report/finance-report.ts new file mode 100644 index 00000000..014b9fec --- /dev/null +++ b/src/services/api/report/finance-report.ts @@ -0,0 +1,66 @@ +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, + }, + } + ); + } + + 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 FinanceApi = new FinanceApiService('reports'); 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/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/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..776d640d --- /dev/null +++ b/src/types/api/report/customer-payment.d.ts @@ -0,0 +1,47 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { BaseCustomer } from '@/types/api/master-data/customer'; +import { BaseProduct } from '@/types/api/master-data/product'; + +export type CustomerPaymentRow = { + no: number; + do_date: string; + payment_date: string; + realization_date: string; + aging: number; + 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; + product?: BaseProduct; +}; + +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; + customer_npwp: string; + customer_address: string; + 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..f7342501 --- /dev/null +++ b/src/types/api/report/debt-supplier.d.ts @@ -0,0 +1,34 @@ +import { BaseMetadata } from '@/types/api/api-general'; +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 type DebtSupplier = BaseMetadata & { + supplier: Supplier; + rows: DebtRow[]; + total: DebtTotal; +}; + +export type DebtRow = { + pr_number: string; + po_number: string; + pr_date: string; + po_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; +}; + +export type DebtTotal = { + aging: number; + total_price: number; + payment_price: number; + debt_price: number; +};