mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE): Refactor marketing report components into a dedicated
folder
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Card from '@/components/Card';
|
||||
import Collapse from '@/components/Collapse';
|
||||
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { DailyMarketingRow } from '@/types/api/report/marketing';
|
||||
import { MarketingReportApi } from '@/services/api/report/marketing-report';
|
||||
|
||||
interface DailyMarketingsTableProps {
|
||||
dailyMarketingsReportUrl: string;
|
||||
onSetPage: (page: number) => void;
|
||||
pageSize: number;
|
||||
onSetPageSize: (pageSize: number) => void;
|
||||
searchValue: string;
|
||||
onSearchChange: ChangeEventHandler<HTMLInputElement>;
|
||||
onFilterByChange: (filterBy: string) => void;
|
||||
onSortByChange: (sort: 'asc' | 'desc' | '') => void;
|
||||
}
|
||||
|
||||
const DailyMarketingsTable = ({
|
||||
dailyMarketingsReportUrl,
|
||||
onSetPage,
|
||||
pageSize,
|
||||
onSetPageSize,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onFilterByChange,
|
||||
onSortByChange,
|
||||
}: DailyMarketingsTableProps) => {
|
||||
const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR(
|
||||
dailyMarketingsReportUrl,
|
||||
MarketingReportApi.getAllDailyMarketingFetcher,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const dailyMarketingColumns: ColumnDef<DailyMarketingRow>[] = [
|
||||
{
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
header: 'Tanggal Jual',
|
||||
cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'),
|
||||
footer: 'Total',
|
||||
},
|
||||
{
|
||||
accessorKey: 'realization_date',
|
||||
header: 'Tanggal Realisasi',
|
||||
cell: (props) =>
|
||||
formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'aging_days',
|
||||
header: 'Aging',
|
||||
cell: (props) => `${props.row.original.aging_days} hari`,
|
||||
},
|
||||
{
|
||||
accessorKey: 'warehouse',
|
||||
header: 'Gudang',
|
||||
cell: ({ row }) => row.original.warehouse.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'customer',
|
||||
header: 'Pelanggan',
|
||||
cell: ({ row }) => row.original.customer.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'do_number',
|
||||
header: 'No. DO',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_person',
|
||||
header: 'Sales/Marketing',
|
||||
cell: (props) => props.row.original.sales.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'vehicle_number',
|
||||
header: 'No. Polisi',
|
||||
cell: (props) => (
|
||||
<span className='text-nowrap'>
|
||||
{formatVechicleNumber(props.row.original.vehicle_number)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'marketing_type',
|
||||
header: 'Marketing Type',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'product',
|
||||
header: 'Produk',
|
||||
cell: ({ row }) => row.original.product.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'qty',
|
||||
header: 'Kuantitas',
|
||||
cell: (props) => formatNumber(props.row.original.qty),
|
||||
footer: () => {
|
||||
const totalQty = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.total?.total_qty
|
||||
: 0;
|
||||
|
||||
return totalQty ? formatNumber(totalQty) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'average_weight',
|
||||
header: 'Bobot Rata-Rata (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||
footer: () => {
|
||||
const totalAverageWeightKg = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.total?.average_weight_kg
|
||||
: 0;
|
||||
|
||||
return totalAverageWeightKg ? formatNumber(totalAverageWeightKg) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_weight',
|
||||
header: 'Bobot Total (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||
footer: () => {
|
||||
const totalWeightKg = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.total?.total_weight_kg
|
||||
: 0;
|
||||
|
||||
return totalWeightKg ? formatNumber(totalWeightKg) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_price',
|
||||
header: 'Harga Jual (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||
footer: () => {
|
||||
const totalSalesPrice = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.total?.average_sales_price
|
||||
: 0;
|
||||
|
||||
return totalSalesPrice ? formatNumber(totalSalesPrice) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'hpp_price',
|
||||
header: 'HPP (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||
footer: () => {
|
||||
const totalHppPricePerKg = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.total?.total_hpp_price_per_kg
|
||||
: 0;
|
||||
|
||||
return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_amount',
|
||||
header: 'Total (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
||||
footer: () => {
|
||||
const totalSalesAmount = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.total?.total_sales_amount
|
||||
: 0;
|
||||
|
||||
return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (sorting.length === 1) {
|
||||
onFilterByChange(sorting[0].id);
|
||||
onSortByChange(sorting[0].desc ? 'desc' : 'asc');
|
||||
} else {
|
||||
onFilterByChange('');
|
||||
onSortByChange('');
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setOpen(
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.length > 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}, [dailyMarketings, isResponseSuccess]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
>
|
||||
<Collapse
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Penjualan Harian</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
width={24}
|
||||
height={24}
|
||||
className={cn('text-primary transition-transform', {
|
||||
'-rotate-180': open,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className='w-full!'
|
||||
titleClassName='w-full p-0!'
|
||||
>
|
||||
<div className='w-full p-0'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Penjualan Harian'
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<DailyMarketingRow>
|
||||
data={
|
||||
isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : []
|
||||
}
|
||||
columns={dailyMarketingColumns}
|
||||
pageSize={pageSize}
|
||||
onPageSizeChange={onSetPageSize}
|
||||
rowOptions={[10, 20, 50, 100]}
|
||||
page={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={onSetPage}
|
||||
isLoading={isLoadingDailyMarketings}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
renderFooter={true}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'w-full mb-20':
|
||||
isResponseSuccess(dailyMarketings) &&
|
||||
dailyMarketings?.data?.length === 0,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingsTable;
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useState } from 'react';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent';
|
||||
import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab';
|
||||
|
||||
type MarketingReportTabType =
|
||||
| 'daily'
|
||||
| 'transaction'
|
||||
| 'hpp-comparison'
|
||||
| 'daily-hpp';
|
||||
|
||||
const marketingReportTabs: {
|
||||
id: MarketingReportTabType;
|
||||
label: string;
|
||||
content: JSX.Element;
|
||||
}[] = [
|
||||
{
|
||||
id: 'daily',
|
||||
label: 'Penjualan Harian',
|
||||
content: <DailyMarketingReportContent />,
|
||||
},
|
||||
{
|
||||
id: 'daily-hpp',
|
||||
label: 'HPP Harian Kandang',
|
||||
content: <HppPerKandangTab />,
|
||||
},
|
||||
];
|
||||
|
||||
const MarketingReportContent = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('daily');
|
||||
|
||||
return (
|
||||
<section className='w-full max-w-full pb-16'>
|
||||
<Tabs
|
||||
activeTabId={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
tabs={marketingReportTabs}
|
||||
variant='lifted'
|
||||
className={{
|
||||
content: '-m-px pl-px',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingReportContent;
|
||||
@@ -0,0 +1,264 @@
|
||||
'use client';
|
||||
|
||||
import { Page, View, Document, StyleSheet, Font } from '@react-pdf/renderer';
|
||||
import {
|
||||
DailyMarketingReport,
|
||||
SalesSummary,
|
||||
} from '@/types/api/report/marketing';
|
||||
import {
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatTitleCase,
|
||||
} from '@/lib/helper';
|
||||
import {
|
||||
PdfTable,
|
||||
PdfColumn,
|
||||
PdfTbodyCell,
|
||||
PdfTfootCell,
|
||||
} from '@/components/helper/pdf/table';
|
||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface DailyMarketingReportPDFProps {
|
||||
data?: DailyMarketingReport;
|
||||
total?: SalesSummary;
|
||||
}
|
||||
|
||||
const getTableColumns = (): PdfColumn[] => [
|
||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
||||
{ key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' },
|
||||
{
|
||||
key: 'do_date',
|
||||
header: 'Tanggal Delivery Order',
|
||||
flex: 1.3,
|
||||
align: 'center',
|
||||
},
|
||||
{ key: 'aging', header: 'Aging (Hari)', flex: 0.7, align: 'center' },
|
||||
{ key: 'warehouse', header: 'Gudang', flex: 1.2, align: 'left' },
|
||||
{ key: 'customer', header: 'Pelanggan', flex: 1.5, align: 'left' },
|
||||
{ key: 'sales', header: 'Sales', flex: 1, align: 'left' },
|
||||
{ key: 'product', header: 'Produk', flex: 1.3, align: 'left' },
|
||||
{ key: 'do_number', header: 'Nomor DO', flex: 1.2, align: 'left' },
|
||||
{ key: 'vehicle', header: 'Nomor Polisi', flex: 1, align: 'left' },
|
||||
{ key: 'marketing_type', header: 'Tipe Marketing', flex: 1, align: 'center' },
|
||||
{ key: 'qty', header: 'Quantity', flex: 0.7, align: 'right' },
|
||||
{ key: 'avg_weight', header: 'Rata-Rata (Kg)', flex: 0.8, align: 'right' },
|
||||
{
|
||||
key: 'total_weight',
|
||||
header: 'Total Berat (Kg)',
|
||||
flex: 0.9,
|
||||
align: 'right',
|
||||
},
|
||||
{ key: 'sales_price', header: 'Harga Jual (Rp)', flex: 0.9, align: 'right' },
|
||||
{ key: 'hpp_price', header: 'HPP (Rp)', flex: 1.3, align: 'right' },
|
||||
{ key: 'sales_amount', header: 'Total Jual (Rp)', flex: 1, align: 'right' },
|
||||
{ key: 'hpp_amount', header: 'Total HPP (Rp)', flex: 1.3, align: 'right' },
|
||||
];
|
||||
|
||||
const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => {
|
||||
return rows.map((row, index) => [
|
||||
{ key: 'no', value: index + 1 },
|
||||
{
|
||||
key: 'so_date',
|
||||
value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-',
|
||||
},
|
||||
{
|
||||
key: 'do_date',
|
||||
value: row.realization_date
|
||||
? formatDate(row.realization_date, 'DD MMM YY')
|
||||
: '-',
|
||||
},
|
||||
{ key: 'aging', value: row.aging_days ?? '-' },
|
||||
{ key: 'warehouse', value: row.warehouse?.name ?? '-' },
|
||||
{ key: 'customer', value: row.customer?.name ?? '-' },
|
||||
{ key: 'sales', value: row.sales?.name ?? '-' },
|
||||
{ key: 'product', value: row.product?.name ?? '-' },
|
||||
{ key: 'do_number', value: row.do_number ?? '-' },
|
||||
{ key: 'vehicle', value: row.vehicle_number ?? '-' },
|
||||
{
|
||||
key: 'marketing_type',
|
||||
value: row.marketing_type ? (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<PdfStatusBadge
|
||||
style={{
|
||||
backgroundColor:
|
||||
row.marketing_type.toLowerCase() === 'ayam'
|
||||
? '#FEF3C7'
|
||||
: row.marketing_type.toLowerCase() === 'trading'
|
||||
? '#DBEAFE'
|
||||
: row.marketing_type.toLowerCase() === 'telur'
|
||||
? '#D1FAE5'
|
||||
: '#F5F5F5',
|
||||
color:
|
||||
row.marketing_type.toLowerCase() === 'ayam'
|
||||
? '#92400E'
|
||||
: row.marketing_type.toLowerCase() === 'trading'
|
||||
? '#1E40AF'
|
||||
: row.marketing_type.toLowerCase() === 'telur'
|
||||
? '#065F46'
|
||||
: '#333333',
|
||||
borderColor:
|
||||
row.marketing_type.toLowerCase() === 'ayam'
|
||||
? '#FBBF24'
|
||||
: row.marketing_type.toLowerCase() === 'trading'
|
||||
? '#60A5FA'
|
||||
: row.marketing_type.toLowerCase() === 'telur'
|
||||
? '#34D399'
|
||||
: '#E5E7EB',
|
||||
}}
|
||||
>
|
||||
{formatTitleCase(row.marketing_type)}
|
||||
</PdfStatusBadge>
|
||||
</View>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{ key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' },
|
||||
{
|
||||
key: 'avg_weight',
|
||||
value: formatNumber(row.average_weight_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'total_weight',
|
||||
value: formatNumber(row.total_weight_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sales_price',
|
||||
value: formatCurrency(row.sales_price_per_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_price',
|
||||
value: formatCurrency(row.hpp_price_per_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sales_amount',
|
||||
value: formatCurrency(row.sales_amount ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_amount',
|
||||
value: formatCurrency(row.hpp_amount ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => {
|
||||
if (!summary) return [];
|
||||
|
||||
return [
|
||||
{ key: 'no', value: 'TOTAL' },
|
||||
{ key: 'so_date', value: '' },
|
||||
{ key: 'do_date', value: '' },
|
||||
{ key: 'aging', value: '' },
|
||||
{ key: 'warehouse', value: '' },
|
||||
{ key: 'customer', value: '' },
|
||||
{ key: 'sales', value: '' },
|
||||
{ key: 'product', value: '' },
|
||||
{ key: 'do_number', value: '' },
|
||||
{ key: 'vehicle', value: '' },
|
||||
{ key: 'marketing_type', value: '' },
|
||||
{
|
||||
key: 'qty',
|
||||
value: formatNumber(summary.total_qty ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'avg_weight',
|
||||
value: formatNumber(summary.total_weight_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'total_weight',
|
||||
value: formatNumber(summary.total_weight_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{ key: 'sales_price', value: '' },
|
||||
{
|
||||
key: 'hpp_price',
|
||||
value: formatCurrency(summary.total_hpp_price_per_kg ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sales_amount',
|
||||
value: formatCurrency(summary.total_sales_amount ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_amount',
|
||||
value: formatCurrency(summary.total_hpp_amount ?? 0),
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const DailyMarketingReportPDF = ({
|
||||
data,
|
||||
total,
|
||||
}: DailyMarketingReportPDFProps) => {
|
||||
const rows = data || [];
|
||||
const summary = total;
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||
{/* Title and Parameters */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan > Penjualan Harian
|
||||
</PdfTypography>
|
||||
<View style={pdfStyles.parameterContainer}>
|
||||
<PdfParamBadge>
|
||||
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</PdfParamBadge>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
<PdfTable
|
||||
columns={getTableColumns()}
|
||||
data={getTableData(rows)}
|
||||
footer={getTableFooter(summary)}
|
||||
footerLabel='TOTAL'
|
||||
/>
|
||||
|
||||
<PdfPageNumber />
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingReportPDF;
|
||||
@@ -0,0 +1,415 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Page,
|
||||
View,
|
||||
Document,
|
||||
StyleSheet,
|
||||
Font,
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
import {
|
||||
HppPerKandangReport,
|
||||
HppPerKandangRow,
|
||||
HppPerKandangPerWeightRange,
|
||||
} from '@/types/api/report/hpp-per-kandang';
|
||||
import { formatDate, formatNumber, formatCurrency } from '@/lib/helper';
|
||||
import {
|
||||
PdfTable,
|
||||
PdfColumn,
|
||||
PdfTbodyCell,
|
||||
PdfTfootCell,
|
||||
} from '@/components/helper/pdf/table';
|
||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 8,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
});
|
||||
|
||||
interface HppPerKandangExportParams {
|
||||
data: HppPerKandangReport;
|
||||
params?: {
|
||||
area_name?: string;
|
||||
location_name?: string;
|
||||
kandang_name?: string;
|
||||
period?: string;
|
||||
weight_min?: string;
|
||||
weight_max?: string;
|
||||
show_unrecorded?: string;
|
||||
sort_by?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const formatSuppliers = (
|
||||
suppliers: { alias?: string; name: string }[] | null | undefined
|
||||
): string => {
|
||||
if (!suppliers || suppliers.length === 0) return '-';
|
||||
return suppliers.map((s) => s.alias || s.name).join(' | ');
|
||||
};
|
||||
|
||||
// Helper functions for PdfTable - Rekapitulasi
|
||||
const getRekapitulasiColumns = (): PdfColumn[] => [
|
||||
{ key: 'rentang_bw', header: 'Rentang BW', flex: 1.2, align: 'center' },
|
||||
{ key: 'sisa_butir', header: 'Sisa Butir', flex: 1, align: 'right' },
|
||||
{ key: 'sisa_kg', header: 'Sisa Kg', flex: 1, align: 'right' },
|
||||
{
|
||||
key: 'rata_rata_bobot',
|
||||
header: 'Rata-Rata Bobot (Kg)',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'feed_supplier',
|
||||
header: 'Feed (Supplier)',
|
||||
flex: 1.5,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
key: 'doc_supplier',
|
||||
header: 'DOC (Supplier)',
|
||||
flex: 1.2,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
key: 'rata_harga_doc',
|
||||
header: 'Rata-Rata Harga DOC',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_telur',
|
||||
header: 'HPP Telur (Rp/Kg)',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'nominal_sisa',
|
||||
header: 'Nominal Sisa',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const getRekapitulasiData = (
|
||||
perWeightRange: HppPerKandangPerWeightRange[]
|
||||
): PdfTbodyCell[][] => {
|
||||
return perWeightRange.map((group) => [
|
||||
{ key: 'rentang_bw', value: group.label || '-' },
|
||||
{
|
||||
key: 'sisa_butir',
|
||||
value: formatNumber(group.egg_production_pieces || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sisa_kg',
|
||||
value: formatNumber(group.egg_production_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'rata_rata_bobot',
|
||||
value: formatNumber(group.avg_weight_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'feed_supplier',
|
||||
value: formatSuppliers(group.feed_suppliers),
|
||||
},
|
||||
{
|
||||
key: 'doc_supplier',
|
||||
value: formatSuppliers(group.doc_suppliers),
|
||||
},
|
||||
{
|
||||
key: 'rata_harga_doc',
|
||||
value: formatCurrency(group.average_doc_price_rp || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_telur',
|
||||
value: formatCurrency(group.egg_hpp_rp_per_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'nominal_sisa',
|
||||
value: formatCurrency(group.egg_value_rp || 0),
|
||||
align: 'right',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Helper functions for PdfTable - Detail Per Kandang
|
||||
const getDetailColumns = (): PdfColumn[] => [
|
||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
||||
{ key: 'kandang', header: 'Kandang', flex: 1.5, align: 'left' },
|
||||
{ key: 'rentang_bw', header: 'Rentang BW', flex: 1, align: 'left' },
|
||||
{
|
||||
key: 'rata_rata_bobot',
|
||||
header: 'Rata-Rata Bobot (Kg)',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
},
|
||||
{ key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' },
|
||||
{ key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' },
|
||||
{
|
||||
key: 'feed_supplier',
|
||||
header: 'Feed (Supplier)',
|
||||
flex: 1.2,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
key: 'doc_supplier',
|
||||
header: 'DOC (Supplier)',
|
||||
flex: 1,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
key: 'rata_harga_doc',
|
||||
header: 'Rata-Rata Harga DOC',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_telur',
|
||||
header: 'HPP Telur (Rp/Kg)',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'nominal_sisa',
|
||||
header: 'Nominal Sisa',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => {
|
||||
return rows.map((item, index) => [
|
||||
{ key: 'no', value: index + 1 },
|
||||
{ key: 'kandang', value: item.kandang?.name || '-' },
|
||||
{
|
||||
key: 'rentang_bw',
|
||||
value: `${item.weight_range.weight_min.toFixed(2)} - ${item.weight_range.weight_max.toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
key: 'rata_rata_bobot',
|
||||
value: formatNumber(item.avg_weight_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sisa_butir',
|
||||
value: formatNumber(item.egg_production_pieces || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sisa_kg',
|
||||
value: formatNumber(item.egg_production_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'feed_supplier',
|
||||
value: formatSuppliers(item.feed_suppliers),
|
||||
},
|
||||
{
|
||||
key: 'doc_supplier',
|
||||
value: formatSuppliers(item.doc_suppliers),
|
||||
},
|
||||
{
|
||||
key: 'rata_harga_doc',
|
||||
value: formatCurrency(item.average_doc_price_rp || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_telur',
|
||||
value: formatCurrency(item.egg_hpp_rp_per_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'nominal_sisa',
|
||||
value: formatCurrency(item.egg_value_rp || 0),
|
||||
align: 'right',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const getDetailFooter = (
|
||||
summary: HppPerKandangReport['summary'],
|
||||
allFeedSuppliers: string,
|
||||
allDocSuppliers: string
|
||||
): PdfTfootCell[] => {
|
||||
if (!summary?.total) return [];
|
||||
|
||||
return [
|
||||
{ key: 'no', value: 'TOTAL' },
|
||||
{ key: 'kandang', value: 'ALL' },
|
||||
{ key: 'rentang_bw', value: '-' },
|
||||
{
|
||||
key: 'rata_rata_bobot',
|
||||
value: formatNumber(summary.total.average_weight_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sisa_butir',
|
||||
value: formatNumber(summary.total.total_egg_production_pieces || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'sisa_kg',
|
||||
value: formatNumber(summary.total.total_egg_production_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{ key: 'feed_supplier', value: allFeedSuppliers },
|
||||
{ key: 'doc_supplier', value: allDocSuppliers },
|
||||
{
|
||||
key: 'rata_harga_doc',
|
||||
value: formatCurrency(summary.total.total_average_doc_price_rp || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'hpp_telur',
|
||||
value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'nominal_sisa',
|
||||
value: formatCurrency(summary.total.total_egg_value_rp || 0),
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const createPDFDocument = (
|
||||
params: HppPerKandangExportParams,
|
||||
allFeedSuppliers: string,
|
||||
allDocSuppliers: string
|
||||
) => {
|
||||
const rekapitulasiByWeightRange = params.data.summary?.per_weight_range || [];
|
||||
|
||||
const weightRangeText =
|
||||
params.params?.weight_min || params.params?.weight_max
|
||||
? params.params.weight_min && params.params.weight_max
|
||||
? `${params.params.weight_min} - ${params.params.weight_max} kg`
|
||||
: params.params.weight_min
|
||||
? `≥ ${params.params.weight_min} kg`
|
||||
: `≤ ${params.params.weight_max} kg`
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||
{/* Title and Parameters */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan > HPP Harian Kandang
|
||||
</PdfTypography>
|
||||
<View style={pdfStyles.parameterContainer}>
|
||||
<PdfParamBadge>
|
||||
Area: {params.params?.area_name || 'Semua Area'}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Lokasi: {params.params?.location_name || 'Semua Lokasi'}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Kandang: {params.params?.kandang_name || 'Semua Kandang'}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Periode:{' '}
|
||||
{params.params?.period
|
||||
? formatDate(params.params.period, 'DD MMM YYYY')
|
||||
: '-'}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>Rentang Bobot: {weightRangeText}</PdfParamBadge>
|
||||
{params.params?.show_unrecorded === 'true' && (
|
||||
<PdfParamBadge>Tampilkan: Tanpa Recording</PdfParamBadge>
|
||||
)}
|
||||
<PdfParamBadge>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</PdfParamBadge>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Rekapitulasi Section */}
|
||||
<View style={pdfStyles.section}>
|
||||
<PdfTypography size='h2' variant='primary'>
|
||||
Rekapitulasi
|
||||
</PdfTypography>
|
||||
<PdfTable
|
||||
columns={getRekapitulasiColumns()}
|
||||
data={getRekapitulasiData(rekapitulasiByWeightRange)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Detail Per Kandang Section */}
|
||||
<View style={pdfStyles.section}>
|
||||
<PdfTypography size='h2' variant='primary'>
|
||||
Detail Per Kandang
|
||||
</PdfTypography>
|
||||
<PdfTable
|
||||
columns={getDetailColumns()}
|
||||
data={getDetailData(params.data.rows)}
|
||||
footer={
|
||||
params.data.summary
|
||||
? getDetailFooter(
|
||||
params.data.summary,
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
footerLabel='TOTAL'
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export const generateHppPerKandangPDF = async (
|
||||
params: HppPerKandangExportParams,
|
||||
allFeedSuppliers: string,
|
||||
allDocSuppliers: string
|
||||
): Promise<void> => {
|
||||
const PDFDocument = createPDFDocument(
|
||||
params,
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers
|
||||
);
|
||||
|
||||
try {
|
||||
const blob = await pdf(PDFDocument).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
const period =
|
||||
params.params?.period || formatDate(new Date(), 'YYYY-MM-DD');
|
||||
link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
|
||||
import ExcelJS from 'exceljs';
|
||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
HppPerKandangReport,
|
||||
HppPerKandangRow,
|
||||
HppPerKandangPerWeightRange,
|
||||
} from '@/types/api/report/hpp-per-kandang';
|
||||
|
||||
interface HppPerKandangExportExcelParams {
|
||||
data: HppPerKandangReport;
|
||||
allFeedSuppliers: string;
|
||||
allDocSuppliers: string;
|
||||
}
|
||||
|
||||
const formatSuppliers = (
|
||||
suppliers: { alias?: string; name: string }[] | null
|
||||
): string => {
|
||||
if (!suppliers || suppliers.length === 0) return '';
|
||||
return suppliers.map((s) => s.alias || s.name).join(' | ');
|
||||
};
|
||||
|
||||
export const generateHppPerKandangExcel = async (
|
||||
params: HppPerKandangExportExcelParams
|
||||
): Promise<void> => {
|
||||
if (!params.data || !params.data.rows || params.data.rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
|
||||
// ===== REKAPITULASI WORKSHEET =====
|
||||
const rekapitulasiColumns = [
|
||||
{ header: 'No', key: 'no', width: 5 },
|
||||
{ header: 'Rentang BW', key: 'weightRange', width: 15 },
|
||||
{ header: 'Sisa Butir', key: 'eggPieces', width: 15 },
|
||||
{ header: 'Sisa Kg', key: 'eggKg', width: 12 },
|
||||
{ header: 'Rata-Rata Bobot (Kg)', key: 'avgWeight', width: 18 },
|
||||
{ header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 },
|
||||
{ header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 },
|
||||
{ header: 'Rata-Rata Harga DOC', key: 'avgDocPrice', width: 20 },
|
||||
{ header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 },
|
||||
{ header: 'Nominal Sisa', key: 'eggValue', width: 25 },
|
||||
];
|
||||
|
||||
const rekapitulasiWorksheet = workbook.addWorksheet('Rekapitulasi');
|
||||
rekapitulasiWorksheet.columns = rekapitulasiColumns;
|
||||
|
||||
const perWeightRangeSummary = params.data.summary.per_weight_range || [];
|
||||
|
||||
perWeightRangeSummary.forEach(
|
||||
(item: HppPerKandangPerWeightRange, index: number) => {
|
||||
rekapitulasiWorksheet.addRow({
|
||||
no: index + 1,
|
||||
weightRange: item.label || '',
|
||||
eggPieces: formatNumber(item.egg_production_pieces || 0),
|
||||
eggKg: formatNumber(item.egg_production_kg || 0),
|
||||
avgWeight: formatNumber(item.avg_weight_kg || 0),
|
||||
feedSuppliers: formatSuppliers(item.feed_suppliers),
|
||||
docSuppliers: formatSuppliers(item.doc_suppliers),
|
||||
avgDocPrice: formatCurrency(item.average_doc_price_rp || 0),
|
||||
eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0),
|
||||
eggValue: formatCurrency(item.egg_value_rp || 0),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// ===== DETAIL PER KANDANG WORKSHEET =====
|
||||
const detailColumns = [
|
||||
{ header: 'No', key: 'no', width: 5 },
|
||||
{ header: 'Kandang', key: 'kandang', width: 30 },
|
||||
{ header: 'Rentang Bobot', key: 'weightRange', width: 15 },
|
||||
{ header: 'Rata-Rata Bobot (KG)', key: 'avgWeightKg', width: 18 },
|
||||
{ header: 'Sisa Telur (Butir)', key: 'eggPieces', width: 15 },
|
||||
{ header: 'Sisa Telur (KG)', key: 'eggKg', width: 15 },
|
||||
{ header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 },
|
||||
{ header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 },
|
||||
{ header: 'Rata-Rata Harga DOC (Rp)', key: 'avgDocPrice', width: 20 },
|
||||
{ header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 },
|
||||
{ header: 'Nilai Nominal Sisa Telur (Rp)', key: 'eggValue', width: 25 },
|
||||
];
|
||||
|
||||
const detailWorksheet = workbook.addWorksheet('Detail Per Kandang');
|
||||
detailWorksheet.columns = detailColumns;
|
||||
|
||||
const allExportData = params.data.rows;
|
||||
|
||||
allExportData.forEach((item: HppPerKandangRow, index: number) => {
|
||||
detailWorksheet.addRow({
|
||||
no: index + 1,
|
||||
kandang: item.kandang?.name || '',
|
||||
weightRange: item.weight_range
|
||||
? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
|
||||
: '',
|
||||
avgWeightKg: formatNumber(item.avg_weight_kg || 0),
|
||||
eggPieces: formatNumber(item.egg_production_pieces || 0),
|
||||
eggKg: formatNumber(item.egg_production_kg || 0),
|
||||
feedSuppliers: formatSuppliers(item.feed_suppliers),
|
||||
docSuppliers: formatSuppliers(item.doc_suppliers),
|
||||
avgDocPrice: formatCurrency(item.average_doc_price_rp || 0),
|
||||
eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0),
|
||||
eggValue: formatCurrency(item.egg_value_rp || 0),
|
||||
});
|
||||
});
|
||||
|
||||
// Add TOTAL row
|
||||
const summaryTotal = params.data.summary.total;
|
||||
detailWorksheet.addRow({
|
||||
no: 'TOTAL',
|
||||
kandang: 'ALL',
|
||||
weightRange: '-',
|
||||
avgWeightKg: formatNumber(summaryTotal?.average_weight_kg || 0),
|
||||
eggPieces: formatNumber(summaryTotal?.total_egg_production_pieces || 0),
|
||||
eggKg: formatNumber(summaryTotal?.total_egg_production_kg || 0),
|
||||
feedSuppliers: params.allFeedSuppliers,
|
||||
docSuppliers: params.allDocSuppliers,
|
||||
avgDocPrice: formatCurrency(summaryTotal?.total_average_doc_price_rp || 0),
|
||||
eggHpp: formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0),
|
||||
eggValue: formatCurrency(summaryTotal?.total_egg_value_rp || 0),
|
||||
});
|
||||
|
||||
const filename = `laporan-hpp-harian-kandang-periode-${params.data.period}.xlsx`;
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
@@ -0,0 +1,472 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { pdf } from '@react-pdf/renderer';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/dropdown/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 DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF';
|
||||
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import {
|
||||
AreaApi,
|
||||
CustomerApi,
|
||||
LocationApi,
|
||||
WarehouseApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { MarketingReportApi } from '@/services/api/report/marketing-report';
|
||||
import {
|
||||
MARKETING_DATE_FILTER_TYPE_OPTIONS,
|
||||
MARKETING_TYPE_OPTIONS,
|
||||
} from '@/config/constant';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
DailyMarketingReport,
|
||||
DailyMarketingReportResponse,
|
||||
} from '@/types/api/report/marketing';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
|
||||
const DailyMarketingReportContent = () => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
reset: resetFilter,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
area_id: '',
|
||||
location_id: '',
|
||||
warehouse_id: '',
|
||||
customer_id: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
marketing_type: '',
|
||||
filter_by: '',
|
||||
sort_by: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
area_id: 'area_id',
|
||||
location_id: 'location_id',
|
||||
warehouse_id: 'warehouse_id',
|
||||
customer_id: 'customer_id',
|
||||
start_date: 'start_date',
|
||||
end_date: 'end_date',
|
||||
marketing_type: 'marketing_type',
|
||||
filter_by: 'filter_by',
|
||||
sort_by: 'sort_by',
|
||||
},
|
||||
});
|
||||
|
||||
const dailyMarketingsReportUrl = `${MarketingReportApi.basePath}${getTableFilterQueryString()}`;
|
||||
|
||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||
useState(false);
|
||||
const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false);
|
||||
|
||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||
const {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedArea(val as OptionType);
|
||||
updateFilter('area_id', val ? ((val as OptionType).value as string) : '');
|
||||
};
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedLocation(val as OptionType);
|
||||
updateFilter(
|
||||
'location_id',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
loadMore: loadMoreWarehouses,
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||
|
||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedWarehouse(val as OptionType);
|
||||
updateFilter(
|
||||
'warehouse_id',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const {
|
||||
setInputValue: setCustomerInputValue,
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
loadMore: loadMoreCustomers,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedCustomer(val as OptionType);
|
||||
updateFilter(
|
||||
'customer_id',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const startDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFilter('start_date', e.target.value ? e.target.value : '');
|
||||
};
|
||||
|
||||
const endDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFilter('end_date', e.target.value ? e.target.value : '');
|
||||
};
|
||||
|
||||
const [selectedMarketingDateFilterType, setSelectedMarketingDateFilterType] =
|
||||
useState<OptionType | null>(null);
|
||||
const marketingDateFilterTypeChangeHandler = (
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
setSelectedMarketingDateFilterType(val as OptionType);
|
||||
updateFilter('filter_by', val ? ((val as OptionType).value as string) : '');
|
||||
};
|
||||
|
||||
const [selectedMarketingType, setSelectedMarketingType] =
|
||||
useState<OptionType | null>(null);
|
||||
const marketingTypeChangeHandler = (
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
setSelectedMarketingType(val as OptionType);
|
||||
updateFilter(
|
||||
'marketing_type',
|
||||
val ? ((val as OptionType).value as string) : ''
|
||||
);
|
||||
};
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const filterByChangeHandler = (filterBy: string) => {
|
||||
updateFilter('filter_by', filterBy);
|
||||
};
|
||||
|
||||
const sortByChangeHandler = (sort: 'asc' | 'desc' | '') => {
|
||||
updateFilter('sort_by', sort);
|
||||
};
|
||||
|
||||
const exportToExcelHandler = async () => {
|
||||
setIsLoadingExportingToExcel(true);
|
||||
|
||||
await MarketingReportApi.exportDailyMarketingToExcel(
|
||||
getTableFilterQueryString()
|
||||
);
|
||||
|
||||
setIsLoadingExportingToExcel(false);
|
||||
};
|
||||
|
||||
const exportToPdfHandler = async () => {
|
||||
setIsLoadingExportingToPdf(true);
|
||||
|
||||
const params = new URLSearchParams(getTableFilterQueryString());
|
||||
|
||||
params.set('limit', '9999999');
|
||||
|
||||
const queryString = `?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const dailyMarketingsReport =
|
||||
await httpClient<DailyMarketingReportResponse>(
|
||||
`${MarketingReportApi.basePath}${queryString}`
|
||||
);
|
||||
|
||||
if (isResponseError(dailyMarketingsReport)) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
const openPdf = async () => {
|
||||
const dailyMarketingReportPdfBlob = await pdf(
|
||||
<DailyMarketingReportPDF
|
||||
data={dailyMarketingsReport.data}
|
||||
total={dailyMarketingsReport.total}
|
||||
/>
|
||||
).toBlob();
|
||||
|
||||
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||
dailyMarketingReportPdfBlob
|
||||
);
|
||||
window.open(dailyMarketingReportPdfUrl, '_blank');
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
const blob = await pdf(
|
||||
<DailyMarketingReportPDF
|
||||
data={dailyMarketingsReport.data}
|
||||
total={dailyMarketingsReport.total}
|
||||
/>
|
||||
).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'laporan-penjualan-harian.pdf';
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
await openPdf();
|
||||
} catch (error) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
}
|
||||
|
||||
setIsLoadingExportingToPdf(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedArea(null);
|
||||
setSelectedLocation(null);
|
||||
setSelectedWarehouse(null);
|
||||
setSelectedCustomer(null);
|
||||
setSelectedMarketingType(null);
|
||||
resetFilter();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
tableFilterState.filter_by === 'realization_date' ||
|
||||
tableFilterState.filter_by === 'so_date'
|
||||
) {
|
||||
setSelectedMarketingDateFilterType({
|
||||
label:
|
||||
tableFilterState.filter_by === 'realization_date'
|
||||
? 'Tanggal Realisasi'
|
||||
: 'Tanggal SO',
|
||||
value: tableFilterState.filter_by,
|
||||
});
|
||||
} else {
|
||||
setSelectedMarketingDateFilterType(null);
|
||||
}
|
||||
}, [tableFilterState.filter_by]);
|
||||
|
||||
return (
|
||||
<div className='w-full border border-gray-200 p-4'>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold text-center'>Penjualan Harian</h2>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className='flex flex-col gap-4 mb-6'>
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
value={selectedArea}
|
||||
onChange={areaChangeHandler}
|
||||
onInputChange={setAreaInputValue}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Gudang'
|
||||
placeholder='Pilih Gudang'
|
||||
options={warehouseOptions}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
value={selectedWarehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Customer'
|
||||
placeholder='Pilih Customer'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
value={selectedCustomer}
|
||||
onChange={customerChangeHandler}
|
||||
onInputChange={setCustomerInputValue}
|
||||
onMenuScrollToBottom={loadMoreCustomers}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
name='startDate'
|
||||
label='Tanggal Awal'
|
||||
placeholder='Tanggal Realisasi'
|
||||
value={tableFilterState.start_date}
|
||||
onChange={startDateChangeHandler}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
name='endDate'
|
||||
label='Tanggal Akhir'
|
||||
placeholder='Tanggal Realisasi'
|
||||
value={tableFilterState.end_date}
|
||||
onChange={endDateChangeHandler}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 gap-4'>
|
||||
<SelectInput
|
||||
label='Filter Tanggal'
|
||||
placeholder='Pilih Filter Tanggal'
|
||||
options={MARKETING_DATE_FILTER_TYPE_OPTIONS}
|
||||
value={selectedMarketingDateFilterType}
|
||||
onChange={marketingDateFilterTypeChangeHandler}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Tipe Marketing'
|
||||
placeholder='Pilih Tipe Marketing'
|
||||
options={MARKETING_TYPE_OPTIONS}
|
||||
value={selectedMarketingType}
|
||||
onChange={marketingTypeChangeHandler}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className='col-span-12 sm:col-span-6 lg:col-span-8 flex flex-wrap sm:justify-end items-end gap-2'>
|
||||
<Button
|
||||
color='primary'
|
||||
// onClick={handleSearch}
|
||||
className='flex-1 sm:flex-none'
|
||||
>
|
||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||
Cari
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={handleReset}
|
||||
className='flex-1 sm:flex-none'
|
||||
>
|
||||
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
align='end'
|
||||
direction='bottom'
|
||||
trigger={
|
||||
<Button>
|
||||
Export{' '}
|
||||
<Icon
|
||||
icon='heroicons-outline:download'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
title='Export to Excel'
|
||||
icon='icon-park-outline:excel'
|
||||
isLoading={isLoadingExportingToExcel}
|
||||
onClick={exportToExcelHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
<MenuItem
|
||||
title='Export to PDF'
|
||||
icon='icon-park-outline:file-pdf-one'
|
||||
onClick={exportToPdfHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DailyMarketingsTable
|
||||
dailyMarketingsReportUrl={dailyMarketingsReportUrl}
|
||||
onSetPage={setPage}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
onSetPageSize={setPageSize}
|
||||
searchValue={tableFilterState.search}
|
||||
onSearchChange={searchChangeHandler}
|
||||
onFilterByChange={filterByChangeHandler}
|
||||
onSortByChange={sortByChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingReportContent;
|
||||
@@ -0,0 +1,870 @@
|
||||
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 { 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';
|
||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
HppPerKandangReport,
|
||||
HppPerKandangRow,
|
||||
HppPerKandangPerWeightRange,
|
||||
} 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 '@/components/pages/report/marketing/export/HppPerkandangExportPDF';
|
||||
import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
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);
|
||||
|
||||
// ===== VALIDATION STATE =====
|
||||
const [weightMaxError, setWeightMaxError] = useState<string>('');
|
||||
|
||||
// ===== 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 {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreas,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocations,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
setInputValue: setKandangInputValue,
|
||||
options: kandangOptions,
|
||||
isLoadingOptions: isLoadingKandangs,
|
||||
loadMore: loadMoreKandangs,
|
||||
} = useSelect(
|
||||
ProjectFlockKandangApi.basePath,
|
||||
'id',
|
||||
'name_with_period',
|
||||
'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);
|
||||
|
||||
if (weightMaxError) {
|
||||
setWeightMaxError('');
|
||||
}
|
||||
},
|
||||
[updateFilter, weightMaxError]
|
||||
);
|
||||
|
||||
const weightMaxChangeHandler = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement>
|
||||
>(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
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, tableFilterState.weight_min]
|
||||
);
|
||||
|
||||
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 summaryTotal =
|
||||
isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total
|
||||
? hppPerKandang.data.summary.total
|
||||
: undefined;
|
||||
|
||||
const perWeightRangeSummary = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(hppPerKandang) &&
|
||||
hppPerKandang?.data?.summary?.per_weight_range
|
||||
? hppPerKandang.data.summary.per_weight_range
|
||||
: [],
|
||||
[hppPerKandang]
|
||||
);
|
||||
|
||||
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 allFeedSuppliers = useMemo(() => {
|
||||
const suppliers = new Set<string>();
|
||||
data.forEach((item: HppPerKandangRow) => {
|
||||
item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => {
|
||||
suppliers.add(s.alias || s.name);
|
||||
});
|
||||
});
|
||||
return Array.from(suppliers).join(' | ');
|
||||
}, [data]);
|
||||
|
||||
const allDocSuppliers = useMemo(() => {
|
||||
const suppliers = new Set<string>();
|
||||
data.forEach((item: HppPerKandangRow) => {
|
||||
item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => {
|
||||
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;
|
||||
}
|
||||
|
||||
await generateHppPerKandangExcel({
|
||||
data: allDataForExport,
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers,
|
||||
});
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [hppPerKandangExport, allFeedSuppliers, allDocSuppliers]);
|
||||
|
||||
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(
|
||||
{
|
||||
data: allDataForExport,
|
||||
params: {
|
||||
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,
|
||||
},
|
||||
},
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers
|
||||
);
|
||||
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
hppPerKandangExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
locationOptions,
|
||||
kandangOptions,
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers,
|
||||
]);
|
||||
|
||||
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 row = props.row.original;
|
||||
return row.name_with_periode || row.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(summaryTotal?.average_weight_kg || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_production_pieces',
|
||||
header: 'Sisa 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(summaryTotal?.total_egg_production_pieces || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_production_kg',
|
||||
header: 'Sisa 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(summaryTotal?.total_egg_production_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: { alias?: string; name: string }) => 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: { alias?: string; name: string }) => 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(summaryTotal?.total_average_doc_price_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(summaryTotal?.average_egg_hpp_rp_per_kg || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'egg_value_rp',
|
||||
header: 'Nilai Nominal Sisa 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(summaryTotal?.total_egg_value_rp || 0)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
return tableColumns;
|
||||
};
|
||||
|
||||
// ===== CUSTOM ROW RENDERER =====
|
||||
const renderCustomRow = useCallback(
|
||||
(row: Row<HppPerKandangReport['rows'][0]>) => {
|
||||
if (row.index === data.length - 1) {
|
||||
const defaultRow = (
|
||||
<tr
|
||||
key={row.id}
|
||||
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border-gray-200'
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
|
||||
const customRows = [
|
||||
<tr
|
||||
className='border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||
key={'rekapitulasi-row'}
|
||||
>
|
||||
<td
|
||||
colSpan={11}
|
||||
className='px-4 py-3 text-gray-900 text-center font-semibold'
|
||||
>
|
||||
Rekapitulasi per rentang bobot
|
||||
</td>
|
||||
</tr>,
|
||||
];
|
||||
|
||||
if (perWeightRangeSummary.length > 0) {
|
||||
perWeightRangeSummary.forEach(
|
||||
(item: HppPerKandangPerWeightRange, index = 0) => {
|
||||
customRows.push(
|
||||
<tr
|
||||
key={`summary-${item.id}`}
|
||||
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200 [&_td]:px-4 [&_td]:py-3 [&_td]:text-xs [&_td]:text-gray-900 [&_td]:whitespace-nowrap'
|
||||
>
|
||||
<td className=''>{index + 1}</td>
|
||||
<td className=''>ALL</td>
|
||||
<td className=''>{item.label}</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.avg_weight_kg)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.egg_production_pieces)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatNumber(item.egg_production_kg)}
|
||||
</td>
|
||||
<td className=''>
|
||||
{item.feed_suppliers
|
||||
?.map((s) => s.alias || s.name)
|
||||
.join(' | ') || '-'}
|
||||
</td>
|
||||
<td className=''>
|
||||
{item.doc_suppliers
|
||||
?.map((s) => s.alias || s.name)
|
||||
.join(' | ') || '-'}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.average_doc_price_rp)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.egg_hpp_rp_per_kg)}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
{formatCurrency(item.egg_value_rp)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return [defaultRow, ...customRows];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[data, perWeightRangeSummary]
|
||||
);
|
||||
|
||||
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='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}
|
||||
onInputChange={setAreaInputValue}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isLoading={isLoadingAreas}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
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}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isLoading={isLoadingLocations}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
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}
|
||||
onInputChange={setKandangInputValue}
|
||||
onMenuScrollToBottom={loadMoreKandangs}
|
||||
isLoading={isLoadingKandangs}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
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}
|
||||
isError={!!weightMaxError}
|
||||
errorMessage={weightMaxError}
|
||||
/>
|
||||
</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='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={handleSubmit}
|
||||
disabled={!!weightMaxError}
|
||||
>
|
||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||
Cari
|
||||
</Button>
|
||||
<Button color='warning' onClick={resetFilters}>
|
||||
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||
Reset
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger={
|
||||
<Button color='success' isLoading={isAnyExportLoading}>
|
||||
Export
|
||||
<Icon
|
||||
icon='heroicons-outline:download'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
align='end'
|
||||
>
|
||||
<Menu className='w-32'>
|
||||
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</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;
|
||||
Reference in New Issue
Block a user