Merge branch 'development' into fix/transfer-to-laying

This commit is contained in:
ValdiANS
2026-01-20 17:37:06 +07:00
55 changed files with 1824 additions and 923 deletions
@@ -300,7 +300,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>Rata-Rata</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Harga Awal</Text>
<Text>Harga/Unit</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Harga Akhir</Text>
@@ -378,7 +378,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>{formatNumber(item.average_weight)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.price)}</Text>
<Text>{formatCurrency(item.unit_price)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.final_price)}</Text>
@@ -38,7 +38,7 @@ export const generateCustomerPaymentExcel = (
'Ekor/Qty': formatNumber(item.qty || 0),
'Berat (Kg)': formatNumber(item.weight || 0),
AVG: formatNumber(item.average_weight || 0),
'Harga Awal': formatCurrency(item.price || 0),
'Harga/Unit': formatCurrency(item.unit_price || 0),
'Harga Akhir': formatCurrency(item.final_price || 0),
Total: formatCurrency(item.total_price || 0),
Pembayaran: formatCurrency(item.payment_amount || 0),
@@ -62,7 +62,7 @@ export const generateCustomerPaymentExcel = (
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0),
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0),
AVG: '',
'Harga Awal': '',
'Harga/Unit': '',
'Harga Akhir': formatCurrency(
customerReport.summary.total_final_amount || 0
),
@@ -89,7 +89,7 @@ export const generateCustomerPaymentExcel = (
{ wch: 10 }, // Ekor/Qty
{ wch: 12 }, // Berat
{ wch: 10 }, // AVG
{ wch: 15 }, // Harga Awal
{ wch: 15 }, // Harga/Unit
{ wch: 15 }, // Harga Akhir
{ wch: 15 }, // Total
{ wch: 15 }, // Pembayaran
@@ -106,7 +106,11 @@ const CustomerPaymentTab = () => {
};
const getPaymentStatusText = (notes: string) => {
return notes;
return notes
.toLowerCase()
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// ===== FILTER HANDLERS =====
@@ -159,7 +163,7 @@ const CustomerPaymentTab = () => {
isSubmitted
? () => {
const params = {
customer_id:
customer_ids:
filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',')
: undefined,
@@ -180,7 +184,7 @@ const CustomerPaymentTab = () => {
: null,
([, params]) =>
FinanceApi.getCustomerPaymentReport(
params.customer_id,
params.customer_ids,
undefined, // TODO: Change to params.sales_id when BE is ready
undefined, // TODO: Change to params.filter_by when BE is ready
params.start_date,
@@ -203,7 +207,7 @@ const CustomerPaymentTab = () => {
CustomerPaymentReport[] | null
> => {
const params = {
customer_id:
customer_ids:
filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',')
: undefined,
@@ -219,7 +223,7 @@ const CustomerPaymentTab = () => {
};
const response = await FinanceApi.getCustomerPaymentReport(
params.customer_id,
params.customer_ids,
undefined, // TODO: Change to params.sales_id when BE is ready
undefined, // TODO: Change to params.filter_by when BE is ready
params.start_date,
@@ -336,7 +340,9 @@ const CustomerPaymentTab = () => {
const value = props.row.original.aging_day;
return (
<div className='text-center'>
{value && value > 0 ? `${formatNumber(value)} hari` : '-'}
{value !== null && value !== undefined
? `${formatNumber(value)} hari`
: '-'}
</div>
);
},
@@ -405,12 +411,12 @@ const CustomerPaymentTab = () => {
),
},
{
id: 'price',
header: 'Harga Awal',
accessorKey: 'price',
id: 'unit_price',
header: 'Harga/Unit',
accessorKey: 'unit_price',
enableSorting: false,
cell: (props) => {
const value = props.row.original.price;
const value = props.row.original.unit_price;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
footer: () => (
@@ -510,7 +516,7 @@ const CustomerPaymentTab = () => {
status: getPaymentStatusIndicatorColor(value),
}}
>
<span>{getPaymentStatusText(value)}</span>
<span className='capitalize'>{getPaymentStatusText(value)}</span>
</Badge>
);
},
@@ -246,7 +246,12 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>HPP Telur (RP/KG)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<View
style={[
pdfStyles.tableCellHeaderRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>Nominal Sisa</Text>
</View>
</View>
@@ -301,7 +306,12 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<View
style={[
pdfStyles.tableCellRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>{formatCurrency(group.egg_value_rp)}</Text>
</View>
</View>
@@ -347,7 +357,12 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>HPP Telur (RP/KG)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<View
style={[
pdfStyles.tableCellHeaderRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>Nominal Sisa</Text>
</View>
</View>
@@ -356,12 +371,7 @@ const createPDFDocument = (
{data.rows.map((item: HppPerKandangRow, index: number) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < data.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}
>
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
@@ -410,11 +420,199 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<View
style={[
pdfStyles.tableCellRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>{formatCurrency(item.egg_value_rp)}</Text>
</View>
</View>
))}
{/* TOTAL Row */}
{data.summary?.total && (
<View style={pdfStyles.tableRow}>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 0.5,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>TOTAL</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1.5,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>ALL</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>-</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatNumber(data.summary.total.average_weight_kg)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 0.8,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatNumber(
data.summary.total.total_egg_production_pieces
)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 0.8,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatNumber(data.summary.total.total_egg_production_kg)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1.2,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{data.rows
.flatMap((row: HppPerKandangRow) =>
row.feed_suppliers?.map(
(s: { alias?: string; name: string }) =>
s.alias || s.name
)
)
.filter(
(v: string, i: number, a: string[]) =>
a.indexOf(v) === i
)
.join(' | ') || '-'}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{data.rows
.flatMap((row: HppPerKandangRow) =>
row.doc_suppliers?.map(
(s: { alias?: string; name: string }) =>
s.alias || s.name
)
)
.filter(
(v: string, i: number, a: string[]) =>
a.indexOf(v) === i
)
.join(' | ') || '-'}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1.2,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatCurrency(
data.summary.total.total_average_doc_price_rp
)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatCurrency(
data.summary.total.average_egg_hpp_rp_per_kg
)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1.2,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
borderRightWidth: 0,
},
]}
>
<Text>
{formatCurrency(data.summary.total.total_egg_value_rp)}
</Text>
</View>
</View>
)}
</View>
</View>
</Page>
@@ -10,7 +10,7 @@ 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 { ProjectFlockKandangApi } from '@/services/api/production';
import { SaleReportApi } from '@/services/api/report/marketing-sale';
import Table from '@/components/Table';
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
@@ -40,6 +40,9 @@ const HppPerKandangTab = () => {
// ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false);
// ===== VALIDATION STATE =====
const [weightMaxError, setWeightMaxError] = useState<string>('');
// ===== TABLE FILTER STATE =====
const { state: tableFilterState, updateFilter } = useTableFilter({
initial: {
@@ -77,7 +80,12 @@ const HppPerKandangTab = () => {
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect(KandangApi.basePath, 'id', 'name', 'search');
} = useSelect(
ProjectFlockKandangApi.basePath,
'id',
'name_with_period',
'search'
);
const showUnrecordedOptions: OptionType[] = [
{ value: 'false', label: 'Sembunyikan' },
@@ -127,8 +135,12 @@ const HppPerKandangTab = () => {
const val = e.target.value;
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
setIsSubmitted(false);
if (weightMaxError) {
setWeightMaxError('');
}
},
[updateFilter]
[updateFilter, weightMaxError]
);
const weightMaxChangeHandler = useCallback<
@@ -136,10 +148,22 @@ const HppPerKandangTab = () => {
>(
(e) => {
const val = e.target.value;
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : '');
const weightMax = val ? parseFloat(val) || 0 : 0;
const weightMin = tableFilterState.weight_min
? parseFloat(tableFilterState.weight_min)
: 0;
if (weightMax < weightMin) {
setWeightMaxError('Rentang bobot max tidak boleh lebih kecil dari min');
toast.error('Rentang bobot max tidak boleh lebih kecil dari min');
return;
}
setWeightMaxError('');
updateFilter('weight_max', val ? String(weightMax) : '');
setIsSubmitted(false);
},
[updateFilter]
[updateFilter, tableFilterState.weight_min]
);
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
@@ -325,8 +349,53 @@ const HppPerKandangTab = () => {
const allExportData =
allDataForExport.rows as HppPerKandangReport['rows'];
const perWeightRangeSummary =
allDataForExport.summary.per_weight_range || [];
const summaryTotal = allDataForExport.summary.total;
const rekapitulasiData: { [key: string]: string | number }[] =
perWeightRangeSummary.map(
(item: HppPerKandangPerWeightRange, index: number) => ({
No: index + 1,
'Rentang BW': item.label || '',
'Sisa Butir': item.egg_production_pieces || 0,
'Sisa Kg': item.egg_production_kg || 0,
'Rata-Rata Bobot (Kg)': item.avg_weight_kg || 0,
'Feed (Supplier)':
item.feed_suppliers
?.map(
(s: { alias?: string; name: string }) => s.alias || s.name
)
.join(' | ') || '',
'DOC (Supplier)':
item.doc_suppliers
?.map(
(s: { alias?: string; name: string }) => s.alias || s.name
)
.join(' | ') || '',
'Rata-Rata Harga DOC': item.average_doc_price_rp || 0,
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
'Nominal Sisa': item.egg_value_rp || 0,
})
);
const rekapitulasiWorksheet = XLSX.utils.json_to_sheet(rekapitulasiData);
const rekapitulasiColWidths = [
{ wch: 5 }, // No
{ wch: 15 }, // Rentang BW
{ wch: 15 }, // Sisa Butir
{ wch: 12 }, // Sisa Kg
{ wch: 18 }, // Rata-Rata Bobot (Kg)
{ wch: 20 }, // Feed (Supplier)
{ wch: 20 }, // DOC (Supplier)
{ wch: 20 }, // Rata-Rata Harga DOC
{ wch: 18 }, // HPP Telur (RP/KG)
{ wch: 25 }, // Nominal Sisa
];
rekapitulasiWorksheet['!cols'] = rekapitulasiColWidths;
const excelData: { [key: string]: string | number }[] = allExportData.map(
(item: HppPerKandangRow, index: number) => ({
No: index + 1,
@@ -384,7 +453,12 @@ const HppPerKandangTab = () => {
worksheet['!cols'] = colWidths;
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang');
XLSX.utils.book_append_sheet(
workbook,
rekapitulasiWorksheet,
'Rekapitulasi'
);
XLSX.utils.book_append_sheet(workbook, worksheet, 'Detail Per Kandang');
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
@@ -488,8 +562,8 @@ const HppPerKandangTab = () => {
header: 'Kandang',
accessorKey: 'kandang.name',
cell: (props) => {
const kandang = props.row.original.kandang;
return kandang?.name || '-';
const row = props.row.original;
return row.name_with_periode || row.kandang?.name || '-';
},
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
},
@@ -741,6 +815,8 @@ const HppPerKandangTab = () => {
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreas}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable
/>
<SelectInput
@@ -757,6 +833,8 @@ const HppPerKandangTab = () => {
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocations}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable
/>
<SelectInput
@@ -773,6 +851,8 @@ const HppPerKandangTab = () => {
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangs}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable
/>
</div>
@@ -792,6 +872,8 @@ const HppPerKandangTab = () => {
placeholder='Masukkan bobot maximum'
value={tableFilterState.weight_max}
onChange={weightMaxChangeHandler}
isError={!!weightMaxError}
errorMessage={weightMaxError}
/>
</div>
<DateInput
@@ -818,7 +900,11 @@ const HppPerKandangTab = () => {
</div>
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
<Button color='primary' onClick={handleSubmit}>
<Button
color='primary'
onClick={handleSubmit}
disabled={!!weightMaxError}
>
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
Cari
</Button>