diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx index 0bde153d..045e7a99 100644 --- a/src/components/pages/report/expense/ReportExpenseTabs.tsx +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -4,7 +4,8 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; -import ReportExpenseTab from './tab/ReportExpenseTab'; +import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab'; +import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab'; const ReportExpenseTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); @@ -16,6 +17,11 @@ const ReportExpenseTabs = () => { label: 'Laporan Biaya Operasional', content: , }, + { + id: '2', + label: 'Laporan Depresiasi', + content: , + }, ]; return ( diff --git a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx index 3e13c539..4f0ab52e 100644 --- a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx +++ b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx @@ -1,27 +1,26 @@ import React from 'react'; import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; import Table from '@/components/Table'; -import { ReportExpense } from '@/types/api/report/report-expense'; import { ColumnDef } from '@tanstack/react-table'; -type ReportExpenseColumn = - | ColumnDef +type ReportSkeletonColumn = + | ColumnDef | { header: string; columns: Array<{ header: string; accessorKey?: string; - cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode; + cell?: (props: { row: { original: TData } }) => React.ReactNode; }>; }; -const ReportExpenseSkeleton = ({ +const ReportExpenseSkeleton = ({ columns, icon, title, subtitle, }: { - columns: ReportExpenseColumn[]; + columns: ReportSkeletonColumn[]; icon: React.ReactNode; title: string; subtitle: string; diff --git a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx new file mode 100644 index 00000000..91e5f536 --- /dev/null +++ b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { RefObject, useMemo, useState } from 'react'; +import { useFormik } from 'formik'; +import * as yup from 'yup'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; + +import { AreaApi, LocationApi } from '@/services/api/master-data'; +import { ProjectFlockApi } from '@/services/api/production'; +import { Area } from '@/types/api/master-data/area'; +import { Location } from '@/types/api/master-data/location'; +import { ProjectFlock } from '@/types/api/production/project-flock'; + +export type ReportDepreciationFilterValues = { + area_id: string | null; + location_id: string | null; + project_flock_id: string | null; + period: string | null; +}; + +export const ReportDepreciationFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + project_flock_id: yup.string().nullable(), + period: yup.string().nullable().required('Periode wajib dipilih'), +}) as yup.ObjectSchema; + +interface ReportDepreciationFilterModalProps { + ref: RefObject; + initialValues?: ReportDepreciationFilterValues; + onSubmit?: (values: Partial) => void; + onReset?: () => void; +} + +const defaultInitialValues: ReportDepreciationFilterValues = { + area_id: null, + location_id: null, + project_flock_id: null, + period: null, +}; + +const ReportDepreciationFilterModal = ({ + ref, + initialValues, + onSubmit, + onReset, +}: ReportDepreciationFilterModalProps) => { + const [selectedAreaId, setSelectedAreaId] = useState( + initialValues?.area_id || undefined + ); + const [selectedLocationId, setSelectedLocationId] = useState< + string | undefined + >(initialValues?.location_id || undefined); + + const closeModalHandler = () => { + ref.current?.close(); + }; + + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: selectedAreaId || '', + }); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedLocationId || '', + } + ); + + const formik = useFormik({ + initialValues: initialValues || defaultInitialValues, + validationSchema: ReportDepreciationFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + onReset: (_) => { + onReset?.(); + closeModalHandler(); + }, + }); + + const areaValue = useMemo(() => { + if (!formik.values.area_id) return null; + return ( + areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || + null + ); + }, [formik.values.area_id, areaOptions]); + + const locationValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const projectFlockValue = useMemo(() => { + if (!formik.values.project_flock_id) return null; + return ( + projectFlockOptions.find( + (opt) => String(opt.value) === formik.values.project_flock_id + ) || null + ); + }, [formik.values.project_flock_id, projectFlockOptions]); + + const areaChangeHandler = (val: OptionType | OptionType[] | null) => { + const areaId = val && !Array.isArray(val) ? String(val.value) : null; + + setSelectedAreaId(areaId || undefined); + formik.setFieldValue('area_id', areaId); + formik.setFieldValue('location_id', null); + formik.setFieldValue('project_flock_id', null); + setSelectedLocationId(undefined); + }; + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const locationId = val && !Array.isArray(val) ? String(val.value) : null; + + setSelectedLocationId(locationId || undefined); + formik.setFieldValue('location_id', locationId); + formik.setFieldValue('project_flock_id', null); + }; + + const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { + const projectFlockId = + val && !Array.isArray(val) ? String(val.value) : null; + + formik.setFieldValue('project_flock_id', projectFlockId); + }; + + return ( + +
+
+
+ +

Filter Data

+
+ + +
+ +
+ + + + + + + +
+ +
+ + + +
+
+
+ ); +}; + +export default ReportDepreciationFilterModal; diff --git a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx new file mode 100644 index 00000000..8c6caaa3 --- /dev/null +++ b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx @@ -0,0 +1,257 @@ +'use client'; + +import React, { useEffect, useMemo } 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'; +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 { useTableFilter } from '@/services/hooks/useTableFilter'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; + +interface ReportDepreciationTabProps { + tabId: string; +} + +const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter({ + initial: { + area_id: '', + location_id: '', + project_flock_id: '', + period: formatDate(Date.now(), 'YYYY-MM-DD'), + }, + paramMap: { + pageSize: 'limit', + area_id: 'area_id', + location_id: 'location_id', + project_flock_id: 'project_flock_id', + period: 'period', + }, + }); + + const { data: depreciationsResponse, isLoading: isLoadingDepreciations } = + useSWR( + `${DepreciationReportApi.basePath}${getTableFilterQueryString()}`, + DepreciationReportApi.getAllFetcher + ); + + const depreciations = isResponseSuccess(depreciationsResponse) + ? depreciationsResponse.data + : []; + + const filterModal = useModal(); + const { ref: filterModalRef } = filterModal; + + 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 tabActionsElement = useMemo( + () => ( +
+ filterModal.openModal()} + variant='outline' + className='px-3 py-2.5' + /> +
+ ), + [tableFilterState] + ); + + useEffect(() => { + setTabActions(tabId, tabActionsElement); + }, [setTabActions, tabActionsElement, tabId]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions, tabId]); + + return ( + <> +
+ {isLoadingDepreciations && ( +
+ +
+ )} + + {!isLoadingDepreciations && depreciations.length === 0 && ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + )} + + {!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} + /> + + )} + + + { + updateFilter('area_id', values.area_id ?? ''); + updateFilter('location_id', values.location_id ?? ''); + updateFilter('project_flock_id', values.project_flock_id ?? ''); + updateFilter( + 'period', + values.period ? formatDate(values.period, 'YYYY-MM-DD') : '' + ); + + console.log({ values }); + }} + /> + + ); +}; + +export default ReportDepreciationTab; diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index e439c09a..811d9c2c 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -23,7 +23,7 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus import Table from '@/components/Table'; import { formatCurrency, formatDate } from '@/lib/helper'; import { ReportExpense } from '@/types/api/report/report-expense'; -import { ReportExpenseApi } from '@/services/api/report'; +import { ReportExpenseApi } from '@/services/api/report/expense-report'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; diff --git a/src/services/api/report.ts b/src/services/api/report/expense-report.ts similarity index 72% rename from src/services/api/report.ts rename to src/services/api/report/expense-report.ts index 5a5e06c8..e6faf8ac 100644 --- a/src/services/api/report.ts +++ b/src/services/api/report/expense-report.ts @@ -1,7 +1,10 @@ import { BaseApiService } from '@/services/api/base'; import { httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; -import { ReportExpense } from '@/types/api/report/report-expense'; +import { + ReportDepreciation, + ReportExpense, +} from '@/types/api/report/report-expense'; export class ReportExpenseApiService extends BaseApiService< ReportExpense, @@ -20,3 +23,9 @@ export class ReportExpenseApiService extends BaseApiService< } export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense'); + +export const DepreciationReportApi = new BaseApiService< + ReportDepreciation, + unknown, + unknown +>('/reports/expense/depreciation'); diff --git a/src/types/api/report/report-expense.d.ts b/src/types/api/report/report-expense.d.ts index bf9b94eb..cf0d3ad2 100644 --- a/src/types/api/report/report-expense.d.ts +++ b/src/types/api/report/report-expense.d.ts @@ -52,3 +52,41 @@ export type ReportExpenseSearchParams = { category: string | null; search: string; }; + +export type ReportDepreciation = { + project_flock_id: number; + farm_name: string; + period: string; + depreciation_percent_effective: number; + depreciation_value: number; + pullet_cost_day_n_total: number; + hpp?: number; + components: { + kandang: { + kandang_id: number; + hpp?: number; + transfer_id: number; + cutover_date: string; + kandang_name: string; + manual_input_id: number; + depreciation_value: number; + start_schedule_day: number; + depreciation_percent: number; + source_project_flock_id: number; + day_n: number; + transfer_date: string; + pullet_cost_day_n: number; + depreciation_source: string; + project_flock_kandang_id: number; + transfer_qty: number; + house_type: string; + origin_date: string; + }[]; + kandang_count: number; + }; +}; + +export type ReportDepreciationSearchParams = { + farm: string | null; + period: string | null; +};