Merge branch 'fix/data-refactor-and-ui-adjustment' into 'development'

[FIX/FE] Data Refactor and UI Adjustment (Closing, Biaya, Finance Report, Sales Report, Recording)

See merge request mbugroup/lti-web-client!170
This commit is contained in:
Rivaldi A N S
2026-01-14 02:30:03 +00:00
9 changed files with 333 additions and 150 deletions
+3 -1
View File
@@ -38,9 +38,11 @@ const ExpenseEditPage = () => {
!isLoadingExpense && !isLoadingExpense &&
isResponseSuccess(expense) && isResponseSuccess(expense) &&
expense.data.latest_approval.step_number !== 5 && expense.data.latest_approval.step_number !== 5 &&
expense.data.latest_approval.step_number !== 6 &&
(expense.data.latest_approval.step_number === 1 || (expense.data.latest_approval.step_number === 1 ||
expense.data.latest_approval.step_number === 2 || expense.data.latest_approval.step_number === 2 ||
expense.data.latest_approval.step_number === 3); expense.data.latest_approval.step_number === 3 ||
expense.data.latest_approval.step_number === 4);
if (!isLoadingExpense && !isExpenseCanBeEdited) { if (!isLoadingExpense && !isExpenseCanBeEdited) {
router.back(); router.back();
@@ -299,7 +299,7 @@ const ClosingFinanceTable = ({
}, },
}, },
{ {
header: 'Type', header: 'Jenis',
enableSorting: false, enableSorting: false,
accessorFn: (item) => formatTitleCase(item.type || '-'), accessorFn: (item) => formatTitleCase(item.type || '-'),
}, },
@@ -678,12 +678,13 @@ const RecordingTable = () => {
{ {
header: 'Nama Project', header: 'Nama Project',
cell: (props) => cell: (props) =>
`Project ${props.row.original.project_flock_kandang_id}`, props.row.original.project_flock?.flock_name || '-',
}, },
{ {
header: 'Kategori', header: 'Kategori',
cell: (props) => { cell: (props) => {
const category = props.row.original.project_flock_category; const category =
props.row.original.project_flock?.project_flock_category;
if (!category) return '-'; if (!category) return '-';
const color = category === 'LAYING' ? 'info' : 'warning'; const color = category === 'LAYING' ? 'info' : 'warning';
return ( return (
@@ -706,7 +707,8 @@ const RecordingTable = () => {
{ {
header: 'Populasi Awal', header: 'Populasi Awal',
cell: (props) => cell: (props) =>
props.row.original.total_chick_qty?.toLocaleString() || '-', props.row.original.project_flock?.total_chick_qty?.toLocaleString() ||
'-',
}, },
{ {
header: 'Status Approval', header: 'Status Approval',
@@ -117,8 +117,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
// ===== PAYLOAD CREATION HELPERS ===== // ===== PAYLOAD CREATION HELPERS =====
const createGrowingPayload = useCallback( const createGrowingPayload = useCallback(
(values: RecordingGrowingFormValues) => { (values: RecordingGrowingFormValues) => {
const today = new Date().toISOString().split('T')[0];
return { return {
project_flock_kandang_id: values.project_flock_kandang_id, project_flock_kandang_id: values.project_flock_kandang_id,
record_date: today,
stocks: (values.stocks ?? []).map((stock) => ({ stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
qty: Number(stock.qty) || 0, qty: Number(stock.qty) || 0,
@@ -134,8 +136,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const createLayingPayload = useCallback( const createLayingPayload = useCallback(
(values: RecordingLayingFormValues) => { (values: RecordingLayingFormValues) => {
const today = new Date().toISOString().split('T')[0];
return { return {
project_flock_kandang_id: values.project_flock_kandang_id, project_flock_kandang_id: values.project_flock_kandang_id,
record_date: today,
stocks: (values.stocks ?? []).map((stock) => ({ stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id, product_warehouse_id: stock.product_warehouse_id,
qty: Number(stock.qty) || 0, qty: Number(stock.qty) || 0,
@@ -252,9 +256,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
: undefined; : undefined;
const projectFlockKandangDetailUrl = useMemo(() => { const projectFlockKandangDetailUrl = useMemo(() => {
if (type === 'add' || !initialValues?.project_flock_kandang_id) return null; if (
return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock_kandang_id}`; type === 'add' ||
}, [type, initialValues?.project_flock_kandang_id]); !initialValues?.project_flock?.project_flock_kandang_id
)
return null;
return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock.project_flock_kandang_id}`;
}, [type, initialValues?.project_flock?.project_flock_kandang_id]);
const { data: projectFlockKandangDetailData } = useSWR( const { data: projectFlockKandangDetailData } = useSWR(
projectFlockKandangDetailUrl, projectFlockKandangDetailUrl,
@@ -404,12 +412,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, [approvedProjectFlockKandangsData]); }, [approvedProjectFlockKandangsData]);
const isLayingCategory = const isLayingCategory =
initialValues?.project_flock_category === 'LAYING' || initialValues?.project_flock?.project_flock_category === 'LAYING' ||
projectFlockKandangLookup?.project_flock?.category === 'LAYING' || projectFlockKandangLookup?.project_flock?.category === 'LAYING' ||
projectFlockKandangDetail?.project_flock?.category === 'LAYING'; projectFlockKandangDetail?.project_flock?.category === 'LAYING';
const isGrowingCategory = const isGrowingCategory =
initialValues?.project_flock_category === 'GROWING' || initialValues?.project_flock?.project_flock_category === 'GROWING' ||
projectFlockKandangLookup?.project_flock?.category === 'GROWING' || projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
projectFlockKandangDetail?.project_flock?.category === 'GROWING'; projectFlockKandangDetail?.project_flock?.category === 'GROWING';
@@ -555,7 +563,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
todayRecordings.forEach((recording) => { todayRecordings.forEach((recording) => {
const recordingDate = recording.record_datetime?.split('T')[0]; const recordingDate = recording.record_datetime?.split('T')[0];
if (recordingDate === today) { if (recordingDate === today) {
recordedIds.add(recording.project_flock_kandang_id); recordedIds.add(recording.project_flock.project_flock_kandang_id);
} }
}); });
@@ -1005,7 +1013,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const hasSameDayRecording = isResponseSuccess(existingRecordings) const hasSameDayRecording = isResponseSuccess(existingRecordings)
? existingRecordings.data?.some( ? existingRecordings.data?.some(
(recording: Recording) => (recording: Recording) =>
recording.project_flock_kandang_id === recording.project_flock.project_flock_kandang_id ===
projectFlockKandangId && projectFlockKandangId &&
recording.day === nextDayRecording.next_day recording.day === nextDayRecording.next_day
) )
@@ -1543,13 +1551,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<Badge <Badge
variant='soft' variant='soft'
color={ color={
initialValues.project_flock_category === 'LAYING' initialValues.project_flock
?.project_flock_category === 'LAYING'
? 'info' ? 'info'
: 'warning' : 'warning'
} }
size='sm' size='sm'
> >
{initialValues.project_flock_category} {initialValues.project_flock?.project_flock_category}
</Badge> </Badge>
</p> </p>
</div> </div>
@@ -1579,7 +1588,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{type === 'detail' && initialValues && ( {type === 'detail' && initialValues && (
<div <div
className={`grid gap-6 mb-6 grid-cols-1 ${ className={`grid gap-6 mb-6 grid-cols-1 ${
initialValues.project_flock_category === 'LAYING' initialValues.project_flock?.project_flock_category === 'LAYING'
? 'xl:grid-cols-3' ? 'xl:grid-cols-3'
: 'xl:grid-cols-2' : 'xl:grid-cols-2'
}`} }`}
@@ -1614,8 +1623,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.fcr_std && initialValues.fcr_std > 0 {initialValues.project_flock?.fcr?.fcr_std &&
? formatNumber(initialValues.fcr_std) initialValues.project_flock?.fcr?.fcr_std > 0
? formatNumber(
initialValues.project_flock?.fcr?.fcr_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -1630,9 +1642,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.feed_intake_std && {initialValues.project_flock?.production_standart
initialValues.feed_intake_std > 0 ?.feed_intake_std &&
? formatNumber(initialValues.feed_intake_std) initialValues.project_flock?.production_standart
?.feed_intake_std > 0
? formatNumber(
initialValues.project_flock?.production_standart
?.feed_intake_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -1650,59 +1667,39 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
<div className='p-4'> <div className='p-4'>
<table className='w-full text-sm'> <table className='w-full text-sm'>
<thead>
<tr className='border-b border-gray-200'>
<th
colSpan={2}
className='text-center py-2 font-semibold text-gray-600'
>
DEPLESI KUMULATIF
</th>
</tr>
<tr className='border-b border-gray-200'>
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
Total
</th>
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
(%)
</th>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td className='text-center py-3 border-r border-gray-100'> <td className='py-2 font-medium'>Deplesi Kumulatif</td>
<td className='text-right py-2'>
<span className='font-semibold'> <span className='font-semibold'>
{initialValues.total_depletion_qty && {initialValues.cum_depletion_rate &&
initialValues.total_depletion_qty > 0 initialValues.cum_depletion_rate > 0
? formatNumber(initialValues.total_depletion_qty) ? `${initialValues.cum_depletion_rate.toFixed(2)}%`
: '-'} : '-'}
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'>
{initialValues.cum_depletion_rate &&
initialValues.cum_depletion_rate > 0
? initialValues.cum_depletion_rate.toFixed(2)
: '-'}
</td>
</tr> </tr>
<tr> <tr>
<td <td className='py-2 font-medium'>Total Depletion</td>
colSpan={2} <td className='text-right py-2 font-semibold'>
className='text-center py-3 border-r border-gray-200 text-gray-600' {initialValues.total_depletion_qty &&
> initialValues.total_depletion_qty > 0
Total Ayam ? formatNumber(initialValues.total_depletion_qty)
</td>
</tr>
<tr>
<td
colSpan={2}
className='text-center py-3 font-semibold'
>
{initialValues.total_chick_qty &&
initialValues.total_chick_qty > 0
? formatNumber(initialValues.total_chick_qty)
: '-'} : '-'}
</td> </td>
<td></td>
</tr>
<tr className='border-t border-gray-200'>
<td className='py-2 text-gray-600'>Total Ayam</td>
<td className='text-right py-2 font-semibold'>
{initialValues.project_flock?.total_chick_qty &&
initialValues.project_flock?.total_chick_qty > 0
? formatNumber(
initialValues.project_flock?.total_chick_qty
)
: '-'}
</td>
<td></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -1712,7 +1709,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Egg Production Section - Only for LAYING category */} {/* Egg Production Section - Only for LAYING category */}
{type === 'detail' && {type === 'detail' &&
initialValues && initialValues &&
initialValues.project_flock_category === 'LAYING' && ( initialValues.project_flock?.project_flock_category ===
'LAYING' && (
<div className='border border-gray-200 rounded-lg bg-white'> <div className='border border-gray-200 rounded-lg bg-white'>
<div className='px-4 py-3 border-b border-gray-200'> <div className='px-4 py-3 border-b border-gray-200'>
<span className='card-title font-bold text-xl'> <span className='card-title font-bold text-xl'>
@@ -1744,9 +1742,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.egg_mass_std && {initialValues.project_flock?.production_standart
initialValues.egg_mass_std > 0 ?.egg_mass_std &&
? formatNumber(initialValues.egg_mass_std) initialValues.project_flock?.production_standart
?.egg_mass_std > 0
? formatNumber(
initialValues.project_flock
?.production_standart?.egg_mass_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -1763,9 +1766,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.egg_weight_std && {initialValues.project_flock?.production_standart
initialValues.egg_weight_std > 0 ?.egg_weight_std &&
? formatNumber(initialValues.egg_weight_std) initialValues.project_flock?.production_standart
?.egg_weight_std > 0
? formatNumber(
initialValues.project_flock
?.production_standart?.egg_weight_std
)
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -1780,9 +1788,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.hen_day_std !== undefined && {initialValues.project_flock?.production_standart
initialValues.hen_day_std > 0 ?.hen_day_std !== undefined &&
? `${initialValues.hen_day_std}%` initialValues.project_flock?.production_standart
?.hen_day_std > 0
? `${initialValues.project_flock?.production_standart?.hen_day_std}%`
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -1797,9 +1807,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</span> </span>
</td> </td>
<td className='text-center py-3 text-gray-600'> <td className='text-center py-3 text-gray-600'>
{initialValues.hen_house_std !== undefined && {initialValues.project_flock?.production_standart
initialValues.hen_house_std > 0 ?.hen_house_std !== undefined &&
? `${initialValues.hen_house_std}%` initialValues.project_flock?.production_standart
?.hen_house_std > 0
? `${initialValues.project_flock?.production_standart?.hen_house_std}%`
: '-'} : '-'}
</td> </td>
</tr> </tr>
@@ -33,7 +33,7 @@ const MarketingReportContent = () => {
const [activeTab, setActiveTab] = useState<string>('daily'); const [activeTab, setActiveTab] = useState<string>('daily');
return ( return (
<section className='w-full max-w-7xl pb-16'> <section className='w-full max-w-full pb-16'>
<Tabs <Tabs
activeTabId={activeTab} activeTabId={activeTab}
onTabChange={setActiveTab} onTabChange={setActiveTab}
@@ -136,41 +136,132 @@ const pdfStyles = StyleSheet.create({
backgroundColor: '#F0F0F0', backgroundColor: '#F0F0F0',
fontWeight: 'bold', fontWeight: 'bold',
}, },
badge: {
backgroundColor: '#1f74bf',
color: '#FFFFFF',
padding: 2,
borderRadius: 2,
fontSize: 7,
fontWeight: 'bold',
alignSelf: 'center',
marginRight: 4,
},
badgeLunas: {
backgroundColor: '#1f74bf',
color: '#FFFFFF',
},
badgeBelumLunas: {
backgroundColor: '#F97316',
color: '#FFFFFF',
},
textError: {
color: '#DC2626',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 8,
},
}); });
interface CustomerPaymentExportPDFParams { interface CustomerPaymentExportPDFParams {
data: CustomerPaymentReport[]; data: CustomerPaymentReport[];
params?: {
customer_name?: string;
sales?: string;
start_date?: string;
end_date?: string;
filter_by?: string;
};
} }
const getParameterText = (
params?: CustomerPaymentExportPDFParams['params']
) => {
const paramsText = [];
if (params?.customer_name) {
paramsText.push(`Customer: ${params.customer_name}`);
} else {
paramsText.push('Semua Customer');
}
if (params?.sales) {
paramsText.push(`Sales: ${params.sales}`);
}
if (params?.start_date && params?.end_date) {
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
const endDate = formatDate(params.end_date, 'DD MMM YYYY');
paramsText.push(`Periode: ${startDate} - ${endDate}`);
} else if (params?.start_date) {
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
paramsText.push(`Tanggal: ${startDate}`);
}
const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm');
paramsText.push(`Dicetak: ${currentDate}`);
return paramsText;
};
const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
return ( return (
<Document> <Document>
{params.data.map((customerReport, customerIndex) => ( {params.data.map((customerReport, customerIndex) => (
<Page <Page
key={customerIndex} key={customerIndex}
size='A4' size='A3'
orientation='landscape' orientation='landscape'
style={pdfStyles.page} style={pdfStyles.page}
> >
{/* Title and Customer Info */} {/* Title and Parameters */}
<View style={pdfStyles.titleSection}> <View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}> <Text style={pdfStyles.mainTitle}>
Laporan &gt; Kontrol Pembayaran Customer Laporan &gt; Kontrol Pembayaran Customer
</Text> </Text>
<View style={pdfStyles.parameterContainer}>
<View style={pdfStyles.parameterBadge}>
<Text>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>Filter Tanggal: Tanggal DO</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>
Customer: {params.params?.customer_name || 'Semua Customer'}
</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text>
</View>
</View>
<Text style={pdfStyles.supplierTitle}> <Text style={pdfStyles.supplierTitle}>
{customerReport.customer.name} {customerReport.customer.name}
</Text> </Text>
<Text style={pdfStyles.supplierInfo}> <Text style={pdfStyles.supplierInfo}>
{customerReport.customer.address || ''} Alamat: {customerReport.customer.address || '-'}
</Text> </Text>
{customerReport.summary && (
<Text style={pdfStyles.supplierInfo}>
Total Saldo Piutang:{' '}
{formatCurrency(
customerReport.summary.total_accounts_receivable
)}
</Text>
)}
</View> </View>
{/* Table */} {/* Table */}
@@ -181,10 +272,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>No</Text> <Text>No</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>Tgl DO/Bayar</Text> <Text>Tanggal DO</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>Tgl Realisasi</Text> <Text>Tanggal Realisasi</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
<Text>Aging</Text> <Text>Aging</Text>
@@ -193,16 +284,16 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>Referensi</Text> <Text>Referensi</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>No. Polisi</Text> <Text>No Polisi</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
<Text>Qty</Text> <Text>Qty</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>Berat (Kg)</Text> <Text>Berat</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
<Text>AVG</Text> <Text>Rata-Rata</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Harga Awal</Text> <Text>Harga Awal</Text>
@@ -214,7 +305,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>Harga Akhir</Text> <Text>Harga Akhir</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
<Text>PPN (%)</Text> <Text>Pajak</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Total</Text> <Text>Total</Text>
@@ -223,10 +314,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>Pembayaran</Text> <Text>Pembayaran</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Saldo Piutang</Text> <Text>Saldo</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
<Text>Ket</Text> <Text>Keterangan</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Pengambilan</Text> <Text>Pengambilan</Text>
@@ -301,10 +392,29 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>{formatCurrency(item.payment)}</Text> <Text>{formatCurrency(item.payment)}</Text>
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.accounts_receivable)}</Text> <Text style={pdfStyles.textError}>
{formatCurrency(item.accounts_receivable)}
</Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}> <View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text>{item.notes || '-'}</Text> {item.notes ? (
<Text>{item.notes}</Text>
) : (
<View
style={[
pdfStyles.badge,
item.accounts_receivable === 0
? pdfStyles.badgeLunas
: pdfStyles.badgeBelumLunas,
]}
>
<Text>
{item.accounts_receivable === 0
? 'Lunas'
: 'Belum Lunas'}
</Text>
</View>
)}
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.pickup_info || '-'}</Text> <Text>{item.pickup_info || '-'}</Text>
@@ -378,7 +488,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
</Text> </Text>
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text> <Text style={pdfStyles.textError}>
{formatCurrency( {formatCurrency(
customerReport.summary.total_accounts_receivable customerReport.summary.total_accounts_receivable
)} )}
@@ -2,6 +2,7 @@ import { useState, useMemo, useCallback } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Badge from '@/components/Badge';
import SelectInput, { import SelectInput, {
useSelect, useSelect,
OptionType, OptionType,
@@ -46,7 +47,6 @@ const CustomerPaymentTab = () => {
const [filterSales, setFilterSales] = useState<OptionType[]>([]); const [filterSales, setFilterSales] = useState<OptionType[]>([]);
const [filterStartDate, setFilterStartDate] = useState(''); const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState('');
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
const filterModal = useModal(); const filterModal = useModal();
@@ -68,6 +68,38 @@ const CustomerPaymentTab = () => {
[] []
); );
const getPaymentStatusColor = (notes: string) => {
const normalizedValue = notes.toLowerCase();
if (normalizedValue === 'lunas') {
return 'bg-info/10 text-info border-info';
}
if (normalizedValue.includes('belum')) {
return 'bg-warning/10 text-warning border-warning';
}
return 'bg-gray-100 text-gray-600 border-gray-300';
};
const getPaymentStatusIndicatorColor = (notes: string) => {
const normalizedValue = notes.toLowerCase();
if (normalizedValue === 'lunas') {
return 'bg-info';
}
if (normalizedValue.includes('belum')) {
return 'bg-warning';
}
return 'bg-gray-400';
};
const getPaymentStatusText = (notes: string) => {
return notes;
};
// ===== FILTER HANDLERS ===== // ===== FILTER HANDLERS =====
const handleResetFilters = useCallback(() => { const handleResetFilters = useCallback(() => {
setIsSubmitted(false); setIsSubmitted(false);
@@ -75,27 +107,13 @@ const CustomerPaymentTab = () => {
setFilterSales([]); setFilterSales([]);
setFilterStartDate(''); setFilterStartDate('');
setFilterEndDate(''); setFilterEndDate('');
setFilterErrors({});
}, []); }, []);
const handleApplyFilters = useCallback(() => { const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {}; setIsSubmitted(true);
setCurrentPage(1);
if (!filterStartDate) { filterModal.closeModal();
errors.start_date = 'Tanggal mulai wajib diisi'; }, [filterModal]);
}
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 ===== // ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR( const { data: customerPayment, isLoading } = useSWR(
@@ -218,7 +236,22 @@ const CustomerPaymentTab = () => {
return; return;
} }
await generateCustomerPaymentPDF({ data: allDataForExport }); await generateCustomerPaymentPDF({
data: allDataForExport,
params: {
customer_name:
filterCustomer.length > 0
? filterCustomer.map((c) => c.label).join(', ')
: undefined,
sales:
filterSales.length > 0
? filterSales.map((s) => s.label).join(', ')
: undefined,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
filter_by: 'do_date',
},
});
toast.success('PDF berhasil dibuat dan diunduh.'); toast.success('PDF berhasil dibuat dan diunduh.');
} catch { } catch {
toast.error('Gagal membuat PDF. Silakan coba lagi.'); toast.error('Gagal membuat PDF. Silakan coba lagi.');
@@ -435,7 +468,9 @@ const CustomerPaymentTab = () => {
accessorKey: 'accounts_receivable', accessorKey: 'accounts_receivable',
cell: (props) => { cell: (props) => {
const value = props.row.original.accounts_receivable; const value = props.row.original.accounts_receivable;
return <div className='text-right'>{formatCurrency(value)}</div>; return (
<div className='text-right text-error'>{formatCurrency(value)}</div>
);
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
@@ -449,7 +484,23 @@ const CustomerPaymentTab = () => {
accessorKey: 'notes', accessorKey: 'notes',
cell: (props) => { cell: (props) => {
const value = props.row.original.notes; const value = props.row.original.notes;
return value || '-';
if (!value) {
return '-';
}
return (
<Badge
statusIndicator={true}
variant='soft'
className={{
badge: `rounded-xl justify-start border border-gray-200 ${getPaymentStatusColor(value)}`,
status: getPaymentStatusIndicatorColor(value),
}}
>
{getPaymentStatusText(value)}
</Badge>
);
}, },
}, },
{ {
@@ -538,15 +589,9 @@ const CustomerPaymentTab = () => {
value={filterStartDate} value={filterStartDate}
onChange={(e) => { onChange={(e) => {
setFilterStartDate(e.target.value); setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}} }}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div> </div>
<div> <div>
@@ -556,15 +601,9 @@ const CustomerPaymentTab = () => {
value={filterEndDate} value={filterEndDate}
onChange={(e) => { onChange={(e) => {
setFilterEndDate(e.target.value); setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}} }}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div> </div>
</div> </div>
@@ -659,14 +698,13 @@ const CustomerPaymentTab = () => {
total_accounts_receivable: 0, total_accounts_receivable: 0,
}; };
const totalAccountsReceivable = summary.total_accounts_receivable;
const tableColumns = getTableColumns(summary); const tableColumns = getTableColumns(summary);
return ( return (
<Card <Card
key={customerReport.customer.id} key={customerReport.customer.id}
title={customerReport.customer.name} title={customerReport.customer.name}
subtitle={`${customerReport.customer.address || ''}\nSaldo Piutang: ${formatCurrency(totalAccountsReceivable)}`} subtitle={`${customerReport.customer.address || ''}`}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
variant='bordered' variant='bordered'
collapsible={true} collapsible={true}
+1 -3
View File
@@ -44,9 +44,7 @@ export class MarketingSaleReportService extends BaseApiService<
} }
} }
export const SaleReportApi = new MarketingSaleReportService( export const SaleReportApi = new MarketingSaleReportService('reports');
'reports/marketings'
);
// export const SaleReportApi = new MarketingSaleReportService( // export const SaleReportApi = new MarketingSaleReportService(
// 'http://localhost:4010/api/reports/marketings' // 'http://localhost:4010/api/reports/marketings'
+33 -12
View File
@@ -1,34 +1,52 @@
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general'; import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Warehouse } from '@/types/api/master-data/warehouse';
export type ProductionStandard = {
id: number;
week: number;
name: string;
hen_day_std: number;
hen_house_std: number;
feed_intake_std: number;
max_depletion_std: number;
egg_mass_std: number;
egg_weight_std: number;
};
export type FCR = {
id: number;
name: string;
fcr_std: number;
};
export type ProjectFlock = {
project_flock_kandang_id: number;
flock_name: string;
project_flock_category: 'GROWING' | 'LAYING';
period: number;
production_standart: ProductionStandard;
fcr: FCR;
total_chick_qty: number;
};
export type ProductionMetrics = { export type ProductionMetrics = {
total_depletion_qty: number; total_depletion_qty: number;
cum_depletion_rate: number; cum_depletion_rate: number;
cum_intake: number; cum_intake: number;
fcr_value: number; fcr_value: number;
fcr_std?: number;
total_chick_qty: number;
hen_day?: number; hen_day?: number;
hen_house?: number; hen_house?: number;
feed_intake?: number; feed_intake?: number;
feed_intake_std?: number;
egg_mass?: number; egg_mass?: number;
egg_weight?: number; egg_weight?: number;
hen_day_std?: number;
hen_house_std?: number;
egg_mass_std?: number;
egg_weight_std?: number;
daily_gain?: number;
avg_daily_gain?: number;
cum_depletion?: number;
}; };
export type BaseRecording = { export type BaseRecording = {
id: number; id: number;
project_flock_kandang_id: number; project_flock: ProjectFlock;
record_datetime: string; record_datetime: string;
day: number; day: number;
project_flock_category?: 'GROWING' | 'LAYING';
} & ProductionMetrics; } & ProductionMetrics;
export type RecordingDepletion = { export type RecordingDepletion = {
@@ -68,6 +86,8 @@ export type Recording = BaseMetadata &
BaseRecording & { BaseRecording & {
approval?: BaseApproval; approval?: BaseApproval;
created_user: User; created_user: User;
warehouse?: Warehouse;
product_category?: 'GROWING' | 'LAYING';
depletions?: RecordingDepletion[]; depletions?: RecordingDepletion[];
stocks?: RecordingStock[]; stocks?: RecordingStock[];
eggs?: RecordingEgg[]; eggs?: RecordingEgg[];
@@ -81,6 +101,7 @@ export type NextDayRecording = {
export type CreateGrowingRecordingPayload = { export type CreateGrowingRecordingPayload = {
project_flock_kandang_id: number; project_flock_kandang_id: number;
record_date: string;
stocks?: { stocks?: {
product_warehouse_id: number; product_warehouse_id: number;
qty: number; qty: number;