mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
870 lines
28 KiB
TypeScript
870 lines
28 KiB
TypeScript
import { useState, useMemo, useCallback } from 'react';
|
|
import { ChangeEventHandler } from 'react';
|
|
import useSWR from 'swr';
|
|
import Card from '@/components/Card';
|
|
import SelectInput, {
|
|
useSelect,
|
|
OptionType,
|
|
} from '@/components/input/SelectInput';
|
|
import DateInput from '@/components/input/DateInput';
|
|
import NumberInput from '@/components/input/NumberInput';
|
|
import { AreaApi } from '@/services/api/master-data';
|
|
import { LocationApi } from '@/services/api/master-data';
|
|
import { KandangApi } from '@/services/api/master-data';
|
|
import { SaleReportApi } from '@/services/api/report/marketing-sale';
|
|
import Table from '@/components/Table';
|
|
import { ColumnDef, Row } from '@tanstack/react-table';
|
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
|
import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang';
|
|
import { isResponseSuccess } from '@/lib/api-helper';
|
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
import Button from '@/components/Button';
|
|
import Dropdown from '@/components/Dropdown';
|
|
import MenuItem from '@/components/menu/MenuItem';
|
|
import Menu from '@/components/menu/Menu';
|
|
import { generateHppPerKandangPDF } from '../export/HppPerkandangExport';
|
|
import toast from 'react-hot-toast';
|
|
import * as XLSX from 'xlsx';
|
|
|
|
interface Totals {
|
|
total_hpp_rp: number;
|
|
total_average_doc_price_rp: number;
|
|
}
|
|
|
|
const HppPerKandangTab = () => {
|
|
// ===== STATE MANAGEMENT =====
|
|
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
|
|
|
// ===== SUBMISSION STATE =====
|
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
|
|
// ===== TABLE FILTER STATE =====
|
|
const { state: tableFilterState, updateFilter } = useTableFilter({
|
|
initial: {
|
|
area_id: [] as string[],
|
|
location_id: [] as string[],
|
|
kandang_id: [] as string[],
|
|
weight_min: '',
|
|
weight_max: '',
|
|
period: '',
|
|
sort_by: '',
|
|
show_unrecorded: false,
|
|
},
|
|
paramMap: {
|
|
page: 'page',
|
|
pageSize: 'limit',
|
|
},
|
|
});
|
|
|
|
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
|
AreaApi.basePath,
|
|
'id',
|
|
'name',
|
|
'search'
|
|
);
|
|
|
|
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
|
|
useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
|
|
|
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
|
useSelect(KandangApi.basePath, 'id', 'name', 'search');
|
|
|
|
const showUnrecordedOptions: OptionType[] = [
|
|
{ value: 'false', label: 'Sembunyikan' },
|
|
{ value: 'true', label: 'Tampilkan' },
|
|
];
|
|
|
|
const areaChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'area_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const locationChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'location_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const kandangChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
|
updateFilter(
|
|
'kandang_id',
|
|
arr.map((v) => String((v as OptionType).value))
|
|
);
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const weightMinChangeHandler = useCallback<
|
|
ChangeEventHandler<HTMLInputElement>
|
|
>(
|
|
(e) => {
|
|
const val = e.target.value;
|
|
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const weightMaxChangeHandler = useCallback<
|
|
ChangeEventHandler<HTMLInputElement>
|
|
>(
|
|
(e) => {
|
|
const val = e.target.value;
|
|
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : '');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
|
(e) => {
|
|
const val = e.target.value;
|
|
updateFilter('period', val || '');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const showUnrecordedChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const newVal = val as OptionType;
|
|
updateFilter('show_unrecorded', newVal?.value === 'true');
|
|
setIsSubmitted(false);
|
|
},
|
|
[updateFilter]
|
|
);
|
|
|
|
const resetFilters = useCallback(() => {
|
|
updateFilter('area_id', []);
|
|
updateFilter('location_id', []);
|
|
updateFilter('kandang_id', []);
|
|
updateFilter('weight_min', '');
|
|
updateFilter('weight_max', '');
|
|
updateFilter('period', '');
|
|
updateFilter('sort_by', '');
|
|
updateFilter('show_unrecorded', false);
|
|
setIsSubmitted(false);
|
|
}, [updateFilter]);
|
|
|
|
const handleSubmit = useCallback(() => {
|
|
if (!tableFilterState.period) {
|
|
toast.error('Periode wajib diisi');
|
|
return;
|
|
}
|
|
setIsSubmitted(true);
|
|
}, [tableFilterState.period]);
|
|
|
|
// ===== DATA FETCHING =====
|
|
const { data: hppPerKandang, isLoading } = useSWR(
|
|
isSubmitted
|
|
? () => {
|
|
const params = {
|
|
area_id:
|
|
tableFilterState.area_id.length > 0
|
|
? tableFilterState.area_id.join(',')
|
|
: undefined,
|
|
location_id:
|
|
tableFilterState.location_id.length > 0
|
|
? tableFilterState.location_id.join(',')
|
|
: undefined,
|
|
kandang_id:
|
|
tableFilterState.kandang_id.length > 0
|
|
? tableFilterState.kandang_id.join(',')
|
|
: undefined,
|
|
weight_min: tableFilterState.weight_min || undefined,
|
|
weight_max: tableFilterState.weight_max || undefined,
|
|
period: tableFilterState.period || undefined,
|
|
sort_by: tableFilterState.sort_by || undefined,
|
|
show_unrecorded: tableFilterState.show_unrecorded,
|
|
};
|
|
|
|
return ['hpp-per-kandang-report', params];
|
|
}
|
|
: null,
|
|
([, params]) =>
|
|
SaleReportApi.getHppPerKandangReport(
|
|
params.area_id,
|
|
params.location_id,
|
|
params.kandang_id,
|
|
params.weight_min,
|
|
params.weight_max,
|
|
params.period,
|
|
params.sort_by,
|
|
params.show_unrecorded
|
|
)
|
|
);
|
|
|
|
const data: HppPerKandangReport['rows'] = useMemo(
|
|
() =>
|
|
isResponseSuccess(hppPerKandang)
|
|
? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || []
|
|
: [],
|
|
[hppPerKandang]
|
|
);
|
|
|
|
const summary =
|
|
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary
|
|
? hppPerKandang.data.summary
|
|
: undefined;
|
|
|
|
const period =
|
|
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period
|
|
? hppPerKandang.data.period
|
|
: undefined;
|
|
|
|
// ===== EXPORT DATA FETCHER =====
|
|
const hppPerKandangExport =
|
|
useCallback(async (): Promise<HppPerKandangReport | null> => {
|
|
const params = {
|
|
area_id:
|
|
tableFilterState.area_id.length > 0
|
|
? tableFilterState.area_id.join(',')
|
|
: undefined,
|
|
location_id:
|
|
tableFilterState.location_id.length > 0
|
|
? tableFilterState.location_id.join(',')
|
|
: undefined,
|
|
kandang_id:
|
|
tableFilterState.kandang_id.length > 0
|
|
? tableFilterState.kandang_id.join(',')
|
|
: undefined,
|
|
weight_min: tableFilterState.weight_min || undefined,
|
|
weight_max: tableFilterState.weight_max || undefined,
|
|
period: tableFilterState.period || undefined,
|
|
sort_by: tableFilterState.sort_by || undefined,
|
|
show_unrecorded: tableFilterState.show_unrecorded,
|
|
limit: 10000,
|
|
page: 1,
|
|
};
|
|
|
|
const response = await SaleReportApi.getHppPerKandangReport(
|
|
params.area_id,
|
|
params.location_id,
|
|
params.kandang_id,
|
|
params.weight_min,
|
|
params.weight_max,
|
|
params.period,
|
|
params.sort_by,
|
|
params.show_unrecorded
|
|
);
|
|
|
|
return isResponseSuccess(response) ? response.data : null;
|
|
}, [tableFilterState]);
|
|
|
|
// ===== TABLE COLUMNS DEFINITION =====
|
|
const totals: Totals = useMemo(() => {
|
|
return {
|
|
total_hpp_rp:
|
|
data.length > 0
|
|
? data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0)
|
|
: 0,
|
|
total_average_doc_price_rp:
|
|
data.length > 0
|
|
? data.reduce(
|
|
(acc, item) => acc + (item.average_doc_price_rp || 0),
|
|
0
|
|
) / data.length
|
|
: 0,
|
|
};
|
|
}, [summary]);
|
|
|
|
const allFeedSuppliers = useMemo(() => {
|
|
const suppliers = new Set<string>();
|
|
data.forEach((item) => {
|
|
item.feed_suppliers?.forEach((s) => {
|
|
suppliers.add(s.alias || s.name);
|
|
});
|
|
});
|
|
return Array.from(suppliers).join(' | ');
|
|
}, [data]);
|
|
|
|
const allDocSuppliers = useMemo(() => {
|
|
const suppliers = new Set<string>();
|
|
data.forEach((item) => {
|
|
item.doc_suppliers?.forEach((s) => {
|
|
suppliers.add(s.alias || s.name);
|
|
});
|
|
});
|
|
return Array.from(suppliers).join(' | ');
|
|
}, [data]);
|
|
|
|
// ===== EXPORT HANDLERS =====
|
|
const handleExportExcel = useCallback(async () => {
|
|
setIsExcelExportLoading(true);
|
|
try {
|
|
const allDataForExport = await hppPerKandangExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!allDataForExport?.rows ||
|
|
allDataForExport.rows.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
const allExportData =
|
|
allDataForExport.rows as HppPerKandangReport['rows'];
|
|
|
|
const summary = allDataForExport.summary;
|
|
|
|
const excelData: { [key: string]: string | number }[] = allExportData.map(
|
|
(item, index) => ({
|
|
No: index + 1,
|
|
Kandang: item.kandang?.name || '',
|
|
'Rentang Bobot': item.weight_range
|
|
? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
|
|
: '',
|
|
'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0,
|
|
'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0,
|
|
'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0,
|
|
'Produksi Telur (Butir)': item.egg_production_pieces || 0,
|
|
'Produksi Telur (KG)': item.egg_production_kg || 0,
|
|
'Feed (Supplier)':
|
|
item.feed_suppliers?.map((s) => s.alias || s.name).join(' | ') ||
|
|
'',
|
|
'DOC (Supplier)':
|
|
item.doc_suppliers?.map((s) => s.alias || s.name).join(' | ') || '',
|
|
'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0,
|
|
'Nilai Nominal Telur (RP)': item.egg_value_rp || 0,
|
|
'HPP Ayam (RP)': item.hpp_rp || 0,
|
|
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
|
|
'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0,
|
|
})
|
|
);
|
|
|
|
excelData.push({
|
|
No: 'TOTAL',
|
|
Kandang: 'ALL',
|
|
'Rentang Bobot': '-',
|
|
'Rata-Rata Bobot (KG)': summary?.average_weight_kg || 0,
|
|
'Sisa Ayam (Ekor)': summary?.total_remaining_chicken_birds || 0,
|
|
'Sisa Ayam (KG)': summary?.total_remaining_chicken_weight_kg || 0,
|
|
'Produksi Telur (Butir)': summary?.total_egg_production_pieces || 0,
|
|
'Produksi Telur (KG)': summary?.total_egg_production_kg || 0,
|
|
'Feed (Supplier)': allFeedSuppliers,
|
|
'DOC (Supplier)': allDocSuppliers,
|
|
'Rata-Rata Harga DOC (RP)': totals?.total_average_doc_price_rp || 0,
|
|
'Nilai Nominal Telur (RP)': summary?.total_egg_value_rp || 0,
|
|
'HPP Ayam (RP)': totals?.total_hpp_rp || 0,
|
|
'HPP Telur (RP/KG)': summary?.average_egg_hpp_rp_per_kg || 0,
|
|
'Nilai Nominal Sisa Ayam (RP)': summary?.total_remaining_value_rp || 0,
|
|
});
|
|
|
|
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
|
|
|
const colWidths = [
|
|
{ wch: 5 }, // No
|
|
{ wch: 30 }, // Kandang
|
|
{ wch: 15 }, // Rentang Bobot
|
|
{ wch: 18 }, // Rata-Rata Bobot (KG)
|
|
{ wch: 15 }, // Sisa Ayam (Ekor)
|
|
{ wch: 15 }, // Sisa Ayam (KG)
|
|
{ wch: 18 }, // Produksi Telur (Butir)
|
|
{ wch: 18 }, // Produksi Telur (KG)
|
|
{ wch: 20 }, // Feed (Supplier)
|
|
{ wch: 20 }, // DOC (Supplier)
|
|
{ wch: 20 }, // Rata-Rata Harga DOC (RP)
|
|
{ wch: 20 }, // Nilai Nominal Telur (RP)
|
|
{ wch: 15 }, // HPP Ayam (RP)
|
|
{ wch: 18 }, // HPP Telur (RP/KG)
|
|
{ wch: 25 }, // Nilai Nominal Sisa Ayam (RP)
|
|
];
|
|
worksheet['!cols'] = colWidths;
|
|
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang');
|
|
|
|
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
|
|
|
|
XLSX.writeFile(workbook, filename);
|
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
} finally {
|
|
setIsExcelExportLoading(false);
|
|
}
|
|
}, [
|
|
hppPerKandangExport,
|
|
tableFilterState,
|
|
areaOptions,
|
|
locationOptions,
|
|
kandangOptions,
|
|
]);
|
|
|
|
const handleExportPDF = useCallback(async () => {
|
|
setIsPdfExportLoading(true);
|
|
try {
|
|
const allDataForExport = await hppPerKandangExport();
|
|
|
|
if (
|
|
!allDataForExport ||
|
|
!allDataForExport?.rows ||
|
|
allDataForExport.rows.length === 0
|
|
) {
|
|
toast.error('Tidak ada data untuk diekspor.');
|
|
return;
|
|
}
|
|
|
|
const areaName =
|
|
tableFilterState.area_id.length > 0
|
|
? tableFilterState.area_id
|
|
.map(
|
|
(id) =>
|
|
areaOptions.find((opt) => opt.value === Number(id))?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Area'
|
|
: 'Semua Area';
|
|
|
|
const locationName =
|
|
tableFilterState.location_id.length > 0
|
|
? tableFilterState.location_id
|
|
.map(
|
|
(id) =>
|
|
locationOptions.find((opt) => opt.value === Number(id))?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Lokasi'
|
|
: 'Semua Lokasi';
|
|
|
|
const kandangName =
|
|
tableFilterState.kandang_id.length > 0
|
|
? tableFilterState.kandang_id
|
|
.map(
|
|
(id) =>
|
|
kandangOptions.find((opt) => opt.value === Number(id))?.label
|
|
)
|
|
.filter(Boolean)
|
|
.join(', ') || 'Semua Kandang'
|
|
: 'Semua Kandang';
|
|
|
|
await generateHppPerKandangPDF(allDataForExport, {
|
|
area_name: areaName,
|
|
location_name: locationName,
|
|
kandang_name: kandangName,
|
|
period: tableFilterState.period,
|
|
weight_min: tableFilterState.weight_min,
|
|
weight_max: tableFilterState.weight_max,
|
|
show_unrecorded: tableFilterState.show_unrecorded.toString(),
|
|
sort_by: tableFilterState.sort_by,
|
|
});
|
|
|
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
|
} catch {
|
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
|
} finally {
|
|
setIsPdfExportLoading(false);
|
|
}
|
|
}, [
|
|
hppPerKandangExport,
|
|
tableFilterState,
|
|
areaOptions,
|
|
locationOptions,
|
|
kandangOptions,
|
|
]);
|
|
|
|
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
|
|
const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [
|
|
{
|
|
id: 'no',
|
|
header: 'No',
|
|
cell: (props) => props.row.index + 1,
|
|
footer: () => <div className='font-semibold text-gray-900'>TOTAL</div>,
|
|
},
|
|
{
|
|
id: 'kandang_name',
|
|
header: 'Kandang',
|
|
accessorKey: 'kandang.name',
|
|
cell: (props) => {
|
|
const kandang = props.row.original.kandang;
|
|
return kandang?.name || '-';
|
|
},
|
|
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
|
|
},
|
|
{
|
|
id: 'weight_range',
|
|
header: 'Rentang Bobot',
|
|
accessorKey: 'weight_range',
|
|
cell: (props) => {
|
|
const weightRange = props.row.original.weight_range;
|
|
return weightRange
|
|
? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}`
|
|
: '-';
|
|
},
|
|
footer: () => <div className='font-semibold text-gray-900'>-</div>,
|
|
},
|
|
{
|
|
id: 'avg_weight_kg',
|
|
header: 'Rata-Rata Bobot (KG)',
|
|
accessorKey: 'avg_weight_kg',
|
|
cell: (props) => {
|
|
const value = props.row.original.avg_weight_kg;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary?.average_weight_kg || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'remaining_chicken_birds',
|
|
header: 'Sisa Ayam (Ekor)',
|
|
accessorKey: 'remaining_chicken_birds',
|
|
cell: (props) => {
|
|
const value = props.row.original.remaining_chicken_birds;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary?.total_remaining_chicken_birds || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'remaining_chicken_weight_kg',
|
|
header: 'Sisa Ayam (KG)',
|
|
accessorKey: 'remaining_chicken_weight_kg',
|
|
cell: (props) => {
|
|
const value = props.row.original.remaining_chicken_weight_kg;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary?.total_remaining_chicken_weight_kg || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'egg_production_pieces',
|
|
header: 'Produksi Telur (Butir)',
|
|
accessorKey: 'egg_production_pieces',
|
|
cell: (props) => {
|
|
const value = props.row.original.egg_production_pieces;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary?.total_egg_production_pieces || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'egg_production_kg',
|
|
header: 'Produksi Telur (KG)',
|
|
accessorKey: 'egg_production_kg',
|
|
cell: (props) => {
|
|
const value = props.row.original.egg_production_kg;
|
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatNumber(summary?.total_remaining_chicken_weight_kg || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'feed_suppliers',
|
|
header: 'Feed (Supplier)',
|
|
accessorKey: 'feed_suppliers',
|
|
cell: (props) => {
|
|
const suppliers = props.row.original.feed_suppliers;
|
|
return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-';
|
|
},
|
|
footer: () => (
|
|
<div className='font-semibold text-gray-900'>
|
|
{allFeedSuppliers || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'doc_suppliers',
|
|
header: 'DOC (Supplier)',
|
|
accessorKey: 'doc_suppliers',
|
|
cell: (props) => {
|
|
const suppliers = props.row.original.doc_suppliers;
|
|
return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-';
|
|
},
|
|
footer: () => (
|
|
<div className='font-semibold text-gray-900'>
|
|
{allDocSuppliers || '-'}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'average_doc_price_rp',
|
|
header: 'Rata-Rata Harga DOC (RP)',
|
|
accessorKey: 'average_doc_price_rp',
|
|
cell: (props) => {
|
|
const value = props.row.original.average_doc_price_rp;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(totals?.total_average_doc_price_rp || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'egg_value_rp',
|
|
header: 'Nilai Nominal Telur (RP)',
|
|
accessorKey: 'egg_value_rp',
|
|
cell: (props) => {
|
|
const value = props.row.original.egg_value_rp;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary?.total_egg_value_rp || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'hpp_rp',
|
|
header: 'HPP Ayam (RP)',
|
|
accessorKey: 'hpp_rp',
|
|
cell: (props) => {
|
|
const value = props.row.original.hpp_rp;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(totals?.total_hpp_rp || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'egg_hpp_rp_per_kg',
|
|
header: 'HPP Telur (RP/KG)',
|
|
accessorKey: 'egg_hpp_rp_per_kg',
|
|
cell: (props) => {
|
|
const value = props.row.original.egg_hpp_rp_per_kg;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary?.average_egg_hpp_rp_per_kg || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'remaining_value_rp',
|
|
header: 'Nilai Nominal Sisa Ayam (RP)',
|
|
accessorKey: 'remaining_value_rp',
|
|
cell: (props) => {
|
|
const value = props.row.original.remaining_value_rp;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{formatCurrency(summary?.total_remaining_value_rp || 0)}
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
return tableColumns;
|
|
};
|
|
|
|
// ===== CUSTOM ROW RENDERER =====
|
|
const renderCustomRow = useCallback(
|
|
(row: Row<HppPerKandangReport['rows'][0]>) => {
|
|
if (row.index === data.length - 1) {
|
|
return (
|
|
<tr className='border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'>
|
|
<td
|
|
colSpan={15}
|
|
className='px-4 py-3 text-gray-900 text-center font-semibold'
|
|
>
|
|
Rekapitulasi per rentang bobot
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
},
|
|
[data]
|
|
);
|
|
|
|
return (
|
|
<div className='w-full p-0 sm:p-4'>
|
|
<Card
|
|
subtitle={
|
|
period
|
|
? `Laporan > HPP Harian Kandang (${period})`
|
|
: 'Laporan > HPP Harian Kandang'
|
|
}
|
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
|
>
|
|
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
|
<Button color='primary' onClick={handleSubmit}>
|
|
Cari
|
|
</Button>
|
|
<Button color='warning' onClick={resetFilters}>
|
|
Reset
|
|
</Button>
|
|
<Dropdown
|
|
trigger={
|
|
<Button color='success' isLoading={isAnyExportLoading}>
|
|
Export
|
|
</Button>
|
|
}
|
|
align='end'
|
|
>
|
|
<Menu className='w-32'>
|
|
<MenuItem title='Excel' onClick={handleExportExcel} />
|
|
<MenuItem title='PDF' onClick={handleExportPDF} />
|
|
</Menu>
|
|
</Dropdown>
|
|
</div>
|
|
|
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
|
<SelectInput
|
|
label='Area'
|
|
placeholder='Pilih Area'
|
|
isMulti
|
|
options={areaOptions}
|
|
value={areaOptions.filter((opt) =>
|
|
(tableFilterState.area_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={areaChangeHandler}
|
|
isLoading={isLoadingAreas}
|
|
isClearable
|
|
/>
|
|
<SelectInput
|
|
label='Lokasi'
|
|
placeholder='Pilih Lokasi'
|
|
isMulti
|
|
options={locationOptions}
|
|
value={locationOptions.filter((opt) =>
|
|
(tableFilterState.location_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={locationChangeHandler}
|
|
isLoading={isLoadingLocations}
|
|
isClearable
|
|
/>
|
|
<SelectInput
|
|
label='Kandang'
|
|
placeholder='Pilih Kandang'
|
|
isMulti
|
|
options={kandangOptions}
|
|
value={kandangOptions.filter((opt) =>
|
|
(tableFilterState.kandang_id || [])
|
|
.map(String)
|
|
.includes(String(opt.value))
|
|
)}
|
|
onChange={kandangChangeHandler}
|
|
isLoading={isLoadingKandangs}
|
|
isClearable
|
|
/>
|
|
</div>
|
|
|
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
|
<div className='flex flex-row gap-4'>
|
|
<NumberInput
|
|
label='Rentang Bobot Min (Kg)'
|
|
name='weight_min'
|
|
placeholder='Masukkan bobot minimum'
|
|
value={tableFilterState.weight_min}
|
|
onChange={weightMinChangeHandler}
|
|
/>
|
|
<NumberInput
|
|
label='Rentang Bobot Max (Kg)'
|
|
name='weight_max'
|
|
placeholder='Masukkan bobot maximum'
|
|
value={tableFilterState.weight_max}
|
|
onChange={weightMaxChangeHandler}
|
|
/>
|
|
</div>
|
|
<DateInput
|
|
label='Periode'
|
|
name='period'
|
|
placeholder='Pilih Periode'
|
|
value={tableFilterState.period}
|
|
onChange={periodChangeHandler}
|
|
required
|
|
/>
|
|
<SelectInput
|
|
label='Tampilkan Kandang Tanpa Recording'
|
|
placeholder='Pilih Opsi'
|
|
options={showUnrecordedOptions}
|
|
value={
|
|
tableFilterState.show_unrecorded
|
|
? showUnrecordedOptions.find((opt) => opt.value === 'true') ||
|
|
null
|
|
: showUnrecordedOptions.find((opt) => opt.value === 'false') ||
|
|
null
|
|
}
|
|
onChange={showUnrecordedChangeHandler}
|
|
/>
|
|
</div>
|
|
|
|
<div className='divider'></div>
|
|
|
|
{!isSubmitted ? (
|
|
<div className='mt-6 text-center text-gray-500'>
|
|
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
|
|
</div>
|
|
) : isLoading ? (
|
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
|
<span className='loading loading-spinner loading-xl' />
|
|
</div>
|
|
) : data.length === 0 ? (
|
|
<div className='mt-6 text-center text-gray-500'>
|
|
Tidak ada data yang dapat ditampilkan...
|
|
</div>
|
|
) : (
|
|
<Table
|
|
data={data}
|
|
columns={getTableColumns()}
|
|
renderFooter={data.length > 0}
|
|
renderCustomRow={renderCustomRow}
|
|
className={{
|
|
containerClassName: 'w-full mt-6',
|
|
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',
|
|
}}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HppPerKandangTab;
|