mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
Merge branch 'dev/randy' into 'feat/finance-debt-supplier-report'
[FEAT/FE] Debt Supplier Report See merge request mbugroup/lti-web-client!153
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
@@ -50,7 +49,7 @@ const DashboardProduction = () => {
|
||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||
'OVERVIEW'
|
||||
);
|
||||
const [endpointUrl, setEndpointUrl] = useState('/dashboard');
|
||||
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
||||
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]);
|
||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||
|
||||
@@ -84,8 +83,8 @@ const DashboardProduction = () => {
|
||||
limit: 'limit',
|
||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||
});
|
||||
const comparedByOptions = [
|
||||
{ value: 'LOCATION', label: 'Farm' },
|
||||
const comparisonTypeOptions = [
|
||||
{ value: 'FARM', label: 'Farm' },
|
||||
{ value: 'FLOCK', label: 'Flock' },
|
||||
{ value: 'KANDANG', label: 'Kandang' },
|
||||
];
|
||||
@@ -99,7 +98,7 @@ const DashboardProduction = () => {
|
||||
location: [] as OptionType[],
|
||||
kandang: [] as OptionType[],
|
||||
analysisMode: analysisMode,
|
||||
comparedBy: '',
|
||||
comparisonType: '',
|
||||
lokasiIds: [],
|
||||
flockIds: [],
|
||||
kandangIds: [],
|
||||
@@ -115,6 +114,7 @@ const DashboardProduction = () => {
|
||||
location_ids: normalizeToArray(values.location),
|
||||
flock_ids: normalizeToArray(values.flock),
|
||||
kandang_ids: normalizeToArray(values.kandang),
|
||||
comparison_type: values.comparisonType,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -122,7 +122,7 @@ const DashboardProduction = () => {
|
||||
const handleResetFilter = () => {
|
||||
formik.resetForm();
|
||||
setAnalysisMode('OVERVIEW');
|
||||
setEndpointUrl('/dashboard');
|
||||
setEndpointUrl('/dashboards');
|
||||
};
|
||||
|
||||
const handleApplyFilter = (values: DashboardFilter) => {
|
||||
@@ -140,8 +140,9 @@ const DashboardProduction = () => {
|
||||
params.flock_ids = values.flock_ids.toString();
|
||||
if (values.kandang_ids.length > 0)
|
||||
params.kandang_ids = values.kandang_ids.toString();
|
||||
if (values.comparison_type) params.comparison_type = values.comparison_type;
|
||||
|
||||
setEndpointUrl(`/dashboard?${new URLSearchParams(params).toString()}`);
|
||||
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
||||
console.log(endpointUrl);
|
||||
filterModal.closeModal();
|
||||
refreshDashboardProductionData();
|
||||
@@ -262,7 +263,13 @@ const DashboardProduction = () => {
|
||||
data={dashboardProductionData}
|
||||
/>
|
||||
) : (
|
||||
<DashboardLineChartSkeleton />
|
||||
<DashboardLineChartSkeleton
|
||||
meta={
|
||||
isResponseSuccess(dashboardProductionResponse)
|
||||
? (dashboardProductionResponse.meta as unknown as DashboardMeta)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -343,7 +350,7 @@ const DashboardProduction = () => {
|
||||
formik.setFieldValue('location', []);
|
||||
formik.setFieldValue('flock', []);
|
||||
formik.setFieldValue('kandang', []);
|
||||
formik.setFieldValue('comparedBy', '');
|
||||
formik.setFieldValue('comparisonType', '');
|
||||
setSelectedLocationIds([]);
|
||||
}}
|
||||
color='primary'
|
||||
@@ -368,21 +375,21 @@ const DashboardProduction = () => {
|
||||
<div className='px-4'>
|
||||
<SelectInput
|
||||
label='Compared By'
|
||||
value={comparedByOptions.find(
|
||||
(option) => option.value === formik.values.comparedBy
|
||||
value={comparisonTypeOptions.find(
|
||||
(option) => option.value === formik.values.comparisonType
|
||||
)}
|
||||
onChange={(selected) =>
|
||||
formik.setFieldValue(
|
||||
'comparedBy',
|
||||
'comparisonType',
|
||||
selected ? (selected as OptionType).value : ''
|
||||
)
|
||||
}
|
||||
errorMessage={formik.errors.comparedBy as string}
|
||||
options={comparedByOptions}
|
||||
errorMessage={formik.errors.comparisonType as string}
|
||||
options={comparisonTypeOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isError={
|
||||
Boolean(formik.errors.comparedBy) &&
|
||||
Boolean(formik.touched.comparedBy)
|
||||
Boolean(formik.errors.comparisonType) &&
|
||||
Boolean(formik.touched.comparisonType)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -405,9 +412,9 @@ const DashboardProduction = () => {
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isMulti={
|
||||
comparedByOptions.find(
|
||||
(option) => option.value === formik.values.comparedBy
|
||||
)?.value === 'LOCATION'
|
||||
comparisonTypeOptions.find(
|
||||
(option) => option.value === formik.values.comparisonType
|
||||
)?.value === 'FARM'
|
||||
}
|
||||
isError={
|
||||
Boolean(formik.errors.location) &&
|
||||
@@ -420,8 +427,8 @@ const DashboardProduction = () => {
|
||||
{!(
|
||||
formik.values.analysisMode === 'COMPARISON' &&
|
||||
!(
|
||||
formik.values.comparedBy === 'FLOCK' ||
|
||||
formik.values.comparedBy === 'KANDANG'
|
||||
formik.values.comparisonType === 'FLOCK' ||
|
||||
formik.values.comparisonType === 'KANDANG'
|
||||
)
|
||||
) && (
|
||||
<div className='px-4'>
|
||||
@@ -435,8 +442,8 @@ const DashboardProduction = () => {
|
||||
options={flockOptions}
|
||||
isLoading={isLoadingFlockOptions}
|
||||
isMulti={
|
||||
comparedByOptions.find(
|
||||
(option) => option.value === formik.values.comparedBy
|
||||
comparisonTypeOptions.find(
|
||||
(option) => option.value === formik.values.comparisonType
|
||||
)?.value === 'FLOCK'
|
||||
}
|
||||
isError={
|
||||
@@ -450,7 +457,7 @@ const DashboardProduction = () => {
|
||||
{/* Kandang */}
|
||||
{!(
|
||||
formik.values.analysisMode === 'COMPARISON' &&
|
||||
!(formik.values.comparedBy === 'KANDANG')
|
||||
!(formik.values.comparisonType === 'KANDANG')
|
||||
) && (
|
||||
<div className='px-4'>
|
||||
<SelectInput
|
||||
@@ -463,8 +470,8 @@ const DashboardProduction = () => {
|
||||
options={kandangOptions}
|
||||
isLoading={isLoadingKandangOptions}
|
||||
isMulti={
|
||||
comparedByOptions.find(
|
||||
(option) => option.value === formik.values.comparedBy
|
||||
comparisonTypeOptions.find(
|
||||
(option) => option.value === formik.values.comparisonType
|
||||
)?.value === 'KANDANG'
|
||||
}
|
||||
isError={
|
||||
|
||||
@@ -5,7 +5,7 @@ export type DashboardFilterType = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
analysisMode: string;
|
||||
comparedBy: string | undefined;
|
||||
comparisonType: string | undefined;
|
||||
location: OptionType | OptionType[];
|
||||
lokasiIds: number[] | undefined;
|
||||
flock: OptionType | OptionType[] | undefined;
|
||||
@@ -20,7 +20,7 @@ export const DashboardFilterOverviewSchema: yup.ObjectSchema<DashboardFilterType
|
||||
startDate: yup.string().required('Start date is required'),
|
||||
endDate: yup.string().required('End date is required'),
|
||||
analysisMode: yup.string().required('Analysis mode is required'),
|
||||
comparedBy: yup.string().when('analysisMode', {
|
||||
comparisonType: yup.string().when('analysisMode', {
|
||||
is: 'COMPARISON',
|
||||
then: (schema) => schema.required('Compared by is required'),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
@@ -63,7 +63,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema<DashboardFilterTy
|
||||
startDate: yup.string().required('Start date is required'),
|
||||
endDate: yup.string().required('End date is required'),
|
||||
analysisMode: yup.string().required('Analysis mode is required'),
|
||||
comparedBy: yup.string().when('analysisMode', {
|
||||
comparisonType: yup.string().when('analysisMode', {
|
||||
is: 'COMPARISON',
|
||||
then: (schema) => schema.required('Compared by is required'),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
@@ -80,7 +80,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema<DashboardFilterTy
|
||||
}
|
||||
return !!value;
|
||||
}),
|
||||
flock: yup.mixed<OptionType | OptionType[]>().when('comparedBy', {
|
||||
flock: yup.mixed<OptionType | OptionType[]>().when('comparisonType', {
|
||||
is: (value: string) => value === 'FLOCK' || value === 'KANDANG',
|
||||
then: (schema) =>
|
||||
schema.test('is-required', 'Flock is required', (value) => {
|
||||
@@ -91,7 +91,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema<DashboardFilterTy
|
||||
}),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
kandang: yup.mixed<OptionType | OptionType[]>().when('comparedBy', {
|
||||
kandang: yup.mixed<OptionType | OptionType[]>().when('comparisonType', {
|
||||
is: 'KANDANG',
|
||||
then: (schema) =>
|
||||
schema.test('is-required', 'Kandang is required', (value) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import { DashboardMeta } from '@/types/api/dashboard/dashboard';
|
||||
|
||||
const DashboardLineChartSkeleton = () => {
|
||||
const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => {
|
||||
return (
|
||||
<div className='w-full bg-white rounded-lg shadow-sm border border-gray-200 p-6 relative'>
|
||||
{/* Header with title skeleton */}
|
||||
@@ -32,24 +33,49 @@ const DashboardLineChartSkeleton = () => {
|
||||
<div className='flex-1 relative'>
|
||||
{/* Empty state centered in chart area */}
|
||||
<div className='absolute inset-0 flex flex-col items-center justify-center pb-12'>
|
||||
{/* Filter icon */}
|
||||
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
||||
<Icon
|
||||
icon='heroicons:funnel'
|
||||
className='text-white'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
{!meta?.filters && (
|
||||
<>
|
||||
{/* Filter icon */}
|
||||
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
||||
<Icon
|
||||
icon='heroicons:funnel'
|
||||
className='text-white'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Empty state text */}
|
||||
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
||||
No Filters Selected
|
||||
</h3>
|
||||
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
||||
Please choose filters to narrow down your results and make your
|
||||
search easier.
|
||||
</p>
|
||||
{/* Empty state text */}
|
||||
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
||||
No Filters Selected
|
||||
</h3>
|
||||
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
||||
Please choose filters to narrow down your results and make
|
||||
your search easier.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{meta?.filters && (
|
||||
<>
|
||||
{/* Filter icon */}
|
||||
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
||||
<Icon
|
||||
icon='heroicons:chart-bar'
|
||||
className='text-white'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Empty state text */}
|
||||
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
||||
Data Not Yet Available
|
||||
</h3>
|
||||
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
||||
Please change your filters to get the data.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Placeholder for chart height */}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab';
|
||||
import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab';
|
||||
|
||||
const FinanceTabs = () => {
|
||||
const tabs = [
|
||||
@@ -11,6 +12,12 @@ const FinanceTabs = () => {
|
||||
|
||||
content: <CustomerPaymentTab />,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Rekapitulasi Hutang Ke Supplier',
|
||||
|
||||
content: <DebtSupplierTab />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
StyleSheet,
|
||||
Font,
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
|
||||
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierInfo: {
|
||||
fontSize: 9,
|
||||
marginBottom: 5,
|
||||
color: '#333333',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'left',
|
||||
},
|
||||
tableCellNo: {
|
||||
flex: 0.5,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellHeaderRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
textAlign: 'right',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellCenter: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
summaryRow: {
|
||||
backgroundColor: '#F0F0F0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
interface DebtSupplierExportPDFParams {
|
||||
data: DebtSupplier[];
|
||||
}
|
||||
|
||||
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
return (
|
||||
<Document>
|
||||
{params.data.map((supplierReport, supplierIndex) => (
|
||||
<Page
|
||||
key={supplierIndex}
|
||||
size='A4'
|
||||
orientation='landscape'
|
||||
style={pdfStyles.page}
|
||||
>
|
||||
{/* Title and Supplier Info */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
Laporan > Hutang Supplier
|
||||
</Text>
|
||||
<Text style={pdfStyles.supplierTitle}>
|
||||
{supplierReport.supplier.name}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||
<Text>No</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>No. PR</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>No. PO</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Tgl PR</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Tgl PO</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
|
||||
<Text>Aging</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Area</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Gudang</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Tgl Jatuh Tempo</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Status JT</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Total Harga</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Pembayaran</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Hutang</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Status</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>No. Perjalanan</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Body */}
|
||||
{supplierReport.rows.map((item, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < supplierReport.rows.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.pr_number || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.po_number || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text>
|
||||
{item.pr_date ? formatDate(item.pr_date, 'DD MMM YY') : '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text>
|
||||
{item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.aging)} Hari</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.area?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text>
|
||||
{item.due_date
|
||||
? formatDate(item.due_date, 'DD MMM YY')
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.due_status || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.total_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.payment_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.debt_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.status || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.travel_number || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Summary Row */}
|
||||
{supplierReport.total && (
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.summaryRow]}>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text>Total</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(supplierReport.total.aging)} Hari</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{formatCurrency(supplierReport.total.total_price)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{formatCurrency(supplierReport.total.payment_price)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Page>
|
||||
))}
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
export const generateDebtSupplierPDF = async (
|
||||
params: DebtSupplierExportPDFParams
|
||||
): Promise<void> => {
|
||||
const PDFDocument = createPDFDocument(params);
|
||||
|
||||
try {
|
||||
const blob = await pdf(PDFDocument).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import * as XLSX from 'xlsx';
|
||||
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||
|
||||
interface DebtSupplierExportExcelParams {
|
||||
data: DebtSupplier[];
|
||||
}
|
||||
|
||||
export const generateDebtSupplierExcel = (
|
||||
params: DebtSupplierExportExcelParams
|
||||
): void => {
|
||||
if (!params.data || params.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
params.data.forEach((supplierReport) => {
|
||||
const supplierData = supplierReport.rows;
|
||||
const supplierName = supplierReport.supplier.name || 'Unknown Supplier';
|
||||
|
||||
const excelData: { [key: string]: string | number }[] = supplierData.map(
|
||||
(item, index) => ({
|
||||
No: index + 1,
|
||||
'Nomor PR': item.pr_number || '',
|
||||
'Nomor PO': item.po_number || '',
|
||||
'Tanggal PR': item.pr_date
|
||||
? formatDate(item.pr_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'Tanggal PO': item.po_date
|
||||
? formatDate(item.po_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'Aging (Hari)': formatNumber(item.aging || 0),
|
||||
Area: item.area?.name || '',
|
||||
Gudang: item.warehouse?.name || '',
|
||||
'Tanggal Jatuh Tempo': item.due_date
|
||||
? formatDate(item.due_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'Status Jatuh Tempo': item.due_status || '',
|
||||
'Total Harga': formatCurrency(item.total_price || 0),
|
||||
'Harga Pembayaran': formatCurrency(item.payment_price || 0),
|
||||
'Harga Hutang': formatCurrency(item.debt_price || 0),
|
||||
Status: item.status || '',
|
||||
'Nomor Perjalanan': item.travel_number || '',
|
||||
})
|
||||
);
|
||||
|
||||
if (supplierReport.total) {
|
||||
excelData.push({
|
||||
No: 'Total',
|
||||
'Nomor PR': '',
|
||||
'Nomor PO': '',
|
||||
'Tanggal PR': '',
|
||||
'Tanggal PO': '',
|
||||
'Aging (Hari)': formatNumber(supplierReport.total.aging || 0),
|
||||
Area: '',
|
||||
Gudang: '',
|
||||
'Tanggal Jatuh Tempo': '',
|
||||
'Status Jatuh Tempo': '',
|
||||
'Total Harga': formatCurrency(supplierReport.total.total_price || 0),
|
||||
'Harga Pembayaran': formatCurrency(
|
||||
supplierReport.total.payment_price || 0
|
||||
),
|
||||
'Harga Hutang': formatCurrency(supplierReport.total.debt_price || 0),
|
||||
Status: '',
|
||||
'Nomor Perjalanan': '',
|
||||
});
|
||||
}
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 5 }, // No
|
||||
{ wch: 15 }, // Nomor PR
|
||||
{ wch: 15 }, // Nomor PO
|
||||
{ wch: 15 }, // Tanggal PR
|
||||
{ wch: 15 }, // Tanggal PO
|
||||
{ wch: 12 }, // Aging
|
||||
{ wch: 15 }, // Area
|
||||
{ wch: 15 }, // Gudang
|
||||
{ wch: 18 }, // Tanggal Jatuh Tempo
|
||||
{ wch: 18 }, // Status Jatuh Tempo
|
||||
{ wch: 15 }, // Total Harga
|
||||
{ wch: 15 }, // Harga Pembayaran
|
||||
{ wch: 15 }, // Harga Hutang
|
||||
{ wch: 12 }, // Status
|
||||
{ wch: 15 }, // Nomor Perjalanan
|
||||
];
|
||||
worksheet['!cols'] = colWidths;
|
||||
|
||||
const sheetName =
|
||||
supplierName.length > 31 ? supplierName.substring(0, 31) : supplierName;
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
});
|
||||
|
||||
const filename = `laporan-hutang-supplier-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
|
||||
|
||||
XLSX.writeFile(workbook, filename);
|
||||
};
|
||||
@@ -0,0 +1,607 @@
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import Dropdown from '@/components/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 Modal, { useModal } from '@/components/Modal';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
import { FinanceApi } from '@/services/api/report/finance-report';
|
||||
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
|
||||
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import Pagination from '@/components/Pagination';
|
||||
|
||||
const DebtSupplierTab = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
|
||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||
|
||||
// ===== PAGINATION STATE =====
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// ===== FILTER STATE =====
|
||||
const [filterSupplier, setFilterSupplier] = useState<OptionType[]>([]);
|
||||
const [filterStartDate, setFilterStartDate] = useState('');
|
||||
const [filterEndDate, setFilterEndDate] = useState('');
|
||||
const [filterDataType, setFilterDataType] = useState<OptionType>();
|
||||
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const filterModal = useModal();
|
||||
|
||||
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
||||
useSelect(SupplierApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
|
||||
const dataTypeOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'do_date', label: 'Tanggal Terima' },
|
||||
{ value: 'po_date', label: 'Tanggal PO' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleResetFilters = useCallback(() => {
|
||||
setIsSubmitted(false);
|
||||
setFilterSupplier([]);
|
||||
setFilterStartDate('');
|
||||
setFilterEndDate('');
|
||||
setFilterErrors({});
|
||||
}, []);
|
||||
|
||||
const handleApplyFilters = useCallback(() => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!filterStartDate) {
|
||||
errors.start_date = 'Tanggal mulai wajib diisi';
|
||||
}
|
||||
if (!filterEndDate) {
|
||||
errors.end_date = 'Tanggal akhir wajib diisi';
|
||||
}
|
||||
|
||||
setFilterErrors(errors);
|
||||
|
||||
if (Object.keys(errors).length === 0) {
|
||||
setIsSubmitted(true);
|
||||
setCurrentPage(1);
|
||||
filterModal.closeModal();
|
||||
}
|
||||
}, [filterModal, filterStartDate, filterEndDate]);
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: debtSupplier, isLoading } = useSWR(
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = {
|
||||
supplier_ids:
|
||||
filterSupplier.length > 0
|
||||
? filterSupplier.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
filter_by: 'do_date' as const,
|
||||
start_date: filterStartDate || undefined,
|
||||
end_date: filterEndDate || undefined,
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
};
|
||||
|
||||
return ['debt-supplier-report', params];
|
||||
}
|
||||
: null,
|
||||
([, params]) =>
|
||||
FinanceApi.getDebtSupplierReport(
|
||||
params.supplier_ids,
|
||||
params.filter_by,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.page,
|
||||
params.limit
|
||||
)
|
||||
);
|
||||
// const { data: debtSupplier, isLoading } = useSWR(FinanceApi.basePath, () =>
|
||||
// FinanceApi.getDebtSupplierReport()
|
||||
// );
|
||||
|
||||
const data: DebtSupplier[] = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(debtSupplier)
|
||||
? (debtSupplier?.data as unknown as DebtSupplier[]) || []
|
||||
: [],
|
||||
[debtSupplier]
|
||||
);
|
||||
const meta =
|
||||
isResponseSuccess(debtSupplier) && debtSupplier?.meta
|
||||
? debtSupplier.meta
|
||||
: null;
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const debtSupplierExport = useCallback(async (): Promise<
|
||||
DebtSupplier[] | null
|
||||
> => {
|
||||
const params = {
|
||||
supplier_ids:
|
||||
filterSupplier.length > 0
|
||||
? filterSupplier.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
filter_by: 'do_date' as const,
|
||||
start_date: filterStartDate || undefined,
|
||||
end_date: filterEndDate || undefined,
|
||||
date_type: filterDataType ? filterDataType.value : undefined,
|
||||
limit: 100,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
const response = await FinanceApi.getDebtSupplierReport(
|
||||
params.supplier_ids,
|
||||
params.filter_by,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.page,
|
||||
params.limit
|
||||
);
|
||||
|
||||
return isResponseSuccess(response)
|
||||
? (response.data as unknown as DebtSupplier[])
|
||||
: null;
|
||||
}, [filterSupplier, filterStartDate, filterEndDate]);
|
||||
|
||||
// ===== EXPORT HANDLERS =====
|
||||
const handleExportExcel = useCallback(async () => {
|
||||
setIsExcelExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await debtSupplierExport();
|
||||
|
||||
if (
|
||||
!allDataForExport ||
|
||||
!Array.isArray(allDataForExport) ||
|
||||
allDataForExport.length === 0
|
||||
) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
generateDebtSupplierExcel({ data: allDataForExport });
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [debtSupplierExport]);
|
||||
|
||||
const handleExportPdf = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
try {
|
||||
const allDataForExport = await debtSupplierExport();
|
||||
|
||||
if (
|
||||
!allDataForExport ||
|
||||
!Array.isArray(allDataForExport) ||
|
||||
allDataForExport.length === 0
|
||||
) {
|
||||
toast.error('Tidak ada data untuk diekspor.');
|
||||
return;
|
||||
}
|
||||
|
||||
await generateDebtSupplierPDF({ data: allDataForExport });
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsPdfExportLoading(false);
|
||||
}
|
||||
}, [debtSupplierExport]);
|
||||
|
||||
// ===== PAGINATION HANDLERS =====
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleRowChange = (pageSize: number) => {
|
||||
setPageSize(pageSize);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (meta && currentPage < meta.total_pages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [
|
||||
{
|
||||
id: 'no',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
},
|
||||
{
|
||||
id: 'pr_number',
|
||||
header: 'Nomor PR',
|
||||
accessorKey: 'pr_number',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.pr_number;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'po_number',
|
||||
header: 'Nomor PO',
|
||||
accessorKey: 'po_number',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.po_number;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pr_date',
|
||||
header: 'Tanggal PR',
|
||||
accessorKey: 'pr_date',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.pr_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'po_date',
|
||||
header: 'Tanggal PO',
|
||||
accessorKey: 'po_date',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.po_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'aging',
|
||||
header: 'Aging',
|
||||
accessorKey: 'aging',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.aging;
|
||||
return <div className='text-center'>{formatNumber(value)} Hari</div>;
|
||||
},
|
||||
footer: () => {
|
||||
const value = supplier.total.aging;
|
||||
return <div className='text-center'>{formatNumber(value)} Hari</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'area',
|
||||
header: 'Area',
|
||||
accessorKey: 'area',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.area?.name;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'warehouse',
|
||||
header: 'Gudang',
|
||||
accessorKey: 'warehouse',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.warehouse?.name;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'due_date',
|
||||
header: 'Tanggal Jatuh Tempo',
|
||||
accessorKey: 'due_date',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.due_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'due_status',
|
||||
header: 'Status Jatuh Tempo',
|
||||
accessorKey: 'due_status',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.due_status;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'total_price',
|
||||
header: 'Total Harga',
|
||||
accessorKey: 'total_price',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.total_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => {
|
||||
const value = supplier.total.total_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'payment_price',
|
||||
header: 'Harga Pembayaran',
|
||||
accessorKey: 'payment_price',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.payment_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => {
|
||||
const value = supplier.total.payment_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'debt_price',
|
||||
header: 'Harga Hutang',
|
||||
accessorKey: 'debt_price',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.debt_price;
|
||||
return (
|
||||
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
footer: () => {
|
||||
const value = supplier.total.debt_price;
|
||||
return (
|
||||
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.status;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'travel_number',
|
||||
header: 'Nomor Perjalanan',
|
||||
accessorKey: 'travel_number',
|
||||
cell: (props) => {
|
||||
const value = props.row.original.travel_number;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-4 flex flex-col gap-4'>
|
||||
<Card
|
||||
subtitle='Laporan > Kontrol Hutang Supplier'
|
||||
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||
>
|
||||
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||
<Button variant='outline' onClick={filterModal.openModal}>
|
||||
<Icon icon='heroicons:funnel' width={18} height={18} />
|
||||
Filter
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
trigger={
|
||||
<Button variant='outline' isLoading={isAnyExportLoading}>
|
||||
<Icon
|
||||
icon='heroicons:cloud-arrow-down'
|
||||
width={18}
|
||||
height={18}
|
||||
/>
|
||||
Export
|
||||
</Button>
|
||||
}
|
||||
align='end'
|
||||
>
|
||||
<Menu>
|
||||
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||
<MenuItem title='PDF' onClick={handleExportPdf} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{!isSubmitted ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Silakan klik tombol Filter untuk mengatur filter dan menampilkan
|
||||
data.
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<div className='mt-6 text-center text-gray-500'>
|
||||
Tidak ada data yang dapat ditampilkan...
|
||||
</div>
|
||||
) : (
|
||||
data.map((supplierReport) => {
|
||||
return (
|
||||
<Card
|
||||
key={supplierReport.supplier.id}
|
||||
title={supplierReport.supplier.name}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
variant='bordered'
|
||||
collapsible={true}
|
||||
>
|
||||
<Table
|
||||
data={supplierReport.rows}
|
||||
columns={getTableColumns(supplierReport)}
|
||||
pageSize={10}
|
||||
renderFooter={supplierReport.rows.length > 0}
|
||||
className={{
|
||||
containerClassName: 'w-full',
|
||||
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
tableFooterClassName:
|
||||
'bg-gray-100 font-semibold border border-gray-200',
|
||||
footerRowClassName: 'border-t-2 border-gray-300',
|
||||
footerColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{meta && data.length > 0 && (
|
||||
<div className='mt-6'>
|
||||
<Pagination
|
||||
currentPage={meta.page}
|
||||
totalItems={meta.total_results}
|
||||
onPageChange={handlePageChange}
|
||||
onRowChange={handleRowChange}
|
||||
onNextPage={handleNextPage}
|
||||
onPrevPage={handlePrevPage}
|
||||
rowOptions={[10, 25, 50, 100]}
|
||||
itemsPerPage={meta.limit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-semibold'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='space-y-4 px-4'>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
|
||||
<div>
|
||||
<DateInput
|
||||
label='Tanggal'
|
||||
name='start_date'
|
||||
value={filterStartDate}
|
||||
onChange={(e) => {
|
||||
setFilterStartDate(e.target.value);
|
||||
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
{filterErrors.start_date && (
|
||||
<p className='text-red-500 text-sm mt-1'>
|
||||
{filterErrors.start_date}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mt-auto'>
|
||||
<DateInput
|
||||
name='end_date'
|
||||
value={filterEndDate}
|
||||
onChange={(e) => {
|
||||
setFilterEndDate(e.target.value);
|
||||
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
{filterErrors.end_date && (
|
||||
<p className='text-red-500 text-sm mt-1'>
|
||||
{filterErrors.end_date}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectInput
|
||||
label='Supplier'
|
||||
placeholder='Pilih Supplier'
|
||||
isMulti
|
||||
options={supplierOptions}
|
||||
value={filterSupplier}
|
||||
onChange={(val) => {
|
||||
setFilterSupplier(
|
||||
Array.isArray(val) ? val : val ? [val] : []
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingSuppliers}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectInput
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
options={dataTypeOptions}
|
||||
value={filterDataType}
|
||||
onChange={(val) => {
|
||||
setFilterDataType(val ? (val as OptionType) : undefined);
|
||||
}}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
||||
<Button
|
||||
variant='soft'
|
||||
className='ms-4 min-w-36 rounded-lg'
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
className='me-4 min-w-36 rounded-lg'
|
||||
onClick={handleApplyFilters}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DebtSupplierTab;
|
||||
@@ -117,7 +117,10 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/report/expense/': ['lti.repport.expense.list'],
|
||||
'/report/marketing/': ['lti.repport.delivery.list'],
|
||||
'/report/production-result/': ['lti.repport.production_result.list'],
|
||||
'/report/finance/': ['lti.repport.finance.list'],
|
||||
'/report/finance/': [
|
||||
'lti.repport.finance.list',
|
||||
'lti.repport.debtsupplier.list',
|
||||
],
|
||||
|
||||
// Inventory
|
||||
'/inventory/adjustment/': ['lti.inventory.list'],
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
{
|
||||
"code": 200,
|
||||
"status": "success",
|
||||
"message": "Get dashboard performance flock comparison successfully",
|
||||
"meta": {
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"total_pages": 1,
|
||||
"total_results": 1,
|
||||
"filters": {
|
||||
"start_date": "2025-12-01",
|
||||
"end_date": "2025-12-31",
|
||||
"analysis_mode": "COMPARASION",
|
||||
"lokasi_ids": [1],
|
||||
"flock_ids": [1, 2, 3],
|
||||
"kandang_ids": []
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"statistics_data": [
|
||||
{
|
||||
"label": "HPP Global",
|
||||
"value": 16200,
|
||||
"percent_last_month": 15.5
|
||||
},
|
||||
{
|
||||
"label": "Avg. Selling Price",
|
||||
"value": 28300,
|
||||
"percent_last_month": -50
|
||||
},
|
||||
{
|
||||
"label": "FCR",
|
||||
"value": 24.02,
|
||||
"percent_last_month": 15.5
|
||||
},
|
||||
{
|
||||
"label": "Mortality",
|
||||
"value": 5,
|
||||
"percent_last_month": -15.5
|
||||
}
|
||||
],
|
||||
"charts": {
|
||||
"flock": {
|
||||
"series": [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "Flock Dago",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"label": "Flock Sulanjana",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"label": "Flock Garut 2",
|
||||
"unit": "%"
|
||||
}
|
||||
],
|
||||
"dataset": [
|
||||
{
|
||||
"week": 1,
|
||||
"1": 18.5,
|
||||
"2": 20.2,
|
||||
"3": 17.8
|
||||
},
|
||||
{
|
||||
"week": 2,
|
||||
"1": 19.2,
|
||||
"2": 21.5,
|
||||
"3": 18.1
|
||||
},
|
||||
{
|
||||
"week": 3,
|
||||
"1": 20.1,
|
||||
"2": 22.8,
|
||||
"3": 18.5
|
||||
},
|
||||
{
|
||||
"week": 4,
|
||||
"1": 21.5,
|
||||
"2": 24.0,
|
||||
"3": 19.2
|
||||
},
|
||||
{
|
||||
"week": 5,
|
||||
"1": 22.8,
|
||||
"2": 23.5,
|
||||
"3": 20.4
|
||||
},
|
||||
{
|
||||
"week": 6,
|
||||
"1": 24.2,
|
||||
"2": 22.1,
|
||||
"3": 21.6
|
||||
},
|
||||
{
|
||||
"week": 7,
|
||||
"1": 25.8,
|
||||
"2": 21.8,
|
||||
"3": 22.9
|
||||
},
|
||||
{
|
||||
"week": 8,
|
||||
"1": 26.5,
|
||||
"2": 22.4,
|
||||
"3": 23.5
|
||||
},
|
||||
{
|
||||
"week": 9,
|
||||
"1": 26.2,
|
||||
"2": 23.9,
|
||||
"3": 24.1
|
||||
},
|
||||
{
|
||||
"week": 10,
|
||||
"1": 25.4,
|
||||
"2": 24.8,
|
||||
"3": 24.8
|
||||
},
|
||||
{
|
||||
"week": 11,
|
||||
"1": 24.8,
|
||||
"2": 26.2,
|
||||
"3": 25.4
|
||||
},
|
||||
{
|
||||
"week": 12,
|
||||
"1": 24.1,
|
||||
"2": 27.5,
|
||||
"3": 26.2
|
||||
},
|
||||
{
|
||||
"week": 13,
|
||||
"1": 23.5,
|
||||
"2": 28.1,
|
||||
"3": 27.5
|
||||
},
|
||||
{
|
||||
"week": 14,
|
||||
"1": 22.8,
|
||||
"2": 27.4,
|
||||
"3": 28.4
|
||||
},
|
||||
{
|
||||
"week": 15,
|
||||
"1": 21.9,
|
||||
"2": 26.5,
|
||||
"3": 29.1
|
||||
},
|
||||
{
|
||||
"week": 16,
|
||||
"1": 21.2,
|
||||
"2": 25.8,
|
||||
"3": 28.5
|
||||
},
|
||||
{
|
||||
"week": 17,
|
||||
"1": 20.8,
|
||||
"2": 24.2,
|
||||
"3": 27.2
|
||||
},
|
||||
{
|
||||
"week": 18,
|
||||
"1": 20.1,
|
||||
"2": 23.1,
|
||||
"3": 26.1
|
||||
},
|
||||
{
|
||||
"week": 19,
|
||||
"1": 19.5,
|
||||
"2": 22.5,
|
||||
"3": 25.8
|
||||
},
|
||||
{
|
||||
"week": 20,
|
||||
"1": 19.8,
|
||||
"2": 21.9,
|
||||
"3": 24.5
|
||||
},
|
||||
{
|
||||
"week": 21,
|
||||
"1": 20.5,
|
||||
"2": 21.4,
|
||||
"3": 23.2
|
||||
},
|
||||
{
|
||||
"week": 22,
|
||||
"1": 21.8,
|
||||
"2": 21.0,
|
||||
"3": 22.8
|
||||
},
|
||||
{
|
||||
"week": 23,
|
||||
"1": 22.5,
|
||||
"2": 21.8,
|
||||
"3": 21.9
|
||||
},
|
||||
{
|
||||
"week": 24,
|
||||
"1": 23.9,
|
||||
"2": 22.5,
|
||||
"3": 21.2
|
||||
},
|
||||
{
|
||||
"week": 25,
|
||||
"1": 24.5,
|
||||
"2": 23.4,
|
||||
"3": 20.5
|
||||
},
|
||||
{
|
||||
"week": 26,
|
||||
"1": 25.1,
|
||||
"2": 24.8,
|
||||
"3": 20.1
|
||||
},
|
||||
{
|
||||
"week": 27,
|
||||
"1": 26.8,
|
||||
"2": 25.5,
|
||||
"3": 19.8
|
||||
},
|
||||
{
|
||||
"week": 28,
|
||||
"1": 27.5,
|
||||
"2": 26.2,
|
||||
"3": 20.4
|
||||
},
|
||||
{
|
||||
"week": 29,
|
||||
"1": 27.2,
|
||||
"2": 27.8,
|
||||
"3": 21.5
|
||||
},
|
||||
{
|
||||
"week": 30,
|
||||
"1": 26.4,
|
||||
"2": 28.5,
|
||||
"3": 22.1
|
||||
},
|
||||
{
|
||||
"week": 31,
|
||||
"1": 25.8,
|
||||
"2": 29.2,
|
||||
"3": 23.4
|
||||
},
|
||||
{
|
||||
"week": 32,
|
||||
"1": 24.9,
|
||||
"2": 28.8,
|
||||
"3": 24.2
|
||||
},
|
||||
{
|
||||
"week": 33,
|
||||
"1": 24.2,
|
||||
"2": 27.4,
|
||||
"3": 25.8
|
||||
},
|
||||
{
|
||||
"week": 34,
|
||||
"1": 23.5,
|
||||
"2": 26.5,
|
||||
"3": 26.4
|
||||
},
|
||||
{
|
||||
"week": 35,
|
||||
"1": 22.8,
|
||||
"2": 25.8,
|
||||
"3": 27.1
|
||||
},
|
||||
{
|
||||
"week": 36,
|
||||
"1": 21.4,
|
||||
"2": 24.2,
|
||||
"3": 27.8
|
||||
},
|
||||
{
|
||||
"week": 37,
|
||||
"1": 20.5,
|
||||
"2": 23.5,
|
||||
"3": 28.2
|
||||
},
|
||||
{
|
||||
"week": 38,
|
||||
"1": 19.8,
|
||||
"2": 22.8,
|
||||
"3": 28.9
|
||||
},
|
||||
{
|
||||
"week": 39,
|
||||
"1": 19.2,
|
||||
"2": 21.9,
|
||||
"3": 27.5
|
||||
},
|
||||
{
|
||||
"week": 40,
|
||||
"1": 18.8,
|
||||
"2": 21.2,
|
||||
"3": 26.4
|
||||
},
|
||||
{
|
||||
"week": 41,
|
||||
"1": 18.5,
|
||||
"2": 20.8,
|
||||
"3": 25.2
|
||||
},
|
||||
{
|
||||
"week": 42,
|
||||
"1": 19.1,
|
||||
"2": 20.5,
|
||||
"3": 24.1
|
||||
},
|
||||
{
|
||||
"week": 43,
|
||||
"1": 20.2,
|
||||
"2": 21.4,
|
||||
"3": 23.5
|
||||
},
|
||||
{
|
||||
"week": 44,
|
||||
"1": 21.5,
|
||||
"2": 22.8,
|
||||
"3": 22.1
|
||||
},
|
||||
{
|
||||
"week": 45,
|
||||
"1": 22.8,
|
||||
"2": 24.1,
|
||||
"3": 21.8
|
||||
},
|
||||
{
|
||||
"week": 46,
|
||||
"1": 23.4,
|
||||
"2": 25.2,
|
||||
"3": 20.9
|
||||
},
|
||||
{
|
||||
"week": 47,
|
||||
"1": 24.1,
|
||||
"2": 26.8,
|
||||
"3": 20.1
|
||||
},
|
||||
{
|
||||
"week": 48,
|
||||
"1": 25.8,
|
||||
"2": 27.5,
|
||||
"3": 19.5
|
||||
},
|
||||
{
|
||||
"week": 49,
|
||||
"1": 26.2,
|
||||
"2": 28.2,
|
||||
"3": 19.1
|
||||
},
|
||||
{
|
||||
"week": 50,
|
||||
"1": 26.8,
|
||||
"2": 28.8,
|
||||
"3": 18.8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
{
|
||||
"statistics_data": [
|
||||
{
|
||||
"label": "HPP Global",
|
||||
"value": 16200,
|
||||
"percent_last_month": 15.5
|
||||
},
|
||||
{
|
||||
"label": "Avg. Selling Price",
|
||||
"value": 28300,
|
||||
"percent_last_month": -50
|
||||
},
|
||||
{
|
||||
"label": "FCR",
|
||||
"value": 24.02,
|
||||
"percent_last_month": 15.5
|
||||
},
|
||||
{
|
||||
"label": "Mortality",
|
||||
"value": 5,
|
||||
"percent_last_month": -15.5
|
||||
}
|
||||
],
|
||||
"charts": {
|
||||
"kandang": {
|
||||
"series": [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "Kandang Dago",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"label": "Kandang Sulanjana",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"label": "Kandang Garut 2",
|
||||
"unit": "%"
|
||||
}
|
||||
],
|
||||
"dataset": [
|
||||
{
|
||||
"week": 1,
|
||||
"1": 21.2,
|
||||
"2": 19.5,
|
||||
"3": 20.1
|
||||
},
|
||||
{
|
||||
"week": 2,
|
||||
"1": 22.5,
|
||||
"2": 19.8,
|
||||
"3": 20.4
|
||||
},
|
||||
{
|
||||
"week": 3,
|
||||
"1": 23.1,
|
||||
"2": 20.2,
|
||||
"3": 21.0
|
||||
},
|
||||
{
|
||||
"week": 4,
|
||||
"1": 24.5,
|
||||
"2": 21.5,
|
||||
"3": 22.1
|
||||
},
|
||||
{
|
||||
"week": 5,
|
||||
"1": 25.8,
|
||||
"2": 22.4,
|
||||
"3": 23.5
|
||||
},
|
||||
{
|
||||
"week": 6,
|
||||
"1": 26.2,
|
||||
"2": 23.1,
|
||||
"3": 24.8
|
||||
},
|
||||
{
|
||||
"week": 7,
|
||||
"1": 27.5,
|
||||
"2": 24.5,
|
||||
"3": 26.2
|
||||
},
|
||||
{
|
||||
"week": 8,
|
||||
"1": 28.1,
|
||||
"2": 25.8,
|
||||
"3": 27.5
|
||||
},
|
||||
{
|
||||
"week": 9,
|
||||
"1": 28.8,
|
||||
"2": 26.2,
|
||||
"3": 28.4
|
||||
},
|
||||
{
|
||||
"week": 10,
|
||||
"1": 29.1,
|
||||
"2": 27.5,
|
||||
"3": 28.1
|
||||
},
|
||||
{
|
||||
"week": 11,
|
||||
"1": 28.5,
|
||||
"2": 28.1,
|
||||
"3": 27.4
|
||||
},
|
||||
{
|
||||
"week": 12,
|
||||
"1": 27.2,
|
||||
"2": 29.1,
|
||||
"3": 26.5
|
||||
},
|
||||
{
|
||||
"week": 13,
|
||||
"1": 26.1,
|
||||
"2": 28.5,
|
||||
"3": 25.8
|
||||
},
|
||||
{
|
||||
"week": 14,
|
||||
"1": 25.8,
|
||||
"2": 27.2,
|
||||
"3": 24.2
|
||||
},
|
||||
{
|
||||
"week": 15,
|
||||
"1": 24.5,
|
||||
"2": 26.1,
|
||||
"3": 23.1
|
||||
},
|
||||
{
|
||||
"week": 16,
|
||||
"1": 23.2,
|
||||
"2": 25.8,
|
||||
"3": 22.5
|
||||
},
|
||||
{
|
||||
"week": 17,
|
||||
"1": 22.8,
|
||||
"2": 24.5,
|
||||
"3": 21.9
|
||||
},
|
||||
{
|
||||
"week": 18,
|
||||
"1": 21.9,
|
||||
"2": 23.2,
|
||||
"3": 21.0
|
||||
},
|
||||
{
|
||||
"week": 19,
|
||||
"1": 21.2,
|
||||
"2": 22.8,
|
||||
"3": 20.5
|
||||
},
|
||||
{
|
||||
"week": 20,
|
||||
"1": 20.5,
|
||||
"2": 21.9,
|
||||
"3": 19.8
|
||||
},
|
||||
{
|
||||
"week": 21,
|
||||
"1": 19.8,
|
||||
"2": 21.2,
|
||||
"3": 19.2
|
||||
},
|
||||
{
|
||||
"week": 22,
|
||||
"1": 20.4,
|
||||
"2": 20.5,
|
||||
"3": 18.5
|
||||
},
|
||||
{
|
||||
"week": 23,
|
||||
"1": 21.0,
|
||||
"2": 19.8,
|
||||
"3": 18.1
|
||||
},
|
||||
{
|
||||
"week": 24,
|
||||
"1": 22.1,
|
||||
"2": 20.4,
|
||||
"3": 17.8
|
||||
},
|
||||
{
|
||||
"week": 25,
|
||||
"1": 23.5,
|
||||
"2": 21.0,
|
||||
"3": 18.5
|
||||
},
|
||||
{
|
||||
"week": 26,
|
||||
"1": 24.8,
|
||||
"2": 22.1,
|
||||
"3": 19.2
|
||||
},
|
||||
{
|
||||
"week": 27,
|
||||
"1": 26.2,
|
||||
"2": 23.5,
|
||||
"3": 20.1
|
||||
},
|
||||
{
|
||||
"week": 28,
|
||||
"1": 27.5,
|
||||
"2": 24.8,
|
||||
"3": 21.5
|
||||
},
|
||||
{
|
||||
"week": 29,
|
||||
"1": 28.4,
|
||||
"2": 26.2,
|
||||
"3": 22.8
|
||||
},
|
||||
{
|
||||
"week": 30,
|
||||
"1": 28.1,
|
||||
"2": 27.5,
|
||||
"3": 24.2
|
||||
},
|
||||
{
|
||||
"week": 31,
|
||||
"1": 27.4,
|
||||
"2": 28.4,
|
||||
"3": 25.8
|
||||
},
|
||||
{
|
||||
"week": 32,
|
||||
"1": 26.5,
|
||||
"2": 28.1,
|
||||
"3": 26.5
|
||||
},
|
||||
{
|
||||
"week": 33,
|
||||
"1": 25.8,
|
||||
"2": 27.4,
|
||||
"3": 27.2
|
||||
},
|
||||
{
|
||||
"week": 34,
|
||||
"1": 24.2,
|
||||
"2": 26.5,
|
||||
"3": 28.1
|
||||
},
|
||||
{
|
||||
"week": 35,
|
||||
"1": 23.1,
|
||||
"2": 25.8,
|
||||
"3": 28.5
|
||||
},
|
||||
{
|
||||
"week": 36,
|
||||
"1": 22.5,
|
||||
"2": 24.2,
|
||||
"3": 29.1
|
||||
},
|
||||
{
|
||||
"week": 37,
|
||||
"1": 21.9,
|
||||
"2": 23.1,
|
||||
"3": 28.8
|
||||
},
|
||||
{
|
||||
"week": 38,
|
||||
"1": 21.0,
|
||||
"2": 22.5,
|
||||
"3": 28.1
|
||||
},
|
||||
{
|
||||
"week": 39,
|
||||
"1": 20.5,
|
||||
"2": 21.9,
|
||||
"3": 27.4
|
||||
},
|
||||
{
|
||||
"week": 40,
|
||||
"1": 19.8,
|
||||
"2": 21.0,
|
||||
"3": 26.5
|
||||
},
|
||||
{
|
||||
"week": 41,
|
||||
"1": 19.2,
|
||||
"2": 20.5,
|
||||
"3": 25.8
|
||||
},
|
||||
{
|
||||
"week": 42,
|
||||
"1": 18.5,
|
||||
"2": 19.8,
|
||||
"3": 24.2
|
||||
},
|
||||
{
|
||||
"week": 43,
|
||||
"1": 18.1,
|
||||
"2": 19.2,
|
||||
"3": 23.1
|
||||
},
|
||||
{
|
||||
"week": 44,
|
||||
"1": 17.8,
|
||||
"2": 18.5,
|
||||
"3": 22.5
|
||||
},
|
||||
{
|
||||
"week": 45,
|
||||
"1": 18.5,
|
||||
"2": 18.1,
|
||||
"3": 21.9
|
||||
},
|
||||
{
|
||||
"week": 46,
|
||||
"1": 19.2,
|
||||
"2": 17.8,
|
||||
"3": 21.0
|
||||
},
|
||||
{
|
||||
"week": 47,
|
||||
"1": 20.1,
|
||||
"2": 18.5,
|
||||
"3": 20.5
|
||||
},
|
||||
{
|
||||
"week": 48,
|
||||
"1": 21.5,
|
||||
"2": 19.2,
|
||||
"3": 19.8
|
||||
},
|
||||
{
|
||||
"week": 49,
|
||||
"1": 22.8,
|
||||
"2": 20.1,
|
||||
"3": 19.2
|
||||
},
|
||||
{
|
||||
"week": 50,
|
||||
"1": 24.2,
|
||||
"2": 21.5,
|
||||
"3": 18.5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"statistics_data": [
|
||||
{
|
||||
"label": "HPP Global",
|
||||
"value": 16200,
|
||||
"percent_last_month": 15.5
|
||||
},
|
||||
{
|
||||
"label": "Avg. Selling Price",
|
||||
"value": 28300,
|
||||
"percent_last_month": -50
|
||||
}
|
||||
],
|
||||
"charts": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Dummy data for DashboardProduction
|
||||
* Generated from: dashboard.production.dummy.json
|
||||
*
|
||||
* This file is auto-generated. Do not edit manually.
|
||||
*/
|
||||
|
||||
import { Dashboard, DashboardMeta } from '../../types/api/dashboard/dashboard';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import dummyData from './dashboard.overview.dummy.json';
|
||||
import dummyData2 from './dashboard.comparasion.location.dummy.json';
|
||||
import dummyData3 from './dashboard.comparasion.flock.dummy.json';
|
||||
import dummyData4 from './dashboard.comparasion.kandang.dummy.json';
|
||||
import dummyData5 from './dashboard.default.json';
|
||||
/**
|
||||
* Get dummy DashboardProduction data
|
||||
* @returns Promise with BaseApiResponse containing DashboardProduction
|
||||
*/
|
||||
export async function getDummySingle(): Promise<BaseApiResponse<Dashboard>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Data retrieved successfully',
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 1,
|
||||
total_pages: 1,
|
||||
total_results: 1,
|
||||
filters: {
|
||||
analysis_mode: 'OVERVIEW',
|
||||
location_ids: [1],
|
||||
flock_ids: [1],
|
||||
kandang_ids: [1],
|
||||
},
|
||||
} as DashboardMeta,
|
||||
data: dummyData as unknown as Dashboard,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { Dashboard } from '@/types/api/dashboard/dashboard';
|
||||
import { getDummySingle } from '@/dummy/dashboard/dashboard.production.dummy';
|
||||
import { httpClientFetcher } from '@/services/http/client';
|
||||
|
||||
class DashboardService extends BaseApiService<Dashboard, unknown, unknown> {
|
||||
constructor(basePath: string) {
|
||||
@@ -12,19 +12,14 @@ class DashboardService extends BaseApiService<Dashboard, unknown, unknown> {
|
||||
* Fetch dashboard production data
|
||||
* @param endpoint - The endpoint URL with query parameters
|
||||
* @returns Promise with BaseApiResponse containing DashboardProduction
|
||||
*
|
||||
* Note: Currently using dummy data. When real API is ready,
|
||||
* uncomment the line below and remove getDummySingle() call:
|
||||
* return await this.customRequest<BaseApiResponse<Dashboard>>(endpoint);
|
||||
*/
|
||||
async getDashboardProductionFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<Dashboard>> {
|
||||
// For now, we're using dummy data regardless of the endpoint
|
||||
// The endpoint parameter is kept for future API integration
|
||||
console.log('Fetching dashboard data with endpoint:', endpoint);
|
||||
return await getDummySingle();
|
||||
): Promise<BaseApiResponse<Dashboard> | undefined> {
|
||||
return await httpClientFetcher<BaseApiResponse<Dashboard>>(
|
||||
`${endpoint ? endpoint : this.basePath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardApi = new DashboardService('/dashboard');
|
||||
export const DashboardApi = new DashboardService('/dashboards');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
|
||||
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||
|
||||
export class FinanceApiService extends BaseApiService<
|
||||
CustomerPaymentReport,
|
||||
@@ -36,10 +37,30 @@ export class FinanceApiService extends BaseApiService<
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getDebtSupplierReport(
|
||||
supplier_ids?: string,
|
||||
filter_by?: 'do_date',
|
||||
start_date?: string,
|
||||
end_date?: string,
|
||||
page?: number,
|
||||
limit?: number
|
||||
): Promise<BaseApiResponse<DebtSupplier[]> | undefined> {
|
||||
return await this.customRequest<BaseApiResponse<DebtSupplier[]>>(
|
||||
`debt-supplier`,
|
||||
{
|
||||
method: 'GET',
|
||||
params: {
|
||||
supplier_ids: supplier_ids,
|
||||
filter_by: filter_by,
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
page: page,
|
||||
limit: limit,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const FinanceApi = new FinanceApiService('reports');
|
||||
|
||||
// export const FinanceApi = new FinanceApiService(
|
||||
// 'http://localhost:4010/api/reports/finance'
|
||||
// );
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ export interface DashboardFilter {
|
||||
end_date: string;
|
||||
analysis_mode: 'OVERVIEW' | 'COMPARISON';
|
||||
location_ids: number[];
|
||||
comparison_type?: string | undefined;
|
||||
flock_ids: number[];
|
||||
kandang_ids: number[];
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
|
||||
export type DebtSupplier = BaseMetadata & {
|
||||
supplier: Supplier;
|
||||
rows: DebtRow[];
|
||||
total: DebtTotal;
|
||||
};
|
||||
|
||||
export type DebtRow = {
|
||||
pr_number: string;
|
||||
po_number: string;
|
||||
pr_date: string;
|
||||
po_date: string;
|
||||
aging: number;
|
||||
area: Area;
|
||||
warehouse: Warehouse;
|
||||
due_date: string;
|
||||
due_status: string;
|
||||
total_price: number;
|
||||
payment_price: number;
|
||||
debt_price: number;
|
||||
status: string;
|
||||
travel_number: string;
|
||||
};
|
||||
|
||||
export type DebtTotal = {
|
||||
aging: number;
|
||||
total_price: number;
|
||||
payment_price: number;
|
||||
debt_price: number;
|
||||
};
|
||||
Reference in New Issue
Block a user