diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ee8a79a5..935cac46 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -165,8 +165,6 @@ deploy:staging:
environment:
name: staging
url: https://stg-lti-erp.mbugroup.id
-
-
# ====== PRODUCTION ======
# build:production:
# <<: *build_template
diff --git a/package-lock.json b/package-lock.json
index 59e9d7db..17c2a7b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"clsx": "^2.1.1",
"formik": "^2.4.6",
"moment": "^2.30.1",
- "next": "15.5.9",
+ "next": "^15.5.9",
"react": "19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.1.0",
@@ -26,7 +26,7 @@
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.6",
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
+ "xlsx": "^0.18.5",
"yup": "^1.7.0",
"zustand": "^5.0.8"
},
@@ -2465,6 +2465,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2925,6 +2934,19 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2966,6 +2988,15 @@
"node": ">=6"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3036,6 +3067,18 @@
"node": ">=10"
}
},
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4181,6 +4224,15 @@
"react": ">=16.8.0"
}
},
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6755,6 +6807,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -7516,6 +7580,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -7527,10 +7609,19 @@
}
},
"node_modules/xlsx": {
- "version": "0.20.3",
- "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
- "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
"bin": {
"xlsx": "bin/xlsx.njs"
},
@@ -7608,4 +7699,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index d0b99b80..86b951e5 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"clsx": "^2.1.1",
"formik": "^2.4.6",
"moment": "^2.30.1",
- "next": "15.5.9",
+ "next": "^15.5.9",
"react": "19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.1.0",
@@ -29,7 +29,7 @@
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.6",
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
+ "xlsx": "^0.18.5",
"yup": "^1.7.0",
"zustand": "^5.0.8"
},
@@ -48,4 +48,4 @@
"tailwindcss": "^4",
"typescript": "^5"
}
-}
+}
\ No newline at end of file
diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx
new file mode 100644
index 00000000..52a3d4dd
--- /dev/null
+++ b/src/app/report/marketing/page.tsx
@@ -0,0 +1,11 @@
+import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
+
+const MarketingReportPage = () => {
+ return (
+
+ );
+};
+
+export default MarketingReportPage;
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 918122d2..0d5b9bc8 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -54,8 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
@@ -67,7 +67,7 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
content: 'w-52 mt-3',
}}
>
-
);
};
diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx
index 65adf48c..9dbd2557 100644
--- a/src/components/helper/RequireAuth.tsx
+++ b/src/components/helper/RequireAuth.tsx
@@ -27,6 +27,9 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
SWRHttpKey
>('/sso/userinfo', httpClientFetcher, {
shouldRetryOnError: false,
+
+ // refresh every 13 minutes
+ refreshInterval: 13 * 60 * 1000,
});
useEffect(() => {
diff --git a/src/components/menu/MenuItem.tsx b/src/components/menu/MenuItem.tsx
index dce81dac..61af4b04 100644
--- a/src/components/menu/MenuItem.tsx
+++ b/src/components/menu/MenuItem.tsx
@@ -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 (
- {href && (
+ {!isLoading && href && (
{menuItemContent}
)}
- {!href && (
+ {!isLoading && !href && (
)}
+
+ {isLoading && (
+
+ )}
);
};
diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx
index e12769a7..8a6331f0 100644
--- a/src/components/pages/closing/ClosingDetail.tsx
+++ b/src/components/pages/closing/ClosingDetail.tsx
@@ -6,16 +6,17 @@ 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 ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
+import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
interface ClosingDetailProps {
id: number;
@@ -60,7 +61,7 @@ const ClosingDetail: React.FC = ({
{
id: 'dataProduksi',
label: 'Data Produksi',
- content: 'Data Produksi',
+ content: ,
},
{
id: 'keuangan',
diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx
new file mode 100644
index 00000000..bffe1707
--- /dev/null
+++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (!productionData || !isResponseSuccess(productionData)) {
+ return (
+
+ Gagal memuat data produksi.
+
+ );
+ }
+
+ 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;
+ }) => (
+
+
{label}
+
+ {value}
+ {unit && {unit}}
+
+
+ );
+
+ return (
+
+
Data Produksi
+
+
+ {/* Left Column */}
+
+ {/* Purchase Section */}
+
+
+ Pembelian
+
+
+
+
+
+
+
+
+
+
+
+ {/* Sales Section */}
+
+
+ Penjualan
+
+
+ {/* Chicken Sales */}
+
+
+
+
+
+
+
+ {/* Egg Sales (if available) */}
+ {sales.egg && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ {/* Divider Line (Absolute centered) */}
+
+
+ {/* Right Column */}
+
+ {/* Performance Section */}
+
+
+ Performance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ClosingProductionDataTabContent;
diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx
new file mode 100644
index 00000000..1eba4ea3
--- /dev/null
+++ b/src/components/pages/report/DailyMarketingReportContent.tsx
@@ -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(null);
+ const {
+ setInputValue: setAreaInputValue,
+ options: areaOptions,
+ isLoadingOptions: isLoadingAreaOptions,
+ } = useSelect(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(
+ null
+ );
+ const {
+ setInputValue: setLocationInputValue,
+ options: locationOptions,
+ isLoadingOptions: isLoadingLocationOptions,
+ } = useSelect(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(
+ null
+ );
+ const {
+ setInputValue: setWarehouseInputValue,
+ options: warehouseOptions,
+ isLoadingOptions: isLoadingWarehouseOptions,
+ } = useSelect(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(
+ null
+ );
+ const {
+ setInputValue: setCustomerInputValue,
+ options: customerOptions,
+ isLoadingOptions: isLoadingCustomerOptions,
+ } = useSelect(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) => {
+ updateFilter('start_date', e.target.value ? e.target.value : '');
+ };
+
+ const endDateChangeHandler = (e: React.ChangeEvent) => {
+ updateFilter('end_date', e.target.value ? e.target.value : '');
+ };
+
+ const [selectedMarketingType, setSelectedMarketingType] =
+ useState(null);
+ const marketingTypeChangeHandler = (
+ val: OptionType | OptionType[] | null
+ ) => {
+ setSelectedMarketingType(val as OptionType);
+ updateFilter(
+ 'marketing_type',
+ val ? ((val as OptionType).value as string) : ''
+ );
+ };
+
+ const searchChangeHandler: ChangeEventHandler = (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
+ >(`${MarketingReportApi.basePath}${queryString}`);
+
+ if (isResponseError(dailyMarketingsReport)) {
+ toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
+ return;
+ }
+
+ const openPdf = async () => {
+ const dailyMarketingReportPdfBlob = await pdf(
+
+ ).toBlob();
+
+ const dailyMarketingReportPdfUrl = URL.createObjectURL(
+ dailyMarketingReportPdfBlob
+ );
+ window.open(dailyMarketingReportPdfUrl, '_blank');
+ };
+
+ const downloadPdf = async () => {
+ const blob = await pdf(
+
+ ).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 (
+
+
+
Penjualan Harian
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Export{' '}
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DailyMarketingReportContent;
diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx
new file mode 100644
index 00000000..337892b3
--- /dev/null
+++ b/src/components/pages/report/DailyMarketingReportPDF.tsx
@@ -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 (
+
+
+
+
+
+
+
+ {formatDate(Date.now(), 'DD MMMM YYYY')}
+
+
+
+
+
+ PT LUMBUNG TELUR INDONESIA
+
+
+ SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
+ Cipedes, Kec. Sukajadi, Kota Bandung 40162
+
+
+
+
+
+
+
+ Laporan Penjualan Harian
+
+
+ {/* Data Table */}
+
+ {/* Header */}
+
+
+ No
+
+
+
+ Tgl SO
+
+
+
+
+ Tgl DO
+
+
+
+ Aging
+
+
+
+ Gudang
+
+
+
+
+ Pelanggan
+
+
+
+ Sales
+
+
+
+ Produk
+
+
+
+ No DO
+
+
+
+ Plat No
+
+
+
+ Tipe
+
+
+ Qty
+
+
+
+ Rerata
+
+
+
+ Berat
+
+
+
+ Hrg Jual
+
+
+
+
+ HPP/kg
+
+
+
+
+ Total Jual
+
+
+
+
+ Total HPP
+
+
+
+
+ {/* Rows */}
+ {rows.map((row, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+ {formatDate(row.so_date, 'DD/MM/YYYY')}
+
+
+
+
+ {formatDate(row.do_date, 'DD/MM/YYYY')}
+
+
+
+
+ {row.aging_days}
+
+
+
+
+ {row.warehouse?.name}
+
+
+
+
+ {row.customer?.name}
+
+
+
+
+ {row.sales}
+
+
+
+
+ {row.product?.name}
+
+
+
+
+ {row.do_number}
+
+
+
+
+ {row.vehicle_number}
+
+
+
+
+ {row.marketing_type}
+
+
+
+
+ {formatNumber(row.qty)}
+
+
+
+
+ {formatNumber(row.average_weight_kg)}
+
+
+
+
+ {formatNumber(row.total_weight_kg)}
+
+
+
+
+ {formatCurrency(row.sales_price_per_kg)}
+
+
+
+
+ {formatCurrency(row.hpp_price_per_kg)}
+
+
+
+
+ {formatCurrency(row.sales_amount)}
+
+
+
+
+ {formatCurrency(row.hpp_amount)}
+
+
+
+ ))}
+
+
+ {/* Summary */}
+
+
+
+
+ Total Qty:
+
+
+ {formatNumber(summary?.total_qty ?? 0)}
+
+
+
+
+ Total Berat (kg):
+
+
+ {formatNumber(summary?.total_weight_kg ?? 0)}
+
+
+
+
+ Total Penjualan:
+
+
+ {formatCurrency(summary?.total_sales_amount ?? 0)}
+
+
+
+
+ Total HPP:
+
+
+ {formatCurrency(summary?.total_hpp_amount ?? 0)}
+
+
+
+
+
+
+
+ `${pageNumber} / ${totalPages}`
+ }
+ fixed
+ />
+
+
+
+ );
+};
+
+export default DailyMarketingReportPDF;
diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx
new file mode 100644
index 00000000..d6914cf1
--- /dev/null
+++ b/src/components/pages/report/DailyMarketingsTable.tsx
@@ -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;
+ 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([]);
+
+ const dailyMarketingColumns: ColumnDef[] = [
+ {
+ 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) => (
+ {props.row.original.vehicle_number}
+ ),
+ },
+ {
+ 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 (
+
+
+ Penjualan Harian
+
+
+
+ }
+ className='w-full!'
+ titleClassName='w-full p-0!'
+ >
+
+
+
+
+ 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,
+ }),
+ }}
+ />
+
+
+
+ );
+};
+
+export default DailyMarketingsTable;
diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/MarketingReportContent.tsx
new file mode 100644
index 00000000..160de8b2
--- /dev/null
+++ b/src/components/pages/report/MarketingReportContent.tsx
@@ -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: ,
+ },
+];
+
+const MarketingReportContent = () => {
+ const [activeTab, setActiveTab] = useState('daily');
+
+ return (
+
+ );
+};
+
+export default MarketingReportContent;
diff --git a/src/config/constant.ts b/src/config/constant.ts
index 5c629c73..ebb890a2 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -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',
+ },
+];
diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts
new file mode 100644
index 00000000..3b9a9a7b
--- /dev/null
+++ b/src/dummy/closing.dummy.ts
@@ -0,0 +1,1175 @@
+/**
+ * Dummy Data untuk Closing API
+ *
+ * File ini berisi dummy data untuk testing API Closing sebelum backend siap.
+ *
+ * Struktur data mengikuti tipe yang didefinisikan di @/types/api/closing.d.ts
+ *
+ * @example
+ * // 1. Menggunakan getAllFetcher dengan SWR:
+ * import useSWR from 'swr';
+ * import { ClosingApi } from '@/services/api/closing';
+ *
+ * const { data, error, isLoading } = useSWR(
+ * '/closings',
+ * ClosingApi.getAllFetcher.bind(ClosingApi)
+ * );
+ *
+ * if (data?.status === 'success') {
+ * console.log(data.data); // Array of Closing objects
+ * }
+ *
+ * @example
+ * // 2. Menggunakan getSingle:
+ * import { ClosingApi } from '@/services/api/closing';
+ *
+ * const response = await ClosingApi.getSingle(1);
+ * if (response?.status === 'success') {
+ * console.log(response.data); // Single Closing object
+ * } else if (response?.status === 'error') {
+ * console.error(response.message); // Error message
+ * }
+ *
+ * @example
+ * // 3. Menggunakan getGeneralInfo dengan SWR:
+ * import useSWR from 'swr';
+ * import { ClosingApi } from '@/services/api/closing';
+ *
+ * const closingId = 1;
+ * const { data, error, isLoading } = useSWR(
+ * closingId,
+ * (id: number) => ClosingApi.getGeneralInfo(id)
+ * );
+ *
+ * if (data?.status === 'success') {
+ * console.log(data.data); // ClosingGeneralInformation object
+ * }
+ *
+ * @example
+ * // 4. Menggunakan getAllIncomingSapronakFetcher dengan SWR:
+ * import useSWR from 'swr';
+ * import { ClosingApi } from '@/services/api/closing';
+ *
+ * const { data, error, isLoading } = useSWR(
+ * `${ClosingApi.basePath}/1/sapronak/incoming`,
+ * ClosingApi.getAllIncomingSapronakFetcher.bind(ClosingApi)
+ * );
+ *
+ * if (data?.status === 'success') {
+ * console.log(data.data); // Array of ClosingIncomingSapronak
+ * }
+ *
+ * @example
+ * // 5. Menggunakan getAllOutgoingSapronakFetcher dengan SWR:
+ * import useSWR from 'swr';
+ * import { ClosingApi } from '@/services/api/closing';
+ *
+ * const { data, error, isLoading } = useSWR(
+ * `${ClosingApi.basePath}/1/sapronak/outgoing`,
+ * ClosingApi.getAllOutgoingSapronakFetcher.bind(ClosingApi)
+ * );
+ *
+ * if (data?.status === 'success') {
+ * console.log(data.data); // Array of ClosingOutgoingSapronak
+ * }
+ *
+ * @see {@link /home/sweetpotet/Documents/projects/lti-web-client/src/types/api/closing.d.ts}
+ */
+
+import { format } from 'date-fns';
+import {
+ Closing,
+ ClosingGeneralInformation,
+ ClosingIncomingSapronak,
+ ClosingOutgoingSapronak,
+ ClosingOverhead,
+ ClosingProductionData,
+ ClosingSapronakCalculation,
+} from '@/types/api/closing';
+import { CreatedUser, BaseApiResponse } from '@/types/api/api-general';
+
+// Waktu saat ini untuk created_at/updated_at
+const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss');
+const today = format(new Date(), 'yyyy-MM-dd');
+const yesterday = format(
+ new Date().setDate(new Date().getDate() - 1),
+ 'yyyy-MM-dd'
+);
+const lastWeek = format(
+ new Date().setDate(new Date().getDate() - 7),
+ 'yyyy-MM-dd'
+);
+const lastMonth = format(
+ new Date().setMonth(new Date().getMonth() - 1),
+ 'yyyy-MM-dd'
+);
+
+// ======================
+// 👤 Created User
+// ======================
+export const createdUser: CreatedUser = {
+ id: 1,
+ id_user: 1,
+ email: 'admin@example.com',
+ name: 'Admin Utama',
+};
+
+// ======================
+// 📊 Closing Dummy Data
+// ======================
+export const dummyClosings: Closing[] = [
+ // 1. Closing dengan status Pengajuan - GROWING
+ {
+ id: 1,
+ location_id: 1,
+ location_name: 'Farm Sukajadi',
+ project_category: 'GROWING',
+ period: 1,
+ closing_date: today,
+ shed_label: 'Kandang A1, A2, A3',
+ shed_count: 3,
+ sales_paid_amount: 150000000,
+ sales_remaining_amount: 50000000,
+ sales_payment_status: 'Sebagian Lunas',
+ project_status: 'Pengajuan',
+ created_user: createdUser,
+ created_at: now,
+ updated_at: now,
+ },
+
+ // 2. Closing dengan status Aktif - LAYING
+ {
+ id: 2,
+ location_id: 2,
+ location_name: 'Farm Cihampelas',
+ project_category: 'LAYING',
+ period: 2,
+ closing_date: yesterday,
+ shed_label: 'Kandang B1, B2',
+ shed_count: 2,
+ sales_paid_amount: 200000000,
+ sales_remaining_amount: 0,
+ sales_payment_status: 'Lunas',
+ project_status: 'Aktif',
+ created_user: createdUser,
+ created_at: lastWeek,
+ updated_at: yesterday,
+ },
+
+ // 3. Closing dengan status Selesai - GROWING
+ {
+ id: 3,
+ location_id: 3,
+ location_name: 'Farm Pasteur',
+ project_category: 'GROWING',
+ period: 3,
+ closing_date: lastWeek,
+ shed_label: 'Kandang C1, C2, C3, C4',
+ shed_count: 4,
+ sales_paid_amount: 300000000,
+ sales_remaining_amount: 25000000,
+ sales_payment_status: 'Sebagian Lunas',
+ project_status: 'Selesai',
+ created_user: createdUser,
+ created_at: lastMonth,
+ updated_at: lastWeek,
+ },
+
+ // 4. Closing dengan status Aktif - LAYING
+ {
+ id: 4,
+ location_id: 4,
+ location_name: 'Farm Setiabudi',
+ project_category: 'LAYING',
+ period: 1,
+ closing_date: today,
+ shed_label: 'Kandang D1',
+ shed_count: 1,
+ sales_paid_amount: 75000000,
+ sales_remaining_amount: 75000000,
+ sales_payment_status: 'Belum Lunas',
+ project_status: 'Aktif',
+ created_user: createdUser,
+ created_at: yesterday,
+ updated_at: now,
+ },
+
+ // 5. Closing dengan status Selesai - GROWING
+ {
+ id: 5,
+ location_id: 5,
+ location_name: 'Farm Dago',
+ project_category: 'GROWING',
+ period: 4,
+ closing_date: lastMonth,
+ shed_label: 'Kandang E1, E2, E3, E4, E5',
+ shed_count: 5,
+ sales_paid_amount: 500000000,
+ sales_remaining_amount: 0,
+ sales_payment_status: 'Lunas',
+ project_status: 'Selesai',
+ created_user: createdUser,
+ created_at: lastMonth,
+ updated_at: lastMonth,
+ },
+
+ // 6. Closing dengan status Pengajuan - LAYING
+ {
+ id: 6,
+ location_id: 6,
+ location_name: 'Farm Lembang',
+ project_category: 'LAYING',
+ period: 2,
+ closing_date: undefined, // Belum ada tanggal closing
+ shed_label: 'Kandang F1, F2',
+ shed_count: 2,
+ sales_paid_amount: 0,
+ sales_remaining_amount: 180000000,
+ sales_payment_status: 'Belum Lunas',
+ project_status: 'Pengajuan',
+ created_user: createdUser,
+ created_at: now,
+ updated_at: now,
+ },
+
+ // 7. Closing dengan status Aktif - GROWING
+ {
+ id: 7,
+ location_id: 7,
+ location_name: 'Farm Ciwidey',
+ project_category: 'GROWING',
+ period: 1,
+ closing_date: yesterday,
+ shed_label: 'Kandang G1, G2, G3',
+ shed_count: 3,
+ sales_paid_amount: 120000000,
+ sales_remaining_amount: 30000000,
+ sales_payment_status: 'Sebagian Lunas',
+ project_status: 'Aktif',
+ created_user: createdUser,
+ created_at: lastWeek,
+ updated_at: yesterday,
+ },
+
+ // 8. Closing dengan status Selesai - LAYING
+ {
+ id: 8,
+ location_id: 8,
+ location_name: 'Farm Bandung Timur',
+ project_category: 'LAYING',
+ period: 3,
+ closing_date: lastMonth,
+ shed_label: 'Kandang H1, H2, H3, H4, H5, H6',
+ shed_count: 6,
+ sales_paid_amount: 600000000,
+ sales_remaining_amount: 0,
+ sales_payment_status: 'Lunas',
+ project_status: 'Selesai',
+ created_user: createdUser,
+ created_at: lastMonth,
+ updated_at: lastMonth,
+ },
+];
+
+// ======================
+// 📊 Closing General Information Dummy Data
+// ======================
+export const dummyClosingGeneralInformations: ClosingGeneralInformation[] = [
+ // 1. General Info - GROWING - Pengajuan
+ {
+ id: 1,
+ location_id: 1,
+ location_name: 'Farm Sukajadi',
+ project_category: 'GROWING',
+ period: 1,
+ closing_date: today,
+ shed_label: 'Kandang A1, A2, A3',
+ shed_count: 3,
+ sales_paid_amount: 150000000,
+ sales_remaining_amount: 50000000,
+ sales_payment_status: 'Sebagian Lunas',
+ project_status: 'Pengajuan',
+ flock_id: 101,
+ project_type: 'GROWING',
+ population: 15000,
+ active_house_count: 3,
+ closing_status: 'Draft',
+ created_user: createdUser,
+ created_at: now,
+ updated_at: now,
+ },
+
+ // 2. General Info - LAYING - Aktif
+ {
+ id: 2,
+ location_id: 2,
+ location_name: 'Farm Cihampelas',
+ project_category: 'LAYING',
+ period: 2,
+ closing_date: yesterday,
+ shed_label: 'Kandang B1, B2',
+ shed_count: 2,
+ sales_paid_amount: 200000000,
+ sales_remaining_amount: 0,
+ sales_payment_status: 'Lunas',
+ project_status: 'Aktif',
+ flock_id: 102,
+ project_type: 'LAYING',
+ population: 10000,
+ active_house_count: 2,
+ closing_status: 'In Progress',
+ created_user: createdUser,
+ created_at: lastWeek,
+ updated_at: yesterday,
+ },
+
+ // 3. General Info - GROWING - Selesai
+ {
+ id: 3,
+ location_id: 3,
+ location_name: 'Farm Pasteur',
+ project_category: 'GROWING',
+ period: 3,
+ closing_date: lastWeek,
+ shed_label: 'Kandang C1, C2, C3, C4',
+ shed_count: 4,
+ sales_paid_amount: 300000000,
+ sales_remaining_amount: 25000000,
+ sales_payment_status: 'Sebagian Lunas',
+ project_status: 'Selesai',
+ flock_id: 103,
+ project_type: 'GROWING',
+ population: 20000,
+ active_house_count: 4,
+ closing_status: 'Completed',
+ created_user: createdUser,
+ created_at: lastMonth,
+ updated_at: lastWeek,
+ },
+
+ // 4. General Info - LAYING - Aktif
+ {
+ id: 4,
+ location_id: 4,
+ location_name: 'Farm Setiabudi',
+ project_category: 'LAYING',
+ period: 1,
+ closing_date: today,
+ shed_label: 'Kandang D1',
+ shed_count: 1,
+ sales_paid_amount: 75000000,
+ sales_remaining_amount: 75000000,
+ sales_payment_status: 'Belum Lunas',
+ project_status: 'Aktif',
+ flock_id: 104,
+ project_type: 'LAYING',
+ population: 5000,
+ active_house_count: 1,
+ closing_status: 'In Progress',
+ created_user: createdUser,
+ created_at: yesterday,
+ updated_at: now,
+ },
+
+ // 5. General Info - GROWING - Selesai
+ {
+ id: 5,
+ location_id: 5,
+ location_name: 'Farm Dago',
+ project_category: 'GROWING',
+ period: 4,
+ closing_date: lastMonth,
+ shed_label: 'Kandang E1, E2, E3, E4, E5',
+ shed_count: 5,
+ sales_paid_amount: 500000000,
+ sales_remaining_amount: 0,
+ sales_payment_status: 'Lunas',
+ project_status: 'Selesai',
+ flock_id: 105,
+ project_type: 'GROWING',
+ population: 25000,
+ active_house_count: 5,
+ closing_status: 'Completed',
+ created_user: createdUser,
+ created_at: lastMonth,
+ updated_at: lastMonth,
+ },
+
+ // 6. General Info - LAYING - Pengajuan
+ {
+ id: 6,
+ location_id: 6,
+ location_name: 'Farm Lembang',
+ project_category: 'LAYING',
+ period: 2,
+ closing_date: undefined,
+ shed_label: 'Kandang F1, F2',
+ shed_count: 2,
+ sales_paid_amount: 0,
+ sales_remaining_amount: 180000000,
+ sales_payment_status: 'Belum Lunas',
+ project_status: 'Pengajuan',
+ flock_id: 106,
+ project_type: 'LAYING',
+ population: 12000,
+ active_house_count: 2,
+ closing_status: 'Draft',
+ created_user: createdUser,
+ created_at: now,
+ updated_at: now,
+ },
+
+ // 7. General Info - GROWING - Aktif
+ {
+ id: 7,
+ location_id: 7,
+ location_name: 'Farm Ciwidey',
+ project_category: 'GROWING',
+ period: 1,
+ closing_date: yesterday,
+ shed_label: 'Kandang G1, G2, G3',
+ shed_count: 3,
+ sales_paid_amount: 120000000,
+ sales_remaining_amount: 30000000,
+ sales_payment_status: 'Sebagian Lunas',
+ project_status: 'Aktif',
+ flock_id: 107,
+ project_type: 'GROWING',
+ population: 18000,
+ active_house_count: 3,
+ closing_status: 'In Progress',
+ created_user: createdUser,
+ created_at: lastWeek,
+ updated_at: yesterday,
+ },
+
+ // 8. General Info - LAYING - Selesai
+ {
+ id: 8,
+ location_id: 8,
+ location_name: 'Farm Bandung Timur',
+ project_category: 'LAYING',
+ period: 3,
+ closing_date: lastMonth,
+ shed_label: 'Kandang H1, H2, H3, H4, H5, H6',
+ shed_count: 6,
+ sales_paid_amount: 600000000,
+ sales_remaining_amount: 0,
+ sales_payment_status: 'Lunas',
+ project_status: 'Selesai',
+ flock_id: 108,
+ project_type: 'LAYING',
+ population: 30000,
+ active_house_count: 6,
+ closing_status: 'Completed',
+ created_user: createdUser,
+ created_at: lastMonth,
+ updated_at: lastMonth,
+ },
+];
+
+// ======================
+// 📦 Incoming Sapronak Dummy Data
+// ======================
+export const dummyIncomingSapronaks: ClosingIncomingSapronak[] = [
+ {
+ id: 1,
+ date: today,
+ reference_number: 'IN-2025-001',
+ transaction_type: 'Pembelian',
+ product_name: 'DOC Broiler Cobb 500',
+ product_category: 'DOC',
+ product_sub_category: 'DOC Broiler',
+ source_warehouse: 'Gudang Pusat',
+ destination_warehouse: 'Kandang A1',
+ quantity: 5000,
+ unit: 'Ekor',
+ formatted_quantity: '5,000 Ekor',
+ notes: 'DOC berkualitas tinggi dari supplier terpercaya',
+ },
+ {
+ id: 2,
+ date: yesterday,
+ reference_number: 'IN-2025-002',
+ transaction_type: 'Transfer Masuk',
+ product_name: 'Pakan Starter BR-1',
+ product_category: 'Pakan',
+ product_sub_category: 'Starter',
+ source_warehouse: 'Gudang Area Bandung',
+ destination_warehouse: 'Kandang B1',
+ quantity: 100,
+ unit: 'Sak',
+ formatted_quantity: '100 Sak (5,000 Kg)',
+ notes: 'Pakan starter untuk periode awal',
+ },
+ {
+ id: 3,
+ date: lastWeek,
+ reference_number: 'IN-2025-003',
+ transaction_type: 'Pembelian',
+ product_name: 'Vitamin B Complex',
+ product_category: 'OVK',
+ product_sub_category: 'Vitamin',
+ source_warehouse: 'Supplier Medion',
+ destination_warehouse: 'Gudang Farmasi',
+ quantity: 50,
+ unit: 'Botol',
+ formatted_quantity: '50 Botol',
+ notes: 'Vitamin untuk meningkatkan daya tahan tubuh',
+ },
+ {
+ id: 4,
+ date: today,
+ reference_number: 'IN-2025-004',
+ transaction_type: 'Pembelian',
+ product_name: 'Pakan Finisher BR-2',
+ product_category: 'Pakan',
+ product_sub_category: 'Finisher',
+ source_warehouse: 'Gudang Pusat',
+ destination_warehouse: 'Kandang C1',
+ quantity: 200,
+ unit: 'Sak',
+ formatted_quantity: '200 Sak (10,000 Kg)',
+ notes: 'Pakan finisher untuk periode akhir',
+ },
+ {
+ id: 5,
+ date: yesterday,
+ reference_number: 'IN-2025-005',
+ transaction_type: 'Transfer Masuk',
+ product_name: 'Antibiotik Enrofloxacin',
+ product_category: 'OVK',
+ product_sub_category: 'Obat',
+ source_warehouse: 'Gudang Area Jakarta',
+ destination_warehouse: 'Gudang Farmasi',
+ quantity: 30,
+ unit: 'Box',
+ formatted_quantity: '30 Box',
+ notes: 'Antibiotik untuk pencegahan penyakit',
+ },
+];
+
+// ======================
+// 📤 Outgoing Sapronak Dummy Data
+// ======================
+export const dummyOutgoingSapronaks: ClosingOutgoingSapronak[] = [
+ {
+ id: 1,
+ date: today,
+ reference_number: 'OUT-2025-001',
+ transaction_type: 'Pemakaian',
+ product_name: 'Pakan Starter BR-1',
+ product_category: 'Pakan',
+ product_sub_category: 'Starter',
+ source_warehouse: 'Kandang A1',
+ destination_warehouse: 'Konsumsi Kandang A1',
+ quantity: 50,
+ unit: 'Sak',
+ formatted_quantity: '50 Sak (2,500 Kg)',
+ notes: 'Pemakaian pakan harian periode starter',
+ },
+ {
+ id: 2,
+ date: yesterday,
+ reference_number: 'OUT-2025-002',
+ transaction_type: 'Transfer Keluar',
+ product_name: 'DOC Broiler Cobb 500',
+ product_category: 'DOC',
+ product_sub_category: 'DOC Broiler',
+ source_warehouse: 'Kandang B1',
+ destination_warehouse: 'Kandang B2',
+ quantity: 1000,
+ unit: 'Ekor',
+ formatted_quantity: '1,000 Ekor',
+ notes: 'Transfer DOC ke kandang baru',
+ },
+ {
+ id: 3,
+ date: lastWeek,
+ reference_number: 'OUT-2025-003',
+ transaction_type: 'Pemakaian',
+ product_name: 'Vitamin B Complex',
+ product_category: 'OVK',
+ product_sub_category: 'Vitamin',
+ source_warehouse: 'Gudang Farmasi',
+ destination_warehouse: 'Konsumsi Kandang C1',
+ quantity: 10,
+ unit: 'Botol',
+ formatted_quantity: '10 Botol',
+ notes: 'Pemberian vitamin untuk meningkatkan kesehatan',
+ },
+ {
+ id: 4,
+ date: today,
+ reference_number: 'OUT-2025-004',
+ transaction_type: 'Pemakaian',
+ product_name: 'Pakan Finisher BR-2',
+ product_category: 'Pakan',
+ product_sub_category: 'Finisher',
+ source_warehouse: 'Kandang C1',
+ destination_warehouse: 'Konsumsi Kandang C1',
+ quantity: 80,
+ unit: 'Sak',
+ formatted_quantity: '80 Sak (4,000 Kg)',
+ notes: 'Pemakaian pakan harian periode finisher',
+ },
+ {
+ id: 5,
+ date: yesterday,
+ reference_number: 'OUT-2025-005',
+ transaction_type: 'Pemakaian',
+ product_name: 'Antibiotik Enrofloxacin',
+ product_category: 'OVK',
+ product_sub_category: 'Obat',
+ source_warehouse: 'Gudang Farmasi',
+ destination_warehouse: 'Konsumsi Kandang D1',
+ quantity: 5,
+ unit: 'Box',
+ formatted_quantity: '5 Box',
+ notes: 'Pengobatan untuk ayam yang sakit',
+ },
+ {
+ id: 6,
+ date: lastWeek,
+ reference_number: 'OUT-2025-006',
+ transaction_type: 'Transfer Keluar',
+ product_name: 'Pakan Starter BR-1',
+ product_category: 'Pakan',
+ product_sub_category: 'Starter',
+ source_warehouse: 'Kandang E1',
+ destination_warehouse: 'Kandang E2',
+ quantity: 30,
+ unit: 'Sak',
+ formatted_quantity: '30 Sak (1,500 Kg)',
+ notes: 'Transfer pakan antar kandang',
+ },
+];
+
+// ======================
+// 📊 Perhitungan Sapronak Dummy Data
+// ======================
+export const dummySapronakCalculation: ClosingSapronakCalculation = {
+ // DOC Broiler Calculation
+ doc_broiler: {
+ rows: [
+ {
+ id: 1,
+ tanggal: today,
+ no_referensi: 'IN-2025-001',
+ qty_masuk: 5000,
+ qty_keluar: 0,
+ qty_pakai: 0,
+ uraian: 'DOC Broiler Cobb 500',
+ kategori_produk: 'DOC Broiler',
+ harga_beli_per_qty: 8000,
+ total_harga: 40000000,
+ keterangan: 'Pembelian DOC dari supplier',
+ },
+ {
+ id: 2,
+ tanggal: yesterday,
+ no_referensi: 'OUT-2025-002',
+ qty_masuk: 0,
+ qty_keluar: 1000,
+ qty_pakai: 0,
+ uraian: 'DOC Broiler Cobb 500',
+ kategori_produk: 'DOC Broiler',
+ harga_beli_per_qty: 8000,
+ total_harga: 8000000,
+ keterangan: 'Transfer DOC ke kandang lain',
+ },
+ {
+ id: 3,
+ tanggal: lastWeek,
+ no_referensi: 'USE-2025-001',
+ qty_masuk: 0,
+ qty_keluar: 0,
+ qty_pakai: 50,
+ uraian: 'DOC Broiler Cobb 500',
+ kategori_produk: 'DOC Broiler',
+ harga_beli_per_qty: 8000,
+ total_harga: 400000,
+ keterangan: 'Mortalitas DOC',
+ },
+ ],
+ total: {
+ label: 'Total DOC Broiler',
+ qty_masuk: 5000,
+ qty_keluar: 1000,
+ qty_pakai: 50,
+ harga_beli_per_qty: 8000,
+ total_harga: 48400000,
+ },
+ },
+
+ // OVK Calculation
+ ovk: {
+ rows: [
+ {
+ id: 1,
+ tanggal: today,
+ no_referensi: 'IN-2025-003',
+ qty_masuk: 50,
+ qty_keluar: 0,
+ qty_pakai: 0,
+ uraian: 'Vitamin B Complex',
+ kategori_produk: 'Vitamin',
+ harga_beli_per_qty: 150000,
+ total_harga: 7500000,
+ keterangan: 'Pembelian vitamin',
+ },
+ {
+ id: 2,
+ tanggal: yesterday,
+ no_referensi: 'IN-2025-005',
+ qty_masuk: 30,
+ qty_keluar: 0,
+ qty_pakai: 0,
+ uraian: 'Antibiotik Enrofloxacin',
+ kategori_produk: 'Obat',
+ harga_beli_per_qty: 250000,
+ total_harga: 7500000,
+ keterangan: 'Pembelian antibiotik',
+ },
+ {
+ id: 3,
+ tanggal: lastWeek,
+ no_referensi: 'OUT-2025-003',
+ qty_masuk: 0,
+ qty_keluar: 0,
+ qty_pakai: 10,
+ uraian: 'Vitamin B Complex',
+ kategori_produk: 'Vitamin',
+ harga_beli_per_qty: 150000,
+ total_harga: 1500000,
+ keterangan: 'Pemakaian vitamin',
+ },
+ {
+ id: 4,
+ tanggal: yesterday,
+ no_referensi: 'OUT-2025-005',
+ qty_masuk: 0,
+ qty_keluar: 0,
+ qty_pakai: 5,
+ uraian: 'Antibiotik Enrofloxacin',
+ kategori_produk: 'Obat',
+ harga_beli_per_qty: 250000,
+ total_harga: 1250000,
+ keterangan: 'Pemakaian antibiotik',
+ },
+ ],
+ total: {
+ label: 'Total OVK',
+ qty_masuk: 80,
+ qty_keluar: 0,
+ qty_pakai: 15,
+ harga_beli_per_qty: 200000,
+ total_harga: 17750000,
+ },
+ },
+
+ // Pakan Calculation
+ pakan: {
+ rows: [
+ {
+ id: 1,
+ tanggal: yesterday,
+ no_referensi: 'IN-2025-002',
+ qty_masuk: 100,
+ qty_keluar: 0,
+ qty_pakai: 0,
+ uraian: 'Pakan Starter BR-1',
+ kategori_produk: 'Starter',
+ harga_beli_per_qty: 450000,
+ total_harga: 45000000,
+ keterangan: 'Pembelian pakan starter',
+ },
+ {
+ id: 2,
+ tanggal: today,
+ no_referensi: 'IN-2025-004',
+ qty_masuk: 200,
+ qty_keluar: 0,
+ qty_pakai: 0,
+ uraian: 'Pakan Finisher BR-2',
+ kategori_produk: 'Finisher',
+ harga_beli_per_qty: 480000,
+ total_harga: 96000000,
+ keterangan: 'Pembelian pakan finisher',
+ },
+ {
+ id: 3,
+ tanggal: today,
+ no_referensi: 'OUT-2025-001',
+ qty_masuk: 0,
+ qty_keluar: 0,
+ qty_pakai: 50,
+ uraian: 'Pakan Starter BR-1',
+ kategori_produk: 'Starter',
+ harga_beli_per_qty: 450000,
+ total_harga: 22500000,
+ keterangan: 'Pemakaian pakan starter',
+ },
+ {
+ id: 4,
+ tanggal: today,
+ no_referensi: 'OUT-2025-004',
+ qty_masuk: 0,
+ qty_keluar: 0,
+ qty_pakai: 80,
+ uraian: 'Pakan Finisher BR-2',
+ kategori_produk: 'Finisher',
+ harga_beli_per_qty: 480000,
+ total_harga: 38400000,
+ keterangan: 'Pemakaian pakan finisher',
+ },
+ {
+ id: 5,
+ tanggal: lastWeek,
+ no_referensi: 'OUT-2025-006',
+ qty_masuk: 0,
+ qty_keluar: 30,
+ qty_pakai: 0,
+ uraian: 'Pakan Starter BR-1',
+ kategori_produk: 'Starter',
+ harga_beli_per_qty: 450000,
+ total_harga: 13500000,
+ keterangan: 'Transfer pakan ke kandang lain',
+ },
+ ],
+ total: {
+ label: 'Total Pakan',
+ qty_masuk: 300,
+ qty_keluar: 30,
+ qty_pakai: 130,
+ harga_beli_per_qty: 465000,
+ total_harga: 215400000,
+ },
+ },
+};
+
+// ======================
+// 💰 Overhead Dummy Data
+// ======================
+export const dummyOverhead: ClosingOverhead = {
+ overheads: [
+ {
+ item_name: 'Expedisi DOC',
+ uom_name: 'Ekor',
+ budget_quantity: 500,
+ budget_unit_price: 8000,
+ budget_total_amount: 4000000,
+ actual_date: '',
+ actual_quantity: 0,
+ actual_unit_price: 0,
+ actual_total_amount: 0,
+ cost_per_bird: 0,
+ },
+ {
+ item_name: 'Solar',
+ uom_name: 'Liter',
+ budget_quantity: 0,
+ budget_unit_price: 0,
+ budget_total_amount: 0,
+ actual_date: today,
+ actual_quantity: 20,
+ actual_unit_price: 10000,
+ actual_total_amount: 200000,
+ cost_per_bird: 200,
+ },
+ {
+ item_name: 'Gaji Karyawan Kandang',
+ uom_name: 'Orang',
+ budget_quantity: 3,
+ budget_unit_price: 3000000,
+ budget_total_amount: 9000000,
+ actual_date: today,
+ actual_quantity: 3,
+ actual_unit_price: 3200000,
+ actual_total_amount: 9600000,
+ cost_per_bird: 640,
+ },
+ {
+ item_name: 'Listrik Kandang',
+ uom_name: 'Bulan',
+ budget_quantity: 1,
+ budget_unit_price: 2500000,
+ budget_total_amount: 2500000,
+ actual_date: today,
+ actual_quantity: 1,
+ actual_unit_price: 2800000,
+ actual_total_amount: 2800000,
+ cost_per_bird: 187,
+ },
+ {
+ item_name: 'Air Bersih',
+ uom_name: 'Bulan',
+ budget_quantity: 1,
+ budget_unit_price: 500000,
+ budget_total_amount: 500000,
+ actual_date: today,
+ actual_quantity: 1,
+ actual_unit_price: 450000,
+ actual_total_amount: 450000,
+ cost_per_bird: 30,
+ },
+ {
+ item_name: 'Perbaikan Kandang',
+ uom_name: 'Paket',
+ budget_quantity: 1,
+ budget_unit_price: 3000000,
+ budget_total_amount: 3000000,
+ actual_date: yesterday,
+ actual_quantity: 1,
+ actual_unit_price: 3500000,
+ actual_total_amount: 3500000,
+ cost_per_bird: 233,
+ },
+ {
+ item_name: 'Service Peralatan',
+ uom_name: 'Kali',
+ budget_quantity: 2,
+ budget_unit_price: 500000,
+ budget_total_amount: 1000000,
+ actual_date: lastWeek,
+ actual_quantity: 2,
+ actual_unit_price: 550000,
+ actual_total_amount: 1100000,
+ cost_per_bird: 73,
+ },
+ {
+ item_name: 'ATK & Supplies',
+ uom_name: 'Paket',
+ budget_quantity: 1,
+ budget_unit_price: 500000,
+ budget_total_amount: 500000,
+ actual_date: today,
+ actual_quantity: 1,
+ actual_unit_price: 450000,
+ actual_total_amount: 450000,
+ cost_per_bird: 30,
+ },
+ {
+ item_name: 'Biaya Komunikasi',
+ uom_name: 'Bulan',
+ budget_quantity: 1,
+ budget_unit_price: 300000,
+ budget_total_amount: 300000,
+ actual_date: today,
+ actual_quantity: 1,
+ actual_unit_price: 320000,
+ actual_total_amount: 320000,
+ cost_per_bird: 21,
+ },
+ {
+ item_name: 'BBM Kendaraan Operasional',
+ uom_name: 'Liter',
+ budget_quantity: 200,
+ budget_unit_price: 10000,
+ budget_total_amount: 2000000,
+ actual_date: today,
+ actual_quantity: 220,
+ actual_unit_price: 10500,
+ actual_total_amount: 2310000,
+ cost_per_bird: 154,
+ },
+ ],
+ total: {
+ budget_quantity: 710,
+ budget_total_amount: 23300000,
+ actual_quantity: 250,
+ actual_total_amount: 24530000,
+ cost_per_bird: 1568,
+ },
+};
+
+// ======================
+// 🔧 Dummy API Response Functions
+// ======================
+
+/**
+ * Dummy implementation for getAllFetcher
+ * Returns all closing records
+ */
+export const dummyGetAllFetcher = async (): Promise<{
+ code: number;
+ status: 'success';
+ message: string;
+ data: Closing[];
+}> => {
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data closing berhasil diambil',
+ data: dummyClosings,
+ };
+};
+
+/**
+ * Dummy implementation for getSingle
+ * Returns a single closing by ID
+ */
+export const dummyGetSingle = async (
+ id: number
+): Promise | undefined> => {
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ const closing = dummyClosings.find((c) => c.id === id);
+
+ if (!closing) {
+ return {
+ code: 404,
+ status: 'error',
+ message: `Closing dengan ID ${id} tidak ditemukan`,
+ };
+ }
+
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data closing berhasil diambil',
+ data: closing,
+ };
+};
+
+/**
+ * Dummy implementation for getAllIncomingSapronakFetcher
+ * Returns all incoming sapronak records
+ */
+export const dummyGetAllIncomingSapronakFetcher = async (): Promise<{
+ code: number;
+ status: 'success';
+ message: string;
+ data: ClosingIncomingSapronak[];
+}> => {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data sapronak masuk berhasil diambil',
+ data: dummyIncomingSapronaks,
+ };
+};
+
+/**
+ * Dummy implementation for getAllOutgoingSapronakFetcher
+ * Returns all outgoing sapronak records
+ */
+export const dummyGetAllOutgoingSapronakFetcher = async (): Promise<{
+ code: number;
+ status: 'success';
+ message: string;
+ data: ClosingOutgoingSapronak[];
+}> => {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data sapronak keluar berhasil diambil',
+ data: dummyOutgoingSapronaks,
+ };
+};
+
+/**
+ * Dummy implementation for getGeneralInfo
+ * Returns closing general information by ID
+ */
+export const dummyGetGeneralInfo = async (
+ id: number
+): Promise | undefined> => {
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ const closingInfo = dummyClosingGeneralInformations.find((c) => c.id == id);
+
+ if (!closingInfo) {
+ return {
+ code: 404,
+ status: 'error',
+ message: `Closing general information dengan ID ${id} tidak ditemukan`,
+ };
+ }
+
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data closing general information berhasil diambil',
+ data: closingInfo,
+ };
+};
+
+/**
+ * Dummy implementation for getPerhitunganSapronak
+ * Returns sapronak calculation data
+ */
+export const dummyGetPerhitunganSapronak = async (
+ id: number
+): Promise<
+ | {
+ code: number;
+ status: 'success';
+ message: string;
+ data: ClosingSapronakCalculation;
+ }
+ | undefined
+> => {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data perhitungan sapronak berhasil diambil',
+ data: dummySapronakCalculation,
+ };
+};
+
+/**
+ * Dummy implementation for getOverhead
+ * Returns overhead data
+ */
+export const dummyGetOverhead = async (
+ id: number
+): Promise | undefined> => {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Data overhead berhasil diambil',
+ data: dummyOverhead,
+ };
+};
+
+export const dummyClosingProductionData: ClosingProductionData = {
+ purchase: {
+ initial_population: 12000,
+ claim_culling: 150,
+ final_population: 11850,
+ feed_in: 24000,
+ feed_used: 22500,
+ feed_used_per_head: 1.9,
+ },
+
+ sales: {
+ chicken: {
+ sales_population: 10500,
+ sales_weight: 21000,
+ average_weight: 2.0,
+ chicken_average_selling_price: 28500,
+ },
+ egg: {
+ egg_pieces: 185000,
+ egg_mass_kg: 9250,
+ average_egg_weight_kg: 0.05,
+ egg_average_selling_price: 1800,
+ },
+ },
+
+ performance: {
+ depletion: 150,
+ age_day: 35,
+ mortality_std: 3.5,
+ mortality_act: 4.2,
+ deff_mortality: 0.7,
+ fcr_std: 1.6,
+ fcr_act: 1.72,
+ deff_fcr: 0.12,
+ awg: 60,
+ },
+};
diff --git a/src/dummy/report/marketing-report.dummy.ts b/src/dummy/report/marketing-report.dummy.ts
new file mode 100644
index 00000000..ea5af398
--- /dev/null
+++ b/src/dummy/report/marketing-report.dummy.ts
@@ -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 =
+ {
+ 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,
+ },
+ },
+ };
diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts
index 16cf24cf..6e38a5e4 100644
--- a/src/services/api/closing.ts
+++ b/src/services/api/closing.ts
@@ -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 {
constructor(basePath: string) {
super(basePath);
@@ -71,6 +86,24 @@ export class ClosingApiService extends BaseApiService {
}
}
+ async getProductionData(
+ id: number
+ ): Promise | undefined> {
+ try {
+ const getProductionDataPath = `${this.basePath}/${id}/production-data`;
+ const getProductionDataRes = await httpClient<
+ BaseApiResponse
+ >(getProductionDataPath);
+
+ return getProductionDataRes;
+ } catch (error) {
+ if (axios.isAxiosError>(error)) {
+ return error.response?.data;
+ }
+ return undefined;
+ }
+ }
+
async getPerhitunganSapronak(
id: number
): Promise | undefined> {
diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts
new file mode 100644
index 00000000..b1bcafae
--- /dev/null
+++ b/src/services/api/report/marketing-report.ts
@@ -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> {
+ return await httpClientFetcher>(
+ endpoint
+ );
+ }
+
+ async exportDailyMarketingToExcel(initialQueryString: string) {
+ const params = new URLSearchParams(initialQueryString);
+
+ params.set('limit', '9999999');
+
+ const queryString = `?${params.toString()}`;
+
+ try {
+ const dailyMarketingsReport = await httpClientFetcher<
+ BaseApiResponse
+ >(`${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'
+);
diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts
index 04eca605..38d135ab 100644
--- a/src/types/api/closing.d.ts
+++ b/src/types/api/closing.d.ts
@@ -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 ======
diff --git a/src/types/api/master-data/kandang.d.ts b/src/types/api/master-data/kandang.d.ts
index c9c14882..eafa0334 100644
--- a/src/types/api/master-data/kandang.d.ts
+++ b/src/types/api/master-data/kandang.d.ts
@@ -10,7 +10,6 @@ export type BaseKandang = {
capacity: number;
pic: BaseUser;
project_flock_kandang_id?: number;
- capacity: number;
};
export type Kandang = BaseMetadata & BaseKandang;
diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts
new file mode 100644
index 00000000..d1e81f77
--- /dev/null
+++ b/src/types/api/report/marketing.d.ts
@@ -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';
+};