From ab6ad7d7b1c4b50b40a185fcee7688463edf6949 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 5 Jun 2026 16:14:57 +0700 Subject: [PATCH] feat: migrate depreciation report to V2 API with daily breakdown view - Add V2 types (ReportDepreciationV2Item, DepreciationV2Meta, DepreciationV2Response) for the new per-day response shape - Add DepreciationReportV2Api service pointing to /reports/expense/v2/depreciation - Require projectFlock in filter (was optional); auto-open filter modal on first load when none is selected - Replace multi-card farm loop with a single project flock card showing farm_name and period only in the header - Replace kandang sub-table with daily depreciation rows: date, day_n, chickin_date, depreciation_value, pullet_cost_day_n_total, multiplication_percentage, total_value_pullet_after_depreciation - Add Total Hari (limit) NumberInput field (default 10) to filter modal; remove pagination - Switch storeName to report-depreciation-v2-table to avoid loading stale localStorage state Co-Authored-By: Claude Sonnet 4.6 --- .../tab/ReportDepreciationFilterModal.tsx | 44 ++- .../expense/tab/ReportDepreciationTab.tsx | 295 ++++++++++-------- src/services/api/report/expense-report.ts | 7 + src/types/api/report/report-expense.d.ts | 60 ++++ 4 files changed, 270 insertions(+), 136 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx index a334e137..8dac1f42 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx @@ -8,6 +8,7 @@ import { Icon } from '@iconify/react'; import Modal from '@/components/Modal'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; +import NumberInput from '@/components/input/NumberInput'; import SelectInput, { OptionType, useSelect, @@ -24,13 +25,20 @@ export type ReportDepreciationFilterValues = { location?: OptionType; projectFlock?: OptionType; period: string | null; + totalDays: number; }; export const ReportDepreciationFilterSchema = yup.object({ area: yup.mixed>().optional(), location: yup.mixed>().optional(), - projectFlock: yup.mixed>().optional(), + projectFlock: yup + .mixed>() + .required('Project Flock wajib dipilih'), period: yup.string().nullable().required('Periode wajib dipilih'), + totalDays: yup + .number() + .min(1, 'Minimal 1 hari') + .required('Total Hari wajib diisi'), }); interface ReportDepreciationFilterModalProps { @@ -47,6 +55,7 @@ const defaultInitialValues: ( location: undefined, projectFlock: undefined, period: initialValues?.period ?? null, + totalDays: initialValues?.totalDays ?? 10, }); const ReportDepreciationFilterModal = ({ @@ -196,6 +205,14 @@ const ReportDepreciationFilterModal = ({ isClearable isSearchable={true} className={{ wrapper: 'w-full' }} + isError={ + formik.touched.projectFlock && !!formik.errors.projectFlock + } + errorMessage={ + formik.touched.projectFlock + ? (formik.errors.projectFlock as string) + : undefined + } /> + + { + const val = Number(e.target.value); + formik.setFieldValue( + 'totalDays', + isNaN(val) || val < 1 ? 1 : Math.floor(val) + ); + }} + onBlur={formik.handleBlur} + decimalScale={0} + allowNegative={false} + thousandSeparator='' + isError={formik.touched.totalDays && !!formik.errors.totalDays} + errorMessage={ + formik.touched.totalDays + ? (formik.errors.totalDays as string) + : undefined + } + className={{ wrapper: 'w-full' }} + />
diff --git a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx index abab3df7..f6e34f95 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx @@ -1,12 +1,11 @@ 'use client'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import useSWR from 'swr'; import { ColumnDef } from '@tanstack/react-table'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; -import Pagination from '@/components/Pagination'; import Table from '@/components/Table'; import ButtonFilter from '@/components/helper/ButtonFilter'; import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; @@ -14,11 +13,14 @@ import { useModal } from '@/components/Modal'; import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; -import { ReportDepreciation } from '@/types/api/report/report-expense'; -import { DepreciationReportApi } from '@/services/api/report/expense-report'; +import { + DepreciationV2Response, + ReportDepreciationV2Item, +} from '@/types/api/report/report-expense'; +import { DepreciationReportV2Api } from '@/services/api/report/expense-report'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { OptionType } from '@/components/input/SelectInput'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { httpClientFetcher } from '@/services/http/client'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; interface ReportDepreciationTabProps { @@ -29,8 +31,6 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { const { state: tableFilterState, updateFilter, - setPage, - setPageSize, toQueryString: getTableFilterQueryString, reset: resetFilter, } = useTableFilter<{ @@ -38,78 +38,117 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { location?: OptionType; projectFlock?: OptionType; period: string; + totalDays: number; }>({ initial: { area: undefined, location: undefined, projectFlock: undefined, period: formatDate(Date.now(), 'YYYY-MM-DD'), + totalDays: 10, }, paramMap: { - pageSize: 'limit', area: 'area_id', location: 'location_id', projectFlock: 'project_flock_id', period: 'period', + totalDays: 'limit', }, persist: true, - storeName: 'report-depreciation-table', + storeName: 'report-depreciation-v2-table', }); - const { data: depreciationsResponse, isLoading: isLoadingDepreciations } = - useSWR( - `${DepreciationReportApi.basePath}${getTableFilterQueryString()}`, - DepreciationReportApi.getAllFetcher - ); + const swrKey = tableFilterState.projectFlock + ? `${DepreciationReportV2Api.basePath}${getTableFilterQueryString()}` + : null; - const depreciations = isResponseSuccess(depreciationsResponse) - ? depreciationsResponse.data - : []; + const { data: depreciationsResponse, isLoading: isLoadingDepreciations } = + useSWR(swrKey, httpClientFetcher); + + const depreciationMeta = + depreciationsResponse?.status === 'success' + ? depreciationsResponse.meta + : null; + const depreciationData = + depreciationsResponse?.status === 'success' + ? depreciationsResponse.data + : []; const filterModal = useModal(); const { ref: filterModalRef } = filterModal; + const initialOpenRef = useRef(false); + useEffect(() => { + if (!initialOpenRef.current) { + initialOpenRef.current = true; + if (!tableFilterState.projectFlock) { + filterModal.openModal(); + } + } + }, []); + const setTabActions = useTabActionsStore((state) => state.setTabActions); const clearTabActions = useTabActionsStore((state) => state.clearTabActions); - const depreciationKandangColumns: ColumnDef< - ReportDepreciation['components']['kandang'][0] - >[] = [ - { - accessorKey: 'kandang_name', - header: 'Kandang', - }, - { - accessorKey: 'house_type', - header: 'Tipe Kandang', - cell: ({ row }) => row.original.house_type.toUpperCase(), - }, - { - accessorKey: 'depreciation_percent', - header: 'Persentase Depresiasi', - cell: ({ row }) => row.original.depreciation_percent + '%', - }, - { - accessorKey: 'depreciation_value', - header: 'Nilai Depresiasi', - cell: ({ row }) => formatCurrency(row.original.depreciation_value), - }, - { - accessorKey: 'depreciation_source', - header: 'Asal Depresiasi', - cell: ({ row }) => row.original.depreciation_source.toUpperCase(), - }, - { - accessorKey: 'cutover_date', - header: 'Tanggal Cutover', - cell: ({ row }) => formatDate(row.original.cutover_date, 'DD MMM YYYY'), - }, - { - accessorKey: 'origin_date', - header: 'Tanggal Origin', - cell: ({ row }) => formatDate(row.original.origin_date, 'DD MMM YYYY'), - }, - ]; + const depreciationColumns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'date', + header: 'Tanggal', + cell: ({ row }) => formatDate(row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'day_n', + header: 'Hari ke-', + }, + { + accessorKey: 'chickin_date', + header: 'Tanggal Chick-in', + cell: ({ row }) => formatDate(row.original.chickin_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'depreciation_value', + header: 'Nilai Depresiasi', + cell: ({ row }) => formatCurrency(row.original.depreciation_value), + }, + { + accessorKey: 'pullet_cost_day_n_total', + header: 'Total Harga Pullet Hari ke-N', + cell: ({ row }) => + formatCurrency( + row.original.pullet_cost_day_n_total, + 'IDR', + 'id-ID', + 0, + 10 + ), + }, + { + accessorKey: 'multiplication_percentage', + header: 'Persentase Multiplikasi', + cell: ({ row }) => + formatNumber( + row.original.multiplication_percentage * 100, + 'en-US', + 0, + 4 + ) + '%', + }, + { + accessorKey: 'total_value_pullet_after_depreciation', + header: 'Total Nilai Pullet Setelah Depresiasi', + cell: ({ row }) => + formatCurrency( + row.original.total_value_pullet_after_depreciation, + 'IDR', + 'id-ID', + 0, + 10 + ), + }, + ], + [] + ); const tabActionsElement = useMemo( () => ( @@ -145,9 +184,9 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => {
)} - {!isLoadingDepreciations && depreciations.length === 0 && ( + {!isLoadingDepreciations && !tableFilterState.projectFlock && ( { height={20} /> } - title='Data Not Yet Available' - subtitle='Please change your filters to get the data.' + title='Pilih Project Flock' + subtitle='Silakan pilih Project Flock pada filter untuk melihat data depresiasi.' /> )} - {!isLoadingDepreciations && depreciations.length > 0 && ( - <> - {depreciations.map((depreciationItem, idx) => ( - - - - ))} - - setPage(tableFilterState.page - 1)} - onNextPage={() => setPage(tableFilterState.page + 1)} - onPageChange={setPage} - rowOptions={[10, 20, 50, 100]} - onRowChange={setPageSize} + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' /> - - )} + )} + + {!isLoadingDepreciations && + depreciationData.length > 0 && + depreciationMeta && ( + +
+ + )} { values.period ? formatDate(values.period, 'YYYY-MM-DD') : '', true ); + updateFilter('totalDays', values.totalDays ?? 10, true); }} /> diff --git a/src/services/api/report/expense-report.ts b/src/services/api/report/expense-report.ts index a1d362ac..af1345ca 100644 --- a/src/services/api/report/expense-report.ts +++ b/src/services/api/report/expense-report.ts @@ -4,6 +4,7 @@ import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { ReportDepreciation, + ReportDepreciationV2Item, ReportExpense, } from '@/types/api/report/report-expense'; @@ -57,3 +58,9 @@ export const DepreciationReportApi = new BaseApiService< unknown, unknown >('/reports/expense/depreciation'); + +export const DepreciationReportV2Api = new BaseApiService< + ReportDepreciationV2Item, + unknown, + unknown +>('/reports/expense/v2/depreciation'); diff --git a/src/types/api/report/report-expense.d.ts b/src/types/api/report/report-expense.d.ts index cf0d3ad2..e4e23051 100644 --- a/src/types/api/report/report-expense.d.ts +++ b/src/types/api/report/report-expense.d.ts @@ -90,3 +90,63 @@ export type ReportDepreciationSearchParams = { farm: string | null; period: string | null; }; + +export type ReportDepreciationV2KandangItem = { + kandang_id: number; + kandang_name: string; + transfer_id: number; + depreciation_percent: number; + pullet_cost_day_n: number; + depreciation_value: number; + chickin_date: string; + project_flock_kandang_id: number; + depreciation_source: string; + transfer_date: string; + source_project_flock_id: number; + house_type: string; + multiplication_percentage: number; + cutover_date: string; + origin_date: string; + standard_effective_date: string; + population: number; + transfer_qty: number; + total_value_pullet_after_depreciation: number; + manual_input_id: number; + start_schedule_day: number; + day_n: number; +}; + +export type ReportDepreciationV2Item = { + date: string; + depreciation_percent_effective: number; + depreciation_value: number; + pullet_cost_day_n_total: number; + multiplication_percentage: number; + day_n: number; + chickin_date: string; + total_value_pullet_after_depreciation: number; + standard_effective_date: string; + total_population: number; + components: { + kandang_count: number; + total_population: number; + kandang: ReportDepreciationV2KandangItem[]; + }; +}; + +export type DepreciationV2Meta = { + project_flock_id: number; + farm_name: string; + location_id: number; + period: string; + limit: number; + total_days: number; +}; + +export type DepreciationV2Response = { + code: number; + status: string; + message: string; + meta: DepreciationV2Meta; + data: ReportDepreciationV2Item[]; +};