mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
fix(FE): resolve merge conflict with branch development
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
||||
|
||||
const MarketingReportPage = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<MarketingReportContent />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingReportPage;
|
||||
@@ -54,8 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Dropdown
|
||||
direction='bottom'
|
||||
align='end'
|
||||
direction='bottom'
|
||||
trigger={
|
||||
<div className='btn btn-ghost btn-circle avatar'>
|
||||
<div className='w-10 rounded-full border flex justify-center items-center'>
|
||||
@@ -67,7 +67,7 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
content: 'w-52 mt-3',
|
||||
}}
|
||||
>
|
||||
<Menu className='p-2 bg-base-100 shadow rounded-box menu-sm'>
|
||||
<Menu>
|
||||
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
|
||||
+13
-6
@@ -21,6 +21,7 @@ export interface TabsProps
|
||||
className?:
|
||||
| string
|
||||
| {
|
||||
container?: string;
|
||||
wrapper?: string;
|
||||
tab?: string;
|
||||
content?: string;
|
||||
@@ -53,10 +54,14 @@ const Tabs = ({
|
||||
onTabChange?.(tabId);
|
||||
};
|
||||
|
||||
const { wrapper: wrapperClassName, tab: tabClassName } =
|
||||
typeof className === 'object'
|
||||
? className
|
||||
: { wrapper: className, tab: undefined };
|
||||
const {
|
||||
container: containerClassName,
|
||||
wrapper: wrapperClassName,
|
||||
tab: tabClassName,
|
||||
content: contentClassName,
|
||||
} = typeof className === 'object'
|
||||
? className
|
||||
: { wrapper: className, tab: undefined };
|
||||
|
||||
const getTabsClasses = () => {
|
||||
const variantClasses: Record<string, string> = {
|
||||
@@ -104,7 +109,7 @@ const Tabs = ({
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-full',
|
||||
typeof className === 'string' ? className : undefined
|
||||
typeof className === 'string' ? className : containerClassName
|
||||
)}
|
||||
>
|
||||
<div role='tablist' className={getTabsClasses()}>
|
||||
@@ -121,7 +126,9 @@ const Tabs = ({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{activeContent && <div className='mt-4'>{activeContent}</div>}
|
||||
{activeContent && (
|
||||
<div className={cn('mt-4', contentClassName)}>{activeContent}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,9 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||
SWRHttpKey
|
||||
>('/sso/userinfo', httpClientFetcher, {
|
||||
shouldRetryOnError: false,
|
||||
|
||||
// refresh every 13 minutes
|
||||
refreshInterval: 13 * 60 * 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,6 +8,7 @@ interface MenuItemProps {
|
||||
href?: string;
|
||||
icon?: string;
|
||||
active?: boolean;
|
||||
isLoading?: boolean;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
@@ -17,6 +18,7 @@ const MenuItem = ({
|
||||
href,
|
||||
icon,
|
||||
active = false,
|
||||
isLoading = false,
|
||||
className,
|
||||
onClick,
|
||||
}: MenuItemProps) => {
|
||||
@@ -50,17 +52,28 @@ const MenuItem = ({
|
||||
|
||||
return (
|
||||
<li>
|
||||
{href && (
|
||||
{!isLoading && href && (
|
||||
<Link href={href} className={menuItemBaseClassName}>
|
||||
{menuItemContent}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!href && (
|
||||
{!isLoading && !href && (
|
||||
<button className={menuItemBaseClassName} onClick={onClick}>
|
||||
{menuItemContent}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<button className={menuItemBaseClassName}>
|
||||
<span
|
||||
className={cn('loading loading-dots loading-md mx-auto', {
|
||||
'text-gray-400': !active,
|
||||
'text-black': active,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,15 +6,16 @@ import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
||||
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
||||
|
||||
import {
|
||||
ClosingGeneralInformation,
|
||||
BaseClosingSales,
|
||||
} from '@/types/api/closing';
|
||||
import ClosingSapronakTabContent from './ClosingSapronakTabContent';
|
||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||
import SalesReportTable from './sale/SalesReportTable';
|
||||
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
||||
|
||||
interface ClosingDetailProps {
|
||||
@@ -60,7 +61,7 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||
{
|
||||
id: 'dataProduksi',
|
||||
label: 'Data Produksi',
|
||||
content: 'Data Produksi',
|
||||
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
||||
},
|
||||
{
|
||||
id: 'keuangan',
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
|
||||
interface ClosingProductionDataTabContentProps {
|
||||
projectFlockId: number;
|
||||
}
|
||||
|
||||
const ClosingProductionDataTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingProductionDataTabContentProps) => {
|
||||
const { data: productionData, isLoading } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/production-data`,
|
||||
() => ClosingApi.getProductionData(projectFlockId)
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='w-full flex justify-center py-8'>
|
||||
<span className='loading loading-spinner loading-lg' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!productionData || !isResponseSuccess(productionData)) {
|
||||
return (
|
||||
<div className='w-full text-center py-8 text-gray-500'>
|
||||
Gagal memuat data produksi.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { purchase, sales, performance } = productionData.data;
|
||||
|
||||
// Helper for consistent row styling
|
||||
const DataRow = ({
|
||||
label,
|
||||
value,
|
||||
unit = '',
|
||||
valueClassName = 'font-bold text-gray-800',
|
||||
unitClassName = 'text-gray-500 w-12 text-right',
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
valueClassName?: string;
|
||||
unitClassName?: string;
|
||||
}) => (
|
||||
<div className='flex justify-between items-center py-1'>
|
||||
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
|
||||
<div className='flex gap-2 w-1/2 justify-end items-center'>
|
||||
<span className={valueClassName}>{value}</span>
|
||||
{unit && <span className={unitClassName}>{unit}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full rounded-xl p-8 shadow-sm'>
|
||||
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
|
||||
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
|
||||
{/* Left Column */}
|
||||
<div className='space-y-10'>
|
||||
{/* Purchase Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Pembelian
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Populasi Awal'
|
||||
value={formatNumber(purchase.initial_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Claim Culling'
|
||||
value={formatNumber(purchase.claim_culling)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Populasi Akhir'
|
||||
value={formatNumber(purchase.final_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Masuk'
|
||||
value={formatNumber(purchase.feed_in)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Terpakai'
|
||||
value={formatNumber(purchase.feed_used)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Pakan Terpakai per Ekor'
|
||||
value={formatNumber(purchase.feed_used_per_head)}
|
||||
unit='Kg'
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sales Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Penjualan
|
||||
</h3>
|
||||
<div className='space-y-4'>
|
||||
{/* Chicken Sales */}
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Penjualan (Ekor)'
|
||||
value={formatNumber(sales.chicken.sales_population)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Penjualan (Kg)'
|
||||
value={formatNumber(sales.chicken.sales_weight)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Bobot Rata-Rata'
|
||||
value={formatNumber(sales.chicken.average_weight)}
|
||||
unit='Kg/Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Rata-Rata'
|
||||
value={formatNumber(
|
||||
sales.chicken.chicken_average_selling_price
|
||||
)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Egg Sales (if available) */}
|
||||
{sales.egg && (
|
||||
<>
|
||||
<div className='h-px bg-gray-100 my-2' />
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Telur (Butir)'
|
||||
value={formatNumber(sales.egg.egg_pieces)}
|
||||
unit='Butir'
|
||||
/>
|
||||
<DataRow
|
||||
label='Telur (Kg)'
|
||||
value={formatNumber(sales.egg.egg_mass_kg)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Berat Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.average_egg_weight_kg)}
|
||||
unit='Kg'
|
||||
/>
|
||||
<DataRow
|
||||
label='Harga Jual Telur Rata-Rata'
|
||||
value={formatNumber(sales.egg.egg_average_selling_price)}
|
||||
unit='Rupiah'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Divider Line (Absolute centered) */}
|
||||
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
|
||||
|
||||
{/* Right Column */}
|
||||
<div className='space-y-10 flex flex-col h-full'>
|
||||
{/* Performance Section */}
|
||||
<section>
|
||||
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||
Performance
|
||||
</h3>
|
||||
<div className='space-y-1'>
|
||||
<DataRow
|
||||
label='Deplesi'
|
||||
value={formatNumber(performance.depletion)}
|
||||
unit='Ekor'
|
||||
/>
|
||||
<DataRow
|
||||
label='Umur'
|
||||
value={formatNumber(performance.age_day)}
|
||||
unit='Hari'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Std'
|
||||
value={formatNumber(performance.mortality_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='Mortalitas Act'
|
||||
value={formatNumber(performance.mortality_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF Mortalitas'
|
||||
value={formatNumber(performance.deff_mortality)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Std'
|
||||
value={formatNumber(performance.fcr_std)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='FCR Act'
|
||||
value={formatNumber(performance.fcr_act)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='DEFF FCR'
|
||||
value={formatNumber(performance.deff_fcr)}
|
||||
unitClassName='hidden'
|
||||
/>
|
||||
<DataRow
|
||||
label='AWG'
|
||||
value={formatNumber(performance.awg)}
|
||||
unit='Gr/Hari'
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosingProductionDataTabContent;
|
||||
@@ -0,0 +1,413 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, 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/DailyMarketingsTable';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import DailyMarketingReportPDF from '@/components/pages/report/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_TYPE_OPTIONS } from '@/config/constant';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { DailyMarketingReport } 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,
|
||||
} = 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,
|
||||
} = 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,
|
||||
} = 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,
|
||||
} = 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 [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<
|
||||
BaseApiResponse<DailyMarketingReport>
|
||||
>(`${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} />
|
||||
).toBlob();
|
||||
|
||||
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||
dailyMarketingReportPdfBlob
|
||||
);
|
||||
window.open(dailyMarketingReportPdfUrl, '_blank');
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
const blob = await pdf(
|
||||
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
|
||||
).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();
|
||||
};
|
||||
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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='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,550 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Document,
|
||||
Image,
|
||||
Page,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from '@react-pdf/renderer';
|
||||
|
||||
import { DailyMarketingReport } from '@/types/api/report/marketing';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
interface DailyMarketingReportPDFProps {
|
||||
data?: DailyMarketingReport;
|
||||
}
|
||||
|
||||
const DailyMarketingReportPDFStyle = StyleSheet.create({
|
||||
page: {
|
||||
paddingTop: 24,
|
||||
paddingBottom: 64,
|
||||
paddingHorizontal: 16, // Reduce padding to fit more columns
|
||||
orientation: 'landscape',
|
||||
},
|
||||
|
||||
companyInfoHeader: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyLogo: {
|
||||
width: 64,
|
||||
height: 'auto',
|
||||
},
|
||||
companyInfoHeaderDate: {
|
||||
paddingTop: 8,
|
||||
fontSize: 10,
|
||||
},
|
||||
companyName: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
companyAddress: {
|
||||
fontSize: 8,
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
title: {
|
||||
marginTop: 16,
|
||||
fontSize: 14,
|
||||
lineHeight: '150%',
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Times-Roman',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
footer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
|
||||
position: 'absolute',
|
||||
fontSize: 8,
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: 'grey',
|
||||
},
|
||||
|
||||
// Table Styles
|
||||
table: {
|
||||
width: '100%',
|
||||
marginTop: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
borderBottomWidth: 0,
|
||||
fontSize: 7, // Smaller font for report
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
alignItems: 'center',
|
||||
minHeight: 20,
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#f0f0f0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
// Columns definition (Total 100%)
|
||||
colNo: {
|
||||
width: '3%',
|
||||
padding: 2,
|
||||
textAlign: 'center',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colSoDate: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colDoDate: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colAging: {
|
||||
width: '3%',
|
||||
padding: 2,
|
||||
textAlign: 'center',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colWarehouse: {
|
||||
width: '7%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colCustomer: {
|
||||
width: '9%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
}, // Reduced slightly
|
||||
colSales: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colProduct: {
|
||||
width: '8%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
}, // Reduced slightly
|
||||
colDoNumber: {
|
||||
width: '7%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colVehicle: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colMarketingType: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'left',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colQty: {
|
||||
width: '4%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colAvgWeight: {
|
||||
width: '4%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colTotalWeight: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colSalesPrice: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colHppPrice: {
|
||||
width: '5%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colSalesAmount: {
|
||||
width: '6%',
|
||||
padding: 2,
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
},
|
||||
colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column
|
||||
|
||||
// Text inside columns
|
||||
cellText: {
|
||||
fontSize: 6,
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
// Utils
|
||||
doubleDivider: {
|
||||
width: '100%',
|
||||
height: 6,
|
||||
borderTop: '2px solid black',
|
||||
borderBottom: '2px solid black',
|
||||
},
|
||||
|
||||
// Summary
|
||||
summaryContainer: {
|
||||
marginTop: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
width: '100%',
|
||||
},
|
||||
summaryTable: {
|
||||
width: '30%',
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
fontSize: 8,
|
||||
},
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
padding: 2,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
summaryLabel: {
|
||||
width: '50%',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
summaryValue: {
|
||||
width: '50%',
|
||||
textAlign: 'right',
|
||||
},
|
||||
});
|
||||
|
||||
const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => {
|
||||
const rows = data?.rows || [];
|
||||
const summary = data?.summary;
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page
|
||||
style={DailyMarketingReportPDFStyle.page}
|
||||
orientation='landscape'
|
||||
size='A4'
|
||||
>
|
||||
<View>
|
||||
<View style={DailyMarketingReportPDFStyle.companyInfoHeader}>
|
||||
<Image
|
||||
style={DailyMarketingReportPDFStyle.companyLogo}
|
||||
src='/assets/img/lti-logo.png'
|
||||
/>
|
||||
|
||||
<Text style={DailyMarketingReportPDFStyle.companyInfoHeaderDate}>
|
||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={DailyMarketingReportPDFStyle.companyName}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.companyAddress}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
|
||||
<View style={DailyMarketingReportPDFStyle.doubleDivider} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={DailyMarketingReportPDFStyle.title}>
|
||||
Laporan Penjualan Harian
|
||||
</Text>
|
||||
|
||||
{/* Data Table */}
|
||||
<View style={DailyMarketingReportPDFStyle.table}>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={[
|
||||
DailyMarketingReportPDFStyle.tableRow,
|
||||
DailyMarketingReportPDFStyle.tableHeader,
|
||||
]}
|
||||
>
|
||||
<View style={DailyMarketingReportPDFStyle.colNo}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>No</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Tgl SO
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Tgl DO
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAging}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Aging</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Gudang
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Pelanggan
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSales}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Sales</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Produk
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>No DO</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Plat No
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Tipe</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colQty}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Qty</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Rerata
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Berat</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Hrg Jual
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
HPP/kg
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Total Jual
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||
Total HPP
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Rows */}
|
||||
{rows.map((row, index) => (
|
||||
<View style={DailyMarketingReportPDFStyle.tableRow} key={index}>
|
||||
<View style={DailyMarketingReportPDFStyle.colNo}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{index + 1}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatDate(row.so_date, 'DD/MM/YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatDate(row.do_date, 'DD/MM/YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAging}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.aging_days}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.warehouse?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.customer?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSales}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.sales}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.product?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.do_number}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.vehicle_number}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{row.marketing_type}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colQty}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatNumber(row.qty)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatNumber(row.average_weight_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatNumber(row.total_weight_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.sales_price_per_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.hpp_price_per_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.sales_amount)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||
{formatCurrency(row.hpp_amount)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Summary */}
|
||||
<View style={DailyMarketingReportPDFStyle.summaryContainer}>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryTable}>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total Qty:
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatNumber(summary?.total_qty ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total Berat (kg):
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatNumber(summary?.total_weight_kg ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total Penjualan:
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatCurrency(summary?.total_sales_amount ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
DailyMarketingReportPDFStyle.summaryRow,
|
||||
{ borderBottomWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||
Total HPP:
|
||||
</Text>
|
||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||
{formatCurrency(summary?.total_hpp_amount ?? 0)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={DailyMarketingReportPDFStyle.footer} fixed>
|
||||
<Text
|
||||
render={({ pageNumber, totalPages }) =>
|
||||
`${pageNumber} / ${totalPages}`
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingReportPDF;
|
||||
@@ -0,0 +1,255 @@
|
||||
'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 } 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: 'do_date',
|
||||
header: 'Tanggal DO',
|
||||
cell: (props) => formatDate(props.row.original.do_date, 'DD-MMM-YYYY'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'aging_days',
|
||||
header: 'Aging',
|
||||
cell: (props) => `${props.row.original.aging_days} hari`,
|
||||
},
|
||||
{
|
||||
accessorKey: 'warehouse.name',
|
||||
header: 'Gudang',
|
||||
},
|
||||
{
|
||||
accessorKey: 'customer.name',
|
||||
header: 'Pelanggan',
|
||||
},
|
||||
{
|
||||
accessorKey: 'do_number',
|
||||
header: 'No. DO',
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales',
|
||||
header: 'Sales/Marketing',
|
||||
},
|
||||
{
|
||||
accessorKey: 'vehicle_number',
|
||||
header: 'No. Polisi',
|
||||
cell: (props) => (
|
||||
<span className='text-nowrap'>{props.row.original.vehicle_number}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'marketing_type',
|
||||
header: 'Marketing Type',
|
||||
},
|
||||
{
|
||||
accessorKey: 'product.name',
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorKey: 'qty',
|
||||
header: 'Kuantitas',
|
||||
cell: (props) => formatNumber(props.row.original.qty),
|
||||
footer: () => {
|
||||
const totalQty = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.summary.total_qty
|
||||
: 0;
|
||||
|
||||
return formatNumber(totalQty);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'average_weight_kg',
|
||||
header: 'Bobot Rata-Rata (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||
},
|
||||
{
|
||||
accessorKey: 'total_weight_kg',
|
||||
header: 'Bobot Total (Kg)',
|
||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||
footer: () => {
|
||||
const totalWeightKg = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.summary.total_weight_kg
|
||||
: 0;
|
||||
|
||||
return formatNumber(totalWeightKg);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_price_per_kg',
|
||||
header: 'Harga Jual (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||
},
|
||||
{
|
||||
accessorKey: 'hpp_price_per_kg',
|
||||
header: 'HPP (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||
},
|
||||
{
|
||||
accessorKey: 'sales_amount',
|
||||
header: 'Total (Rp)',
|
||||
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
||||
footer: () => {
|
||||
const totalSalesAmount = isResponseSuccess(dailyMarketings)
|
||||
? dailyMarketings.data.summary.total_sales_amount
|
||||
: 0;
|
||||
|
||||
return 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.rows.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.rows
|
||||
: []
|
||||
}
|
||||
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?.rows.length === 0,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyMarketingsTable;
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { JSX, useState } from 'react';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent';
|
||||
|
||||
type MarketingReportTabType =
|
||||
| 'daily'
|
||||
| 'transaction'
|
||||
| 'hpp-comparison'
|
||||
| 'daily-hpp';
|
||||
|
||||
const marketingReportTabs: {
|
||||
id: MarketingReportTabType;
|
||||
label: string;
|
||||
content: JSX.Element;
|
||||
}[] = [
|
||||
{
|
||||
id: 'daily',
|
||||
label: 'Penjualan Harian',
|
||||
content: <DailyMarketingReportContent />,
|
||||
},
|
||||
];
|
||||
|
||||
const MarketingReportContent = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>('daily');
|
||||
|
||||
return (
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<Tabs
|
||||
activeTabId={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
tabs={marketingReportTabs}
|
||||
variant='lifted'
|
||||
className={{
|
||||
content: '-m-px pl-px',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingReportContent;
|
||||
+30
-1
@@ -58,9 +58,12 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
text: 'Biaya Operasional',
|
||||
link: '/report/expense',
|
||||
},
|
||||
{
|
||||
text: 'Penjualan',
|
||||
link: '/report/marketing',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Persediaan',
|
||||
link: '/inventory',
|
||||
@@ -267,3 +270,29 @@ export const ACCEPTED_FILE_TYPE = {
|
||||
'image/*': [],
|
||||
},
|
||||
};
|
||||
|
||||
export const FILTER_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'Tanggal Realisasi',
|
||||
value: 'REALIZATION_DATE',
|
||||
},
|
||||
{
|
||||
label: 'Tanggal DO',
|
||||
value: 'DO_DATE',
|
||||
},
|
||||
];
|
||||
|
||||
export const MARKETING_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'Ayam',
|
||||
value: 'ayam',
|
||||
},
|
||||
{
|
||||
label: 'Telur',
|
||||
value: 'telur',
|
||||
},
|
||||
{
|
||||
label: 'Trading',
|
||||
value: 'trading',
|
||||
},
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { DailyMarketingReport } from '@/types/api/report/marketing';
|
||||
|
||||
// TODO: delete this later
|
||||
export const DAILY_MARKETING_DUMMY_DATA: BaseApiResponse<DailyMarketingReport> =
|
||||
{
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Get daily marketing report successfully',
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total_pages: 1,
|
||||
total_results: 2,
|
||||
},
|
||||
data: {
|
||||
rows: [
|
||||
{
|
||||
// metadata
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 101,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2025-12-01T08:00:00Z',
|
||||
updated_at: '2025-12-01T08:00:00Z',
|
||||
|
||||
// row data
|
||||
no: 1,
|
||||
so_date: '2025-12-01',
|
||||
do_date: '2025-12-08',
|
||||
aging_days: 7,
|
||||
|
||||
warehouse: {
|
||||
id: 1,
|
||||
name: 'Warehouse Kandang A',
|
||||
type: 'KANDANG',
|
||||
area: {
|
||||
id: 1,
|
||||
name: 'Area Barat',
|
||||
},
|
||||
location: {
|
||||
id: 1,
|
||||
name: 'Farm Bandung',
|
||||
address: 'Jl. Raya Farm No. 1',
|
||||
area: null,
|
||||
},
|
||||
kandang: {
|
||||
id: 1,
|
||||
name: 'Kandang A1',
|
||||
status: 'ACTIVE',
|
||||
capacity: 5000,
|
||||
location: null,
|
||||
pic: null,
|
||||
},
|
||||
},
|
||||
|
||||
customer: {
|
||||
id: 1,
|
||||
name: 'PT Maju Jaya',
|
||||
pic_id: 10,
|
||||
pic: {
|
||||
id: 10,
|
||||
id_user: 210,
|
||||
email: 'pic@majujaya.com',
|
||||
name: 'Budi Santoso',
|
||||
},
|
||||
type: 'BROILER',
|
||||
address: 'Jl. Industri No. 10',
|
||||
phone: '08123456789',
|
||||
email: 'contact@majujaya.com',
|
||||
account_number: '1234567890',
|
||||
},
|
||||
|
||||
sales: 'Andi Wijaya',
|
||||
|
||||
product: {
|
||||
id: 1,
|
||||
name: 'Live Chicken',
|
||||
brand: 'LTI Farm',
|
||||
sku: 'LC-001',
|
||||
product_price: 18_000,
|
||||
selling_price: 20_000,
|
||||
tax: 0,
|
||||
expiry_period: 0,
|
||||
uom: {
|
||||
id: 1,
|
||||
name: 'Kg',
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 101,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
product_category: {
|
||||
id: 1,
|
||||
code: 'BROILER',
|
||||
name: 'Broiler Chicken',
|
||||
created_user: {
|
||||
id: 1,
|
||||
id_user: 101,
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
},
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
suppliers: [],
|
||||
flags: ['LIVE'],
|
||||
},
|
||||
|
||||
do_number: 'DO-2025-0001',
|
||||
vehicle_number: 'B 1234 CD',
|
||||
marketing_type: 'REGULAR',
|
||||
|
||||
qty: 1000,
|
||||
average_weight_kg: 1.8,
|
||||
total_weight_kg: 1800,
|
||||
|
||||
sales_price_per_kg: 20_000,
|
||||
hpp_price_per_kg: 18_000,
|
||||
|
||||
sales_amount: 36_000_000,
|
||||
hpp_amount: 32_400_000,
|
||||
},
|
||||
],
|
||||
|
||||
summary: {
|
||||
total_qty: 1000,
|
||||
total_weight_kg: 1800,
|
||||
total_sales_amount: 36_000_000,
|
||||
total_hpp_amount: 32_400_000,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -9,10 +9,25 @@ import {
|
||||
ClosingOutgoingSapronak,
|
||||
ClosingOverhead,
|
||||
ClosingSapronakCalculation,
|
||||
ClosingProductionData,
|
||||
} from '@/types/api/closing';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
import { ClosingSales } from '@/types/api/closing';
|
||||
|
||||
// TODO: delete these dummy data later
|
||||
import {
|
||||
dummyGetAllFetcher,
|
||||
dummyGetSingle,
|
||||
dummyGetAllIncomingSapronakFetcher,
|
||||
dummyGetAllOutgoingSapronakFetcher,
|
||||
dummyGetGeneralInfo,
|
||||
dummyGetPerhitunganSapronak,
|
||||
dummyGetOverhead,
|
||||
dummyClosingProductionData,
|
||||
} from '@/dummy/closing.dummy';
|
||||
import { sleep } from '@/lib/helper';
|
||||
|
||||
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
constructor(basePath: string) {
|
||||
super(basePath);
|
||||
@@ -71,6 +86,24 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||
}
|
||||
}
|
||||
|
||||
async getProductionData(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<ClosingProductionData> | undefined> {
|
||||
try {
|
||||
const getProductionDataPath = `${this.basePath}/${id}/production-data`;
|
||||
const getProductionDataRes = await httpClient<
|
||||
BaseApiResponse<ClosingProductionData>
|
||||
>(getProductionDataPath);
|
||||
|
||||
return getProductionDataRes;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError<BaseApiResponse<ClosingProductionData>>(error)) {
|
||||
return error.response?.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getPerhitunganSapronak(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<ClosingSapronakCalculation> | undefined> {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { DailyMarketingReport } from '@/types/api/report/marketing';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatDate, sleep } from '@/lib/helper';
|
||||
|
||||
export class MarketingReportApiService extends BaseApiService<
|
||||
DailyMarketingReport,
|
||||
unknown,
|
||||
unknown
|
||||
> {
|
||||
constructor(basePath: string = '/reports/marketings/daily-marketing') {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
async getAllDailyMarketingFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<DailyMarketingReport>> {
|
||||
return await httpClientFetcher<BaseApiResponse<DailyMarketingReport>>(
|
||||
endpoint
|
||||
);
|
||||
}
|
||||
|
||||
async exportDailyMarketingToExcel(initialQueryString: string) {
|
||||
const params = new URLSearchParams(initialQueryString);
|
||||
|
||||
params.set('limit', '9999999');
|
||||
|
||||
const queryString = `?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const dailyMarketingsReport = await httpClientFetcher<
|
||||
BaseApiResponse<DailyMarketingReport>
|
||||
>(`${this.basePath}${queryString}`);
|
||||
|
||||
if (isResponseError(dailyMarketingsReport)) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = dailyMarketingsReport.data.rows;
|
||||
|
||||
const formattedRows = [];
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
formattedRows.push({
|
||||
...rows[i],
|
||||
created_user: rows[i].created_user.name,
|
||||
created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'),
|
||||
updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'),
|
||||
warehouse: rows[i].warehouse.name,
|
||||
customer: rows[i].customer.name,
|
||||
product: rows[i].product.name,
|
||||
});
|
||||
}
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(formattedRows);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'laporan-penjualan-harian');
|
||||
|
||||
// triggers download in browser
|
||||
XLSX.writeFile(wb, 'laporan-penjualan-harian.xlsx');
|
||||
} catch (error) {
|
||||
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MarketingReportApi = new MarketingReportApiService(
|
||||
'/reports/marketings/daily-marketing'
|
||||
);
|
||||
Vendored
+66
@@ -23,6 +23,33 @@ export type BaseSales = {
|
||||
payment_status: string;
|
||||
};
|
||||
|
||||
export type BaseClosingSales = {
|
||||
project_type: string;
|
||||
flock_id: number;
|
||||
period: number;
|
||||
sales: BaseSales[];
|
||||
};
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Product } from '@type/api/master-data/product';
|
||||
import { Customer } from '@type/api/master-data/customer';
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
|
||||
export type BaseSales = {
|
||||
id: number;
|
||||
realization_date: string;
|
||||
age: number;
|
||||
do_number: string;
|
||||
product: Product;
|
||||
customer: Customer;
|
||||
qty: number;
|
||||
weight: number;
|
||||
avg_weight: number;
|
||||
price: number;
|
||||
total_price: number;
|
||||
kandang: Kandang;
|
||||
payment_status: string;
|
||||
};
|
||||
|
||||
export type BaseClosingSales = {
|
||||
project_type: string;
|
||||
flock_id: number;
|
||||
@@ -79,6 +106,44 @@ export type ClosingIncomingSapronak = {
|
||||
|
||||
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
||||
|
||||
export type ClosingProductionData = {
|
||||
purchase: {
|
||||
initial_population: number;
|
||||
claim_culling: number;
|
||||
final_population: number;
|
||||
feed_in: number;
|
||||
feed_used: number;
|
||||
feed_used_per_head: number;
|
||||
};
|
||||
|
||||
sales: {
|
||||
chicken: {
|
||||
sales_population: number;
|
||||
sales_weight: number;
|
||||
average_weight: number;
|
||||
chicken_average_selling_price: number;
|
||||
};
|
||||
egg?: {
|
||||
egg_pieces: number;
|
||||
egg_mass_kg: number;
|
||||
average_egg_weight_kg: number;
|
||||
egg_average_selling_price: number;
|
||||
};
|
||||
};
|
||||
|
||||
performance: {
|
||||
depletion: number;
|
||||
age_day: number;
|
||||
mortality_std: number;
|
||||
mortality_act: number;
|
||||
deff_mortality: number;
|
||||
fcr_std: number;
|
||||
fcr_act: number;
|
||||
deff_fcr: number;
|
||||
awg: number;
|
||||
};
|
||||
};
|
||||
|
||||
// ====== PERHITUNGAN SAPRONAK ======
|
||||
|
||||
export type RowSapronakCalculation = {
|
||||
@@ -141,6 +206,7 @@ export type OverheadTotal = {
|
||||
actual_total_amount: number;
|
||||
cost_per_bird: number;
|
||||
};
|
||||
|
||||
export type ClosingSales = BaseMetadata & BaseClosingSales;
|
||||
|
||||
// ====== FINANCE ======
|
||||
|
||||
-1
@@ -10,7 +10,6 @@ export type BaseKandang = {
|
||||
capacity: number;
|
||||
pic: BaseUser;
|
||||
project_flock_kandang_id?: number;
|
||||
capacity: number;
|
||||
};
|
||||
|
||||
export type Kandang = BaseMetadata & BaseKandang;
|
||||
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { BaseCustomer, Customer } from '@/types/api/master-data/customer';
|
||||
import {
|
||||
BaseWarehouseArea,
|
||||
BaseWarehouseKandang,
|
||||
BaseWarehouseLocation,
|
||||
Warehouse,
|
||||
} from '@/types/api/master-data/warehouse';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { BaseProduct } from '@/types/api/master-data/product';
|
||||
|
||||
export type BaseDailyMarketingRow = {
|
||||
no: number;
|
||||
so_date: string; // e.g. "01-Dec-2025"
|
||||
do_date: string; // e.g. "08-Dec-2025"
|
||||
aging_days: number;
|
||||
|
||||
warehouse: BaseWarehouseArea | BaseWarehouseLocation | BaseWarehouseKandang;
|
||||
customer: BaseCustomer;
|
||||
sales: string;
|
||||
product: BaseProduct;
|
||||
|
||||
do_number: string;
|
||||
vehicle_number: string;
|
||||
marketing_type: string;
|
||||
|
||||
qty: number;
|
||||
average_weight_kg: number;
|
||||
total_weight_kg: number;
|
||||
|
||||
sales_price_per_kg: number;
|
||||
hpp_price_per_kg: number;
|
||||
|
||||
sales_amount: number;
|
||||
hpp_amount: number;
|
||||
};
|
||||
|
||||
export type DailyMarketingRow = BaseMetadata & BaseDailyMarketingRow;
|
||||
|
||||
export interface SalesSummary {
|
||||
total_qty: number;
|
||||
total_weight_kg: number;
|
||||
total_sales_amount: number;
|
||||
total_hpp_amount: number;
|
||||
}
|
||||
|
||||
export type DailyMarketingReport = {
|
||||
rows: DailyMarketingRow[];
|
||||
summary: SalesSummary;
|
||||
};
|
||||
|
||||
export type MarketingReportFilters = {
|
||||
area_id?: number;
|
||||
location_id?: number;
|
||||
warehouse_id?: number;
|
||||
customer_id?: number;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
date_type?: 'realized' | 'transaction';
|
||||
};
|
||||
Reference in New Issue
Block a user