mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 584b495e4b | |||
| c8e76a8558 | |||
| a468a5948b | |||
| e05e1a4121 | |||
| e3b86e3033 | |||
| d467c56ea6 | |||
| 784d9f26ab | |||
| 978ef764ea | |||
| 928136ff18 | |||
| 79567e4f1b | |||
| 633deece21 | |||
| 46483af4c2 | |||
| c2653e5068 | |||
| 8569bda7d6 | |||
| 7da63dc542 | |||
| aeceef4361 | |||
| ff6955be54 | |||
| eccab314b3 |
@@ -66,7 +66,7 @@ const ExpenseRealizationForm = ({
|
||||
toast.success(createExpenseRes?.message as string);
|
||||
router.push('/expense');
|
||||
},
|
||||
[router]
|
||||
[router, initialValues?.id]
|
||||
);
|
||||
|
||||
const updateExpenseHandler = useCallback(
|
||||
|
||||
@@ -178,12 +178,14 @@ const ExpenseRequestForm = ({
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocationOptions,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
setInputValue: setVendorInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingVendorOptions,
|
||||
loadMore: loadMoreVendorOptions,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||
|
||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -414,6 +416,7 @@ const ExpenseRequestForm = ({
|
||||
errorMessage={formik.errors.location_id as string}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||
onMenuScrollToBottom={loadMoreLocationOptions}
|
||||
/>
|
||||
|
||||
<DateInput
|
||||
@@ -457,6 +460,7 @@ const ExpenseRequestForm = ({
|
||||
}
|
||||
errorMessage={formik.errors.supplier_id as string}
|
||||
className={{ wrapper: 'col-span-12' }}
|
||||
onMenuScrollToBottom={loadMoreVendorOptions}
|
||||
/>
|
||||
|
||||
<RequirePermission permissions='lti.expense.document'>
|
||||
|
||||
@@ -1,212 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Document,
|
||||
Image,
|
||||
Link,
|
||||
Page,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from '@react-pdf/renderer';
|
||||
import React from 'react';
|
||||
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
|
||||
|
||||
import { Expense } from '@/types/api/expense';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
|
||||
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||
|
||||
interface ExpensePDFProps {
|
||||
expense?: Expense;
|
||||
}
|
||||
|
||||
const ExpensePDFStyle = StyleSheet.create({
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingTop: 24,
|
||||
paddingBottom: 64,
|
||||
paddingHorizontal: 32,
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
|
||||
companyInfoHeader: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyLogo: {
|
||||
width: 64,
|
||||
height: 'auto',
|
||||
},
|
||||
companyInfoHeaderDate: {
|
||||
paddingTop: 8,
|
||||
fontSize: 12,
|
||||
},
|
||||
companyName: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
companyAddress: {
|
||||
fontSize: 8,
|
||||
maxWidth: 400,
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
title: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
lineHeight: '150%',
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Times-Roman',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
footer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 32,
|
||||
|
||||
position: 'absolute',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 8,
|
||||
},
|
||||
infoTableSection: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
infoTableTitle: {
|
||||
fontSize: 10,
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: 'grey',
|
||||
},
|
||||
|
||||
// wrapper
|
||||
generalInfoTable: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
borderBottomWidth: 0,
|
||||
fontSize: 12,
|
||||
},
|
||||
|
||||
generalInfoTableRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
},
|
||||
|
||||
// columns
|
||||
generalInfoTableColLabel: {
|
||||
width: '30%',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
generalInfoTableColSeparator: {
|
||||
width: '3%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 6,
|
||||
},
|
||||
generalInfoTableColValue: {
|
||||
width: '67%',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
|
||||
generalInfoTableLabelText: {
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 6,
|
||||
color: '#333',
|
||||
},
|
||||
generalInfoTableValueText: {},
|
||||
|
||||
// expense detail table
|
||||
expenseDetailContainer: {
|
||||
width: '100%',
|
||||
marginTop: 12,
|
||||
tableSection: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
expenseDetailTitle: {
|
||||
fontSize: 14,
|
||||
lineHeight: '150%',
|
||||
fontFamily: 'Times-Roman',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
kandangExpenseContainer: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
kandangExpenseTitle: {
|
||||
fontSize: 14,
|
||||
lineHeight: '150%',
|
||||
fontFamily: 'Times-Roman',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
kandangExpenseTable: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
borderBottomWidth: 0,
|
||||
fontSize: 12,
|
||||
},
|
||||
kandangExpenseTableRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
},
|
||||
kandangExpenseTableColLabel: {
|
||||
width: '20%',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
kandangExpenseTableColLabelBorderRight: {
|
||||
borderRight: '1px solid #000000',
|
||||
},
|
||||
kandangExpenseTableColNonstock: {
|
||||
width: '20%',
|
||||
},
|
||||
kandangExpenseTableColNote: {
|
||||
width: '40%',
|
||||
},
|
||||
kandangExpenseHeaderLabelText: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
kandangExpenseLabelText: {
|
||||
tableTitle: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 6,
|
||||
color: '#333',
|
||||
},
|
||||
kandangExpenseTableFooterColTotalExpenseCaption: {
|
||||
width: '40%',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
textAlign: 'right',
|
||||
},
|
||||
kandangExpenseTableFooterColTotalExpenseValue: {
|
||||
width: '60%',
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
|
||||
// utils
|
||||
doubleDivider: {
|
||||
width: '100%',
|
||||
height: 6,
|
||||
borderTop: '2px solid black',
|
||||
borderBottom: '2px solid black',
|
||||
emptyText: {
|
||||
fontSize: 8,
|
||||
color: '#666',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
|
||||
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
type ExpenseKandang = Expense['kandangs'][number];
|
||||
type PengajuanItem = NonNullable<ExpenseKandang['pengajuans']>[number];
|
||||
type RealisasiItem = NonNullable<ExpenseKandang['realisasi']>[number];
|
||||
|
||||
const valueText = (v: unknown) => {
|
||||
if (v === null || v === undefined) return '-';
|
||||
if (typeof v === 'number') return formatNumber(v);
|
||||
return String(v);
|
||||
};
|
||||
|
||||
const getPengajuanColumns = (): PdfColumn<PengajuanItem>[] => [
|
||||
{
|
||||
key: 'no',
|
||||
header: 'No',
|
||||
flex: 0.5,
|
||||
align: 'center',
|
||||
cell: ({ index }) => index + 1,
|
||||
},
|
||||
{
|
||||
key: 'nonstock',
|
||||
header: 'Nonstock',
|
||||
flex: 1.5,
|
||||
cell: ({ row }) => row.nonstock.name,
|
||||
},
|
||||
{
|
||||
key: 'qty',
|
||||
header: 'Kuantitas',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
cell: ({ row }) => valueText(row.qty),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: 'Harga Satuan',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatCurrency(row.price),
|
||||
},
|
||||
{
|
||||
key: 'notes',
|
||||
header: 'Catatan',
|
||||
flex: 1.5,
|
||||
cell: ({ row }) => row.notes || '-',
|
||||
},
|
||||
];
|
||||
|
||||
const getRealisasiColumns = (): PdfColumn<RealisasiItem>[] => [
|
||||
{
|
||||
key: 'no',
|
||||
header: 'No',
|
||||
flex: 0.5,
|
||||
align: 'center',
|
||||
cell: ({ index }) => index + 1,
|
||||
},
|
||||
{
|
||||
key: 'nonstock',
|
||||
header: 'Nonstock',
|
||||
flex: 1.5,
|
||||
cell: ({ row }) => row.nonstock.name,
|
||||
},
|
||||
{
|
||||
key: 'qty',
|
||||
header: 'Kuantitas',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
cell: ({ row }) => valueText(row.qty),
|
||||
},
|
||||
{
|
||||
key: 'price',
|
||||
header: 'Harga Satuan',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatCurrency(row.price),
|
||||
},
|
||||
{
|
||||
key: 'notes',
|
||||
header: 'Catatan',
|
||||
flex: 1.5,
|
||||
cell: ({ row }) => row.notes || '-',
|
||||
},
|
||||
];
|
||||
|
||||
const getInfoTableRows = (expense?: Expense) => {
|
||||
const isLatestApprovalRejected =
|
||||
expense?.latest_approval?.action === 'REJECTED';
|
||||
const isExpenseRealized =
|
||||
expense?.latest_approval?.step_number &&
|
||||
expense?.latest_approval.step_number >= 5;
|
||||
|
||||
const realizationStatus = isExpenseRealized
|
||||
? 'Sudah Realisasi'
|
||||
: 'Belum Realisasi';
|
||||
|
||||
const rows = [
|
||||
{ label: 'Nomor PO', value: expense?.po_number },
|
||||
{ label: 'Nomor Referensi', value: expense?.reference_number },
|
||||
return [
|
||||
{ label: 'Nomor PO', value: expense?.po_number || '-' },
|
||||
{ label: 'Nomor Referensi', value: expense?.reference_number || '-' },
|
||||
{
|
||||
label: 'Kategori',
|
||||
value:
|
||||
@@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
? 'Biaya Operasional'
|
||||
: expense?.category === 'NON-BOP'
|
||||
? 'Non Biaya Operasional'
|
||||
: '',
|
||||
: '-',
|
||||
},
|
||||
{ label: 'Lokasi', value: expense?.location.name },
|
||||
{ label: 'Lokasi', value: expense?.location?.name || '-' },
|
||||
{
|
||||
label: 'Kandang',
|
||||
value:
|
||||
@@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
.join(', ')
|
||||
: '-',
|
||||
},
|
||||
{ label: 'Vendor', value: expense?.supplier.name },
|
||||
{ label: 'Vendor', value: expense?.supplier?.name || '-' },
|
||||
{
|
||||
label: 'Tanggal Transaksi',
|
||||
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
|
||||
@@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
? formatDate(expense?.realization_date, 'DD MMMM YYYY')
|
||||
: '-',
|
||||
},
|
||||
{ label: 'Nama Pengaju', value: expense?.created_user.name },
|
||||
{ label: 'Nama Pengaju', value: expense?.created_user?.name || '-' },
|
||||
{
|
||||
label: 'Nominal Biaya',
|
||||
value: formatCurrency(
|
||||
expense?.latest_approval.step_number === 5 ||
|
||||
expense?.latest_approval.step_number === 6
|
||||
expense?.latest_approval?.step_number === 5 ||
|
||||
expense?.latest_approval?.step_number === 6
|
||||
? (expense?.total_realisasi ?? 0)
|
||||
: (expense?.total_pengajuan ?? 0)
|
||||
),
|
||||
@@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
label: 'Status Biaya',
|
||||
value: isLatestApprovalRejected
|
||||
? 'Ditolak'
|
||||
: expense?.latest_approval?.step_name,
|
||||
: expense?.latest_approval?.step_name || '-',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
interface InfoRow {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const getInfoTableColumns = (): PdfColumn<InfoRow>[] => [
|
||||
{
|
||||
key: 'label',
|
||||
header: 'Field',
|
||||
flex: 1,
|
||||
cell: ({ row }) => row.label,
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
header: 'Value',
|
||||
flex: 2,
|
||||
cell: ({ row }) => row.value,
|
||||
},
|
||||
];
|
||||
|
||||
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
const kandangs = expense?.kandangs || [];
|
||||
const infoRows = getInfoTableRows(expense);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page style={ExpensePDFStyle.page}>
|
||||
<View>
|
||||
<View style={ExpensePDFStyle.companyInfoHeader}>
|
||||
<Image
|
||||
style={ExpensePDFStyle.companyLogo}
|
||||
src='/assets/img/lti-logo.png'
|
||||
/>
|
||||
|
||||
<Text style={ExpensePDFStyle.companyInfoHeaderDate}>
|
||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={ExpensePDFStyle.companyName}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text style={ExpensePDFStyle.companyAddress}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
|
||||
<View style={ExpensePDFStyle.doubleDivider} />
|
||||
<Page style={styles.page} size='A4'>
|
||||
{/* Title Section */}
|
||||
<View style={styles.titleSection}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan{' '}
|
||||
{expense?.category === 'BOP'
|
||||
? 'Biaya Operasional'
|
||||
: 'Non-Biaya Operasional'}
|
||||
</PdfTypography>
|
||||
<PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
|
||||
<View style={styles.parameterContainer}>
|
||||
<PdfParamBadge>
|
||||
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</PdfParamBadge>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={ExpensePDFStyle.title}>
|
||||
Laporan{' '}
|
||||
{expense?.category === 'BOP'
|
||||
? 'Biaya Operasional'
|
||||
: 'Non-Biaya Operasional'}{' '}
|
||||
{expense?.po_number}
|
||||
</Text>
|
||||
|
||||
{/* General info table */}
|
||||
<View style={ExpensePDFStyle.generalInfoTable}>
|
||||
{rows.map((row) => (
|
||||
<View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}>
|
||||
<View style={ExpensePDFStyle.generalInfoTableColLabel}>
|
||||
<Text style={ExpensePDFStyle.generalInfoTableLabelText}>
|
||||
{row.label}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
|
||||
<Text>:</Text>
|
||||
</View>
|
||||
<View style={ExpensePDFStyle.generalInfoTableColValue}>
|
||||
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
|
||||
{row.value}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{/* Info Table Section */}
|
||||
<View style={styles.infoTableSection}>
|
||||
<Text style={styles.infoTableTitle}>Informasi Biaya</Text>
|
||||
<PdfTable columns={getInfoTableColumns()} data={infoRows} />
|
||||
</View>
|
||||
|
||||
{/* Detail expense request */}
|
||||
<View
|
||||
minPresenceAhead={80}
|
||||
style={ExpensePDFStyle.expenseDetailContainer}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.expenseDetailTitle}>
|
||||
Rincian Pengajuan Biaya Operasional
|
||||
</Text>
|
||||
{/* Rincian Pengajuan Section */}
|
||||
<View style={styles.tableSection}>
|
||||
<Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
|
||||
{kandangs.length === 0 ? (
|
||||
<Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
|
||||
) : (
|
||||
kandangs.map((kandang, idx) => {
|
||||
const pengajuans = kandang.pengajuans || [];
|
||||
const kandangName =
|
||||
kandang.kandang_id && kandang.name
|
||||
? kandang.name
|
||||
: expense?.location?.name || 'Umum';
|
||||
|
||||
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseRequestTotal = 0;
|
||||
|
||||
kandangExpense.pengajuans?.forEach(
|
||||
(item) => (expenseRequestTotal += item.qty * item.price)
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={kandangExpenseIdx}
|
||||
style={ExpensePDFStyle.kandangExpenseContainer}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
||||
{kandangExpense.kandang_id && kandangExpense.name
|
||||
? `Biaya ${kandangExpense.name}`
|
||||
: `Biaya ${expense?.location.name || 'Umum'}`}
|
||||
</Text>
|
||||
|
||||
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Nonstock
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Kuantitas
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Harga Satuan
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Catatan
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
|
||||
<View
|
||||
key={pengajuanIdx}
|
||||
style={ExpensePDFStyle.kandangExpenseTableRow}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{pengajuan.nonstock.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{formatNumber(pengajuan.qty)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{formatCurrency(pengajuan.price)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{pengajuan.notes}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Total Biaya Keseluruhan
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
{formatCurrency(expenseRequestTotal)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
return (
|
||||
<View key={idx} style={{ marginBottom: 12 }}>
|
||||
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
|
||||
{idx + 1}) {kandangName}
|
||||
</PdfTypography>
|
||||
{pengajuans.length > 0 ? (
|
||||
<PdfTable
|
||||
columns={getPengajuanColumns()}
|
||||
data={pengajuans}
|
||||
showFooter={true}
|
||||
footerLabel='Total'
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.emptyText}>
|
||||
Tidak ada item pengajuan untuk kandang ini.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Detail expense realization */}
|
||||
<View
|
||||
minPresenceAhead={80}
|
||||
style={ExpensePDFStyle.expenseDetailContainer}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.expenseDetailTitle}>
|
||||
Rincian Realisasi Biaya Operasional
|
||||
</Text>
|
||||
{/* Rincian Realisasi Section */}
|
||||
<View style={styles.tableSection}>
|
||||
<Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
|
||||
{kandangs.length === 0 ? (
|
||||
<Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
|
||||
) : (
|
||||
kandangs.map((kandang, idx) => {
|
||||
const realisasi = kandang.realisasi || [];
|
||||
const kandangName =
|
||||
kandang.kandang_id && kandang.name
|
||||
? kandang.name
|
||||
: expense?.location?.name || 'Umum';
|
||||
|
||||
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseRealizationTotal = 0;
|
||||
|
||||
kandangExpense.realisasi?.forEach(
|
||||
(item) => (expenseRealizationTotal += item.qty * item.price)
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={kandangExpenseIdx}
|
||||
style={ExpensePDFStyle.kandangExpenseContainer}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
||||
{kandangExpense.kandang_id && kandangExpense.name
|
||||
? `Biaya ${kandangExpense.name}`
|
||||
: `Biaya ${expense?.location.name || 'Umum'}`}
|
||||
</Text>
|
||||
|
||||
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Nonstock
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Kuantitas
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Harga Satuan
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Catatan
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{kandangExpense.realisasi?.map((realisasi, realisasiIdx) => (
|
||||
<View
|
||||
key={realisasiIdx}
|
||||
style={ExpensePDFStyle.kandangExpenseTableRow}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
ExpensePDFStyle.kandangExpenseTableColNonstock,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{realisasi.nonstock.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{formatNumber(realisasi.qty)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{formatCurrency(realisasi.price)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableColLabel,
|
||||
ExpensePDFStyle.kandangExpenseTableColNote,
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{realisasi.notes}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
|
||||
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
Total Biaya Keseluruhan
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
|
||||
>
|
||||
{formatCurrency(expenseRealizationTotal)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
return (
|
||||
<View key={idx} style={{ marginBottom: 12 }}>
|
||||
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
|
||||
{idx + 1}) {kandangName}
|
||||
</PdfTypography>
|
||||
{realisasi.length > 0 ? (
|
||||
<PdfTable
|
||||
columns={getRealisasiColumns()}
|
||||
data={realisasi}
|
||||
showFooter={true}
|
||||
footerLabel='Total'
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.emptyText}>
|
||||
Tidak ada item realisasi untuk kandang ini.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={ExpensePDFStyle.footer} fixed>
|
||||
<Link
|
||||
src={`${process.env.NEXT_PUBLIC_LTI_URL}expense/detail?expenseId=${expense?.id}`}
|
||||
>
|
||||
{expense?.po_number}
|
||||
</Link>
|
||||
|
||||
<Text
|
||||
render={({ pageNumber, totalPages }) =>
|
||||
`${pageNumber} / ${totalPages}`
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
</View>
|
||||
<PdfPageNumber />
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
@@ -315,7 +315,7 @@ const InventoryAdjustmentTable = () => {
|
||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||
},
|
||||
],
|
||||
[tableFilterState.pageSize, tableFilterState.page]
|
||||
[]
|
||||
);
|
||||
|
||||
const updateSortingFilter = useCallback(
|
||||
|
||||
@@ -323,6 +323,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { setFieldValue, setFieldTouched, setFieldError } = formik;
|
||||
|
||||
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||
formik.values.source_warehouse_id
|
||||
);
|
||||
@@ -336,14 +338,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||
prevSourceWarehouseId !== null
|
||||
) {
|
||||
formik.setFieldValue('products', [
|
||||
setFieldValue('products', [
|
||||
{
|
||||
product: null,
|
||||
product_id: 0,
|
||||
product_qty: '',
|
||||
},
|
||||
]);
|
||||
formik.setFieldTouched('products', false);
|
||||
setFieldTouched('products', false);
|
||||
|
||||
const updatedDeliveries = formik.values.deliveries.map(
|
||||
(delivery: DeliverySchema) => ({
|
||||
@@ -357,12 +359,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
],
|
||||
})
|
||||
);
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
formik.setFieldTouched('deliveries', false);
|
||||
setFieldValue('deliveries', updatedDeliveries);
|
||||
setFieldTouched('deliveries', false);
|
||||
}
|
||||
|
||||
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||
}, [
|
||||
formik.values.source_warehouse_id,
|
||||
formik.values.deliveries,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
]);
|
||||
|
||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||
const {
|
||||
@@ -455,9 +462,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
// ===== EVENT HANDLERS =====
|
||||
const handleTransferDateChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
formik.setFieldValue('transfer_date', e.target.value);
|
||||
setFieldValue('transfer_date', e.target.value);
|
||||
},
|
||||
[]
|
||||
[setFieldValue]
|
||||
);
|
||||
|
||||
const handleSourceWarehouseChange = useCallback(
|
||||
@@ -477,14 +484,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
formik.setFieldTouched('source_warehouse', true);
|
||||
formik.setFieldValue('source_warehouse', val);
|
||||
formik.setFieldTouched('source_warehouse_id', true);
|
||||
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||
setFieldTouched('source_warehouse', true);
|
||||
setFieldValue('source_warehouse', val);
|
||||
setFieldTouched('source_warehouse_id', true);
|
||||
setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||
},
|
||||
[
|
||||
formik.values.destination_warehouse_id,
|
||||
formik.values.destination_warehouse,
|
||||
setFieldTouched,
|
||||
setFieldValue,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -505,15 +514,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
formik.setFieldTouched('destination_warehouse', true);
|
||||
formik.setFieldValue('destination_warehouse', val);
|
||||
formik.setFieldTouched('destination_warehouse_id', true);
|
||||
formik.setFieldValue(
|
||||
'destination_warehouse_id',
|
||||
newDestinationWarehouseId
|
||||
);
|
||||
setFieldTouched('destination_warehouse', true);
|
||||
setFieldValue('destination_warehouse', val);
|
||||
setFieldTouched('destination_warehouse_id', true);
|
||||
setFieldValue('destination_warehouse_id', newDestinationWarehouseId);
|
||||
},
|
||||
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
||||
[
|
||||
formik.values.source_warehouse_id,
|
||||
formik.values.source_warehouse,
|
||||
setFieldTouched,
|
||||
setFieldValue,
|
||||
]
|
||||
);
|
||||
|
||||
const addProduct = useCallback(() => {
|
||||
@@ -525,15 +536,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
product_qty: '',
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('products', newProducts);
|
||||
}, [formik.values.products]);
|
||||
setFieldValue('products', newProducts);
|
||||
}, [formik.values.products, setFieldValue]);
|
||||
|
||||
const removeProduct = useCallback(
|
||||
(i: number) => {
|
||||
const updatedProducts = formik.values.products?.filter(
|
||||
(_, idx) => idx !== i
|
||||
);
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
setFieldValue('products', updatedProducts);
|
||||
|
||||
setSelectedProducts([]);
|
||||
|
||||
@@ -542,7 +553,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
setProductQtyErrorShown(false);
|
||||
}
|
||||
},
|
||||
[formik.values.products, productQtyErrorShown, setSelectedProducts]
|
||||
[
|
||||
formik.values.products,
|
||||
productQtyErrorShown,
|
||||
setSelectedProducts,
|
||||
setFieldValue,
|
||||
]
|
||||
);
|
||||
|
||||
const bulkRemoveProduct = useCallback(() => {
|
||||
@@ -550,26 +566,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
formik.values.products?.filter(
|
||||
(_, idx) => !selectedProducts.includes(idx)
|
||||
) ?? [];
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
setFieldValue('products', updatedProducts);
|
||||
setSelectedProducts([]);
|
||||
|
||||
if (productQtyErrorShown) {
|
||||
toast.dismiss();
|
||||
setProductQtyErrorShown(false);
|
||||
}
|
||||
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]);
|
||||
}, [
|
||||
selectedProducts,
|
||||
setSelectedProducts,
|
||||
productQtyErrorShown,
|
||||
setFieldValue,
|
||||
formik.values.products,
|
||||
]);
|
||||
|
||||
const handleProductChange = useCallback(
|
||||
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched(`products.${idx}.product`, true);
|
||||
formik.setFieldValue(`products.${idx}.product`, val);
|
||||
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
||||
formik.setFieldValue(
|
||||
setFieldTouched(`products.${idx}.product`, true);
|
||||
setFieldValue(`products.${idx}.product`, val);
|
||||
setFieldTouched(`products.${idx}.product_id`, true);
|
||||
setFieldValue(
|
||||
`products.${idx}.product_id`,
|
||||
(val as ProductWarehouseOptionType)?.value
|
||||
);
|
||||
},
|
||||
[]
|
||||
[setFieldTouched, setFieldValue]
|
||||
);
|
||||
|
||||
const handleProductSelectAllChange = useCallback(
|
||||
@@ -596,7 +618,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
);
|
||||
|
||||
const addDelivery = useCallback(() => {
|
||||
formik.setFieldValue('deliveries', [
|
||||
setFieldValue('deliveries', [
|
||||
...(formik.values.deliveries || []),
|
||||
{
|
||||
delivery_cost: '',
|
||||
@@ -615,14 +637,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
],
|
||||
},
|
||||
]);
|
||||
}, [formik.values.deliveries]);
|
||||
}, [formik.values.deliveries, setFieldValue]);
|
||||
|
||||
const removeDelivery = useCallback(
|
||||
(i: number) => {
|
||||
const updatedDeliveries = formik.values.deliveries?.filter(
|
||||
(_, idx) => idx !== i
|
||||
);
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
setFieldValue('deliveries', updatedDeliveries);
|
||||
|
||||
setSelectedDeliveries([]);
|
||||
|
||||
@@ -631,7 +653,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
setDeliveryQtyErrorShown(false);
|
||||
}
|
||||
},
|
||||
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries]
|
||||
[
|
||||
formik.values.deliveries,
|
||||
deliveryQtyErrorShown,
|
||||
setSelectedDeliveries,
|
||||
setFieldValue,
|
||||
]
|
||||
);
|
||||
|
||||
const bulkRemoveDelivery = useCallback(() => {
|
||||
@@ -639,7 +666,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
formik.values.deliveries?.filter(
|
||||
(_, idx) => !selectedDeliveries.includes(idx)
|
||||
) ?? [];
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
setFieldValue('deliveries', updatedDeliveries);
|
||||
setSelectedDeliveries([]);
|
||||
|
||||
if (deliveryQtyErrorShown) {
|
||||
@@ -647,10 +674,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
setDeliveryQtyErrorShown(false);
|
||||
}
|
||||
}, [
|
||||
formik,
|
||||
selectedDeliveries,
|
||||
setSelectedDeliveries,
|
||||
deliveryQtyErrorShown,
|
||||
setFieldValue,
|
||||
formik.values.deliveries,
|
||||
]);
|
||||
|
||||
const handleDeliverySelectAllChange = useCallback(
|
||||
@@ -680,34 +708,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
|
||||
const handleDeliveryProductChange = useCallback(
|
||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${deliveryIdx}.products.0.product`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true);
|
||||
setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true);
|
||||
setFieldValue(
|
||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||
(val as OptionType)?.value
|
||||
);
|
||||
},
|
||||
[]
|
||||
[setFieldTouched, setFieldValue]
|
||||
);
|
||||
|
||||
const handleDeliverySupplierChange = useCallback(
|
||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||
formik.setFieldValue(
|
||||
setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||
setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||
setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||
setFieldValue(
|
||||
`deliveries.${deliveryIdx}.supplier_id`,
|
||||
(val as OptionType)?.value
|
||||
);
|
||||
},
|
||||
[]
|
||||
[setFieldTouched, setFieldValue]
|
||||
);
|
||||
|
||||
const handleDeliveryDocumentChange = useCallback(
|
||||
@@ -719,15 +741,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||
setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||
}
|
||||
},
|
||||
[]
|
||||
[setFieldValue]
|
||||
);
|
||||
|
||||
const handleDeliveryCostChange = useCallback(
|
||||
(idx: number, value: number) => {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||
|
||||
const delivery = formik.values.deliveries?.[idx];
|
||||
if (delivery) {
|
||||
@@ -737,21 +759,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
);
|
||||
if (productQty > 0 && value > 0) {
|
||||
const perItem = value / productQty;
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.delivery_cost_per_item`,
|
||||
perItem
|
||||
);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
|
||||
} else if (value === 0) {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[formik.values.deliveries]
|
||||
[formik.values.deliveries, setFieldValue]
|
||||
);
|
||||
|
||||
const handleDeliveryCostPerItemChange = useCallback(
|
||||
(idx: number, value: number) => {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||
|
||||
const delivery = formik.values.deliveries?.[idx];
|
||||
if (delivery) {
|
||||
@@ -761,13 +780,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
);
|
||||
if (productQty > 0 && value > 0) {
|
||||
const totalCost = value * productQty;
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||
} else if (value === 0) {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[formik.values.deliveries]
|
||||
[formik.values.deliveries, setFieldValue]
|
||||
);
|
||||
|
||||
const handleDeliveryCostChangeWrapper = useCallback(
|
||||
@@ -1044,12 +1063,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
return !validateDeliveryQty(deliveryIdx, productIdx, qty);
|
||||
})
|
||||
) ?? []),
|
||||
[
|
||||
formik.values.deliveries,
|
||||
formik.values.products,
|
||||
validateDeliveryQty,
|
||||
type,
|
||||
]
|
||||
[formik.values.deliveries, validateDeliveryQty, type]
|
||||
);
|
||||
|
||||
const hasInvalidQty = useMemo(
|
||||
@@ -1066,6 +1080,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
);
|
||||
}, [formik.values.products, getProductQtyError, type]);
|
||||
|
||||
const deliveryCostDepString = useMemo(
|
||||
() =>
|
||||
formik.values.deliveries
|
||||
?.map((d, idx) => ({
|
||||
idx,
|
||||
productQty: d.products.reduce(
|
||||
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||
0
|
||||
),
|
||||
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
|
||||
deliveryCostPerItem:
|
||||
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
||||
}))
|
||||
.map(
|
||||
(item) =>
|
||||
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
|
||||
)
|
||||
.join('|'),
|
||||
[formik.values.deliveries]
|
||||
);
|
||||
|
||||
// ===== EFFECTS =====
|
||||
useEffect(() => {
|
||||
formik.values.deliveries?.forEach((delivery, idx) => {
|
||||
@@ -1082,36 +1117,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
if (deliveryCost > 0 && productQty > 0) {
|
||||
const perItem = deliveryCost / productQty;
|
||||
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.delivery_cost_per_item`,
|
||||
perItem
|
||||
);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
|
||||
}
|
||||
} else if (deliveryCostPerItem > 0 && productQty > 0) {
|
||||
const totalCost = deliveryCostPerItem * productQty;
|
||||
if (Math.abs(deliveryCost - totalCost) > 0.01) {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [
|
||||
formik.values.deliveries
|
||||
?.map((d, idx) => ({
|
||||
idx,
|
||||
productQty: d.products.reduce(
|
||||
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||
0
|
||||
),
|
||||
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
|
||||
deliveryCostPerItem:
|
||||
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
||||
}))
|
||||
.map(
|
||||
(item) =>
|
||||
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
|
||||
)
|
||||
.join('|'),
|
||||
]);
|
||||
}, [deliveryCostDepString, setFieldValue, formik.values.deliveries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -1121,7 +1136,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
!isInitialized
|
||||
) {
|
||||
if (formik.values.products.length === 0) {
|
||||
formik.setFieldValue('products', [
|
||||
setFieldValue('products', [
|
||||
{
|
||||
product: null,
|
||||
product_id: 0,
|
||||
@@ -1130,7 +1145,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
]);
|
||||
}
|
||||
if (formik.values.deliveries.length === 0) {
|
||||
formik.setFieldValue('deliveries', [
|
||||
setFieldValue('deliveries', [
|
||||
{
|
||||
delivery_cost: undefined,
|
||||
delivery_cost_per_item: undefined,
|
||||
@@ -1152,7 +1167,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [formik.values.source_warehouse_id, isInitialized, type]);
|
||||
}, [
|
||||
formik.values.source_warehouse_id,
|
||||
isInitialized,
|
||||
type,
|
||||
setFieldValue,
|
||||
formik.values.products.length,
|
||||
formik.values.deliveries.length,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -1161,7 +1183,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
formik.values.source_warehouse_id ===
|
||||
formik.values.destination_warehouse_id
|
||||
) {
|
||||
formik.setFieldError(
|
||||
setFieldError(
|
||||
'destination_warehouse_id',
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
);
|
||||
@@ -1170,13 +1192,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
formik.errors.destination_warehouse_id ===
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
) {
|
||||
formik.setFieldError('destination_warehouse_id', undefined);
|
||||
setFieldError('destination_warehouse_id', undefined);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
formik.values.source_warehouse_id,
|
||||
formik.values.destination_warehouse_id,
|
||||
formik.errors.destination_warehouse_id,
|
||||
setFieldError,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1212,29 +1235,37 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
);
|
||||
|
||||
if (hasChanges) {
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
setFieldValue('deliveries', updatedDeliveries);
|
||||
}
|
||||
}
|
||||
}, [formik.values.products]);
|
||||
}, [formik.values.products, formik.values.deliveries, setFieldValue]);
|
||||
|
||||
const productQtyDepString = useMemo(
|
||||
() => formik.values.products?.map((p) => p.product_qty).join(','),
|
||||
[formik.values.products]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (productQtyErrorShown) {
|
||||
toast.dismiss();
|
||||
setProductQtyErrorShown(false);
|
||||
}
|
||||
}, [formik.values.products?.map((p) => p.product_qty).join(',')]);
|
||||
}, [productQtyErrorShown]);
|
||||
|
||||
const deliveryProductQtyDepString = useMemo(
|
||||
() =>
|
||||
formik.values.deliveries
|
||||
?.map((d) => d.products.map((p) => p.product_qty).join(','))
|
||||
.join('|'),
|
||||
[formik.values.deliveries]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deliveryQtyErrorShown) {
|
||||
toast.dismiss();
|
||||
setDeliveryQtyErrorShown(false);
|
||||
}
|
||||
}, [
|
||||
formik.values.deliveries
|
||||
?.map((d) => d.products.map((p) => p.product_qty).join(','))
|
||||
.join('|'),
|
||||
formik.values.products?.map((p) => p.product_qty).join(','),
|
||||
]);
|
||||
}, [deliveryProductQtyDepString, productQtyDepString, deliveryQtyErrorShown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
|
||||
|
||||
@@ -536,9 +536,13 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
||||
formModal.closeModal();
|
||||
};
|
||||
|
||||
const hasLoadedInitialValues = useRef(false);
|
||||
useEffect(() => {
|
||||
const getFilledInitialValues = async () => {
|
||||
if (marketingId && isResponseSuccess(marketing)) {
|
||||
if (hasLoadedInitialValues.current) return;
|
||||
hasLoadedInitialValues.current = true;
|
||||
|
||||
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
||||
marketing.data
|
||||
);
|
||||
@@ -582,9 +586,15 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
|
||||
setFormErrorMessage('');
|
||||
}, [step]);
|
||||
|
||||
// sync delivery order values to formik
|
||||
const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues);
|
||||
useEffect(() => {
|
||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||
if (
|
||||
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
|
||||
JSON.stringify(deliveryOrderValues)
|
||||
) {
|
||||
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
|
||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||
}
|
||||
}, [deliveryOrderValues]);
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
|
||||
@@ -226,11 +226,6 @@ const MarketingTable = () => {
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const productsClickHandler = (item: Marketing) => {
|
||||
setSelectedItem(item);
|
||||
productsModal.openModal();
|
||||
};
|
||||
|
||||
const deleteMarketingHandler = async () => {
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
selectedItem?.id as number
|
||||
@@ -450,6 +445,11 @@ const MarketingTable = () => {
|
||||
accessorKey: 'marketing_products.length',
|
||||
header: 'Product Details',
|
||||
cell: (props) => {
|
||||
const productsClickHandler = (item: Marketing) => {
|
||||
setSelectedItem(item);
|
||||
productsModal.openModal();
|
||||
};
|
||||
|
||||
if (props?.row?.original?.sales_order?.length) {
|
||||
if (props?.row?.original?.sales_order?.length > 1) {
|
||||
return (
|
||||
@@ -504,7 +504,7 @@ const MarketingTable = () => {
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
}, [deleteModal, deliveryModal, setSelectedItem, productsModal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -458,9 +458,13 @@ const SalesOrderFormModal = ({
|
||||
);
|
||||
}, [memoSalesOrder]);
|
||||
|
||||
const hasLoadedInitialValues = useRef(false);
|
||||
useEffect(() => {
|
||||
const getFilledInitialValues = async () => {
|
||||
if (marketingId && isResponseSuccess(marketing)) {
|
||||
if (hasLoadedInitialValues.current) return;
|
||||
hasLoadedInitialValues.current = true;
|
||||
|
||||
const filledInitialValues = await getFilledMarketingFormInitialValues(
|
||||
marketing.data
|
||||
);
|
||||
|
||||
+26
-15
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
DeliveryOrderProductFormValues,
|
||||
DeliveryOrderProductSchema,
|
||||
@@ -224,6 +224,8 @@ const DeliveryOrderProductForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { resetForm } = formik;
|
||||
|
||||
const hasWeekField = useMemo(() => {
|
||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||
if (marketingType === 'ayam_pullet') {
|
||||
@@ -243,9 +245,9 @@ const DeliveryOrderProductForm = ({
|
||||
return false;
|
||||
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
const handleResetForm = useCallback(() => {
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
resetForm({
|
||||
values: {
|
||||
delivery_date: '',
|
||||
vehicle_number: '',
|
||||
@@ -269,17 +271,20 @@ const DeliveryOrderProductForm = ({
|
||||
},
|
||||
});
|
||||
// setSelectedProduct(null);
|
||||
};
|
||||
}, [resetForm]);
|
||||
|
||||
const handleBlurField = (field: string) => {
|
||||
setCurrentInput(field);
|
||||
const handleBlurField = useCallback(
|
||||
(field: string) => {
|
||||
setCurrentInput(field);
|
||||
|
||||
handleMarketingCalculation(field, {
|
||||
values: formik.values,
|
||||
setFieldValue: formik.setFieldValue,
|
||||
hasSisaBerat,
|
||||
});
|
||||
};
|
||||
handleMarketingCalculation(field, {
|
||||
values: formik.values,
|
||||
setFieldValue: formik.setFieldValue,
|
||||
hasSisaBerat,
|
||||
});
|
||||
},
|
||||
[formik.values, formik.setFieldValue, hasSisaBerat]
|
||||
);
|
||||
|
||||
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
||||
const handleFieldChange = (
|
||||
@@ -324,8 +329,12 @@ const DeliveryOrderProductForm = ({
|
||||
|
||||
const { setValues: setFormikValues } = formik;
|
||||
|
||||
const processedInitialValuesRef = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
if (processedInitialValuesRef.current === initialValues.id) return;
|
||||
processedInitialValuesRef.current = initialValues.id as number;
|
||||
|
||||
if (!Boolean(initialValues.qty)) {
|
||||
handleResetForm();
|
||||
} else {
|
||||
@@ -338,7 +347,7 @@ const DeliveryOrderProductForm = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [initialValues]);
|
||||
}, [handleResetForm, initialValues, setFormikValues]);
|
||||
|
||||
// ===== Formik Error List =====
|
||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||
@@ -356,8 +365,10 @@ const DeliveryOrderProductForm = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleBlurField('week');
|
||||
}, [formik.values.week]);
|
||||
if (formik.values.week) {
|
||||
handleBlurField('week');
|
||||
}
|
||||
}, [formik.values.week, handleBlurField]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
+16
-11
@@ -5,7 +5,7 @@ import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import { RefObject, useEffect, useMemo, useState } from 'react';
|
||||
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { WarehouseApi } from '@/services/api/master-data';
|
||||
@@ -240,15 +240,18 @@ const SalesOrderProductForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlurField = (field: string) => {
|
||||
setCurrentInput(field);
|
||||
const handleBlurField = useCallback(
|
||||
(field: string) => {
|
||||
setCurrentInput(field);
|
||||
|
||||
handleMarketingCalculation(field, {
|
||||
values: formik.values,
|
||||
setFieldValue: formik.setFieldValue,
|
||||
hasSisaBerat,
|
||||
});
|
||||
};
|
||||
handleMarketingCalculation(field, {
|
||||
values: formik.values,
|
||||
setFieldValue: formik.setFieldValue,
|
||||
hasSisaBerat,
|
||||
});
|
||||
},
|
||||
[formik.values, formik.setFieldValue, hasSisaBerat]
|
||||
);
|
||||
|
||||
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
|
||||
const handleFieldChange = (
|
||||
@@ -307,8 +310,10 @@ const SalesOrderProductForm = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleBlurField('week');
|
||||
}, [formik.values.week]);
|
||||
if (formik.values.week) {
|
||||
handleBlurField('week');
|
||||
}
|
||||
}, [formik.values.week, handleBlurField]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
|
||||
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
interface DeliveryOrderExportProps {
|
||||
data?: Marketing;
|
||||
@@ -20,9 +19,6 @@ const DeliveryOrderExport = ({
|
||||
}: DeliveryOrderExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
const salesData = data;
|
||||
const searchParams = useSearchParams();
|
||||
const action = searchParams.get('action');
|
||||
const id = searchParams.get('id');
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
@@ -53,7 +49,6 @@ const DeliveryOrderExport = ({
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
window.location.href = `/marketing?action=${action}&id=${id}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,7 +87,7 @@ const PDFDocument = ({
|
||||
return (
|
||||
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
||||
);
|
||||
}, []);
|
||||
}, [deliveryOrder.deliveries]);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
interface SalesOrderExportProps {
|
||||
data?: Marketing;
|
||||
@@ -16,9 +15,6 @@ interface SalesOrderExportProps {
|
||||
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
const salesData = data;
|
||||
const searchParams = useSearchParams();
|
||||
const action = searchParams.get('action');
|
||||
const id = searchParams.get('id');
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
@@ -47,7 +43,6 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
window.location.href = `/marketing?action=${action}&id=${id}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ const ChickinFormKandang = ({
|
||||
|
||||
const afterSubmitFormChickin = () => {
|
||||
setOpenChickin(true);
|
||||
afterSubmit && afterSubmit();
|
||||
if (afterSubmit) {
|
||||
afterSubmit();
|
||||
}
|
||||
refreshApprovals();
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ const ChickinLogsView = ({
|
||||
toast.error(approveChickinRes?.message as string);
|
||||
setChickinErrorMessage(approveChickinRes?.message as string);
|
||||
}
|
||||
afterSubmit && afterSubmit();
|
||||
if (afterSubmit) {
|
||||
afterSubmit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const TransferToLayingDetailModal = () => {
|
||||
if (modalAction === 'detail') {
|
||||
detailModal.openModal();
|
||||
}
|
||||
}, [modalAction]);
|
||||
}, [modalAction, detailModal]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -463,7 +463,7 @@ const TransferToLayingsTable = () => {
|
||||
updateFilter('filter_by', '');
|
||||
updateFilter('sort_by', '');
|
||||
}
|
||||
}, [sorting]);
|
||||
}, [sorting, updateFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -60,25 +60,6 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
||||
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`);
|
||||
};
|
||||
|
||||
const handleViewUniformityDetails = () => {
|
||||
if (!uniformity_details || uniformity_details.length === 0) {
|
||||
setShouldFetchDetails(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedDrawerContent(
|
||||
<UniformityDetailsPreview
|
||||
info_umum={initialValues.info_umum}
|
||||
uniformity_details={uniformity_details}
|
||||
uniformityId={initialValues.id}
|
||||
/>
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
setExpandedDrawerOpen(true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
shouldFetchDetails &&
|
||||
@@ -202,6 +183,25 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
||||
}
|
||||
|
||||
if (id === 'document-name') {
|
||||
const handleViewUniformityDetails = () => {
|
||||
if (!uniformity_details || uniformity_details.length === 0) {
|
||||
setShouldFetchDetails(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedDrawerContent(
|
||||
<UniformityDetailsPreview
|
||||
info_umum={initialValues.info_umum}
|
||||
uniformity_details={uniformity_details}
|
||||
uniformityId={initialValues.id}
|
||||
/>
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
setExpandedDrawerOpen(true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>{valueMap[id]}</span>
|
||||
@@ -231,7 +231,14 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
[initialValues, handleViewUniformityDetails, isLoading]
|
||||
[
|
||||
initialValues,
|
||||
isLoading,
|
||||
uniformity_details,
|
||||
setShouldFetchDetails,
|
||||
setExpandedDrawerContent,
|
||||
setExpandedDrawerOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const samplingTableData: DetailOptionType[] = useMemo(() => {
|
||||
|
||||
@@ -301,8 +301,93 @@ export function DailyChecklistContent() {
|
||||
checkAndLoadChecklist();
|
||||
}, [date, kandangId, selectedCategory]);
|
||||
|
||||
// Load employees when kandang changes
|
||||
useEffect(() => {
|
||||
if (kandangId) {
|
||||
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
|
||||
setSelectedEmployees([]);
|
||||
setAssignments({});
|
||||
} else {
|
||||
setSelectedEmployees([]);
|
||||
setAssignments({});
|
||||
}
|
||||
}, [kandangId]);
|
||||
|
||||
// Load activities and tasks when phases change
|
||||
useEffect(() => {
|
||||
const loadAssignments = async (taskIds: string[]) => {
|
||||
if (taskIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const existingDailyChecklist =
|
||||
await DailyChecklistApi.getOneDailyChecklist(
|
||||
String(dailyChecklistId)
|
||||
);
|
||||
|
||||
if (isResponseError(existingDailyChecklist)) {
|
||||
console.error(
|
||||
'Error loading assignments:',
|
||||
existingDailyChecklist.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// set existing document
|
||||
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
|
||||
|
||||
// Build assignments map
|
||||
const assignmentMap: {
|
||||
[taskId: string]: {
|
||||
[employeeId: string]: { checked: boolean; note: string };
|
||||
};
|
||||
} = {};
|
||||
|
||||
(existingDailyChecklist?.data.tasks || []).forEach(
|
||||
(dailyChecklistTask) => {
|
||||
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||
assignmentMap[dailyChecklistTask.id] = {};
|
||||
}
|
||||
|
||||
dailyChecklistTask.assignments.forEach((assignment) => {
|
||||
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||
assignmentMap[dailyChecklistTask.id] = {};
|
||||
}
|
||||
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
|
||||
checked: assignment.checked,
|
||||
note: assignment.note || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
setAssignments(assignmentMap);
|
||||
|
||||
// Load employees from assignments
|
||||
const employeeIds = Array.from(
|
||||
new Set(
|
||||
(existingDailyChecklist?.data.assigned_employees || []).map(
|
||||
(a) => a.id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (employeeIds.length > 0) {
|
||||
const existingDailyChecklist =
|
||||
await DailyChecklistApi.getOneDailyChecklist(
|
||||
String(dailyChecklistId)
|
||||
);
|
||||
|
||||
if (isResponseSuccess(existingDailyChecklist)) {
|
||||
setSelectedEmployees(
|
||||
existingDailyChecklist.data.assigned_employees
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading assignments:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadActivitiesAndTasks = async () => {
|
||||
if (!dailyChecklistId || selectedPhaseIds.length === 0) {
|
||||
setActivitiesByPhase({});
|
||||
@@ -377,87 +462,6 @@ export function DailyChecklistContent() {
|
||||
loadActivitiesAndTasks();
|
||||
}, [dailyChecklistId, selectedPhaseIds]);
|
||||
|
||||
// Load employees when kandang changes
|
||||
useEffect(() => {
|
||||
if (kandangId) {
|
||||
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
|
||||
setSelectedEmployees([]);
|
||||
setAssignments({});
|
||||
} else {
|
||||
setSelectedEmployees([]);
|
||||
setAssignments({});
|
||||
}
|
||||
}, [kandangId]);
|
||||
|
||||
const loadAssignments = async (taskIds: string[]) => {
|
||||
if (taskIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const existingDailyChecklist =
|
||||
await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId));
|
||||
|
||||
if (isResponseError(existingDailyChecklist)) {
|
||||
console.error(
|
||||
'Error loading assignments:',
|
||||
existingDailyChecklist.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// set existing document
|
||||
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
|
||||
|
||||
// Build assignments map
|
||||
const assignmentMap: {
|
||||
[taskId: string]: {
|
||||
[employeeId: string]: { checked: boolean; note: string };
|
||||
};
|
||||
} = {};
|
||||
|
||||
(existingDailyChecklist?.data.tasks || []).forEach(
|
||||
(dailyChecklistTask) => {
|
||||
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||
assignmentMap[dailyChecklistTask.id] = {};
|
||||
}
|
||||
|
||||
dailyChecklistTask.assignments.forEach((assignment) => {
|
||||
if (!assignmentMap[dailyChecklistTask.id]) {
|
||||
assignmentMap[dailyChecklistTask.id] = {};
|
||||
}
|
||||
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
|
||||
checked: assignment.checked,
|
||||
note: assignment.note || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
setAssignments(assignmentMap);
|
||||
|
||||
// Load employees from assignments
|
||||
const employeeIds = Array.from(
|
||||
new Set(
|
||||
(existingDailyChecklist?.data.assigned_employees || []).map(
|
||||
(a) => a.id
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (employeeIds.length > 0) {
|
||||
const existingDailyChecklist =
|
||||
await DailyChecklistApi.getOneDailyChecklist(
|
||||
String(dailyChecklistId)
|
||||
);
|
||||
|
||||
if (isResponseSuccess(existingDailyChecklist)) {
|
||||
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading assignments:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Phase selection modal
|
||||
const handleAddPhase = () => {
|
||||
if (!selectedCategory) {
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/figma-make/components/base/card';
|
||||
@@ -137,15 +137,7 @@ export function DetailDailyChecklistContent() {
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (checklistId) {
|
||||
fetchChecklistDetail();
|
||||
} else {
|
||||
router.push('/404');
|
||||
}
|
||||
}, [checklistId]);
|
||||
|
||||
const fetchChecklistDetail = async () => {
|
||||
const fetchChecklistDetail = useCallback(async () => {
|
||||
if (!checklistId) {
|
||||
console.warn('checklistId missing');
|
||||
setLoading(false);
|
||||
@@ -320,7 +312,15 @@ export function DetailDailyChecklistContent() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [checklistId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (checklistId) {
|
||||
fetchChecklistDetail();
|
||||
} else {
|
||||
router.push('/404');
|
||||
}
|
||||
}, [checklistId, fetchChecklistDetail, router]);
|
||||
|
||||
const groupDetailData = (rows: ChecklistDetailRow[]) => {
|
||||
// Group by phase_id
|
||||
|
||||
@@ -305,3 +305,17 @@ export function transformConstants(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function omit<T extends Record<string, unknown>, K extends keyof T>(
|
||||
obj: T,
|
||||
keys: K | K[]
|
||||
): Omit<T, K> {
|
||||
const keysArray = Array.isArray(keys) ? keys : [keys];
|
||||
const result = { ...obj };
|
||||
|
||||
keysArray.forEach((key) => {
|
||||
delete result[key];
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store';
|
||||
import { omit } from '@/lib/helper';
|
||||
import { StateCreator } from 'zustand';
|
||||
|
||||
export const createTabActionsSlice: StateCreator<
|
||||
@@ -20,10 +21,9 @@ export const createTabActionsSlice: StateCreator<
|
||||
})),
|
||||
|
||||
clearTabActions: (tabId) =>
|
||||
set((state) => {
|
||||
const { [tabId]: _, ...rest } = state.tabActions;
|
||||
return { tabActions: rest };
|
||||
}),
|
||||
set((state) => ({
|
||||
tabActions: omit(state.tabActions, tabId),
|
||||
})),
|
||||
|
||||
clearAllTabActions: () => set({ tabActions: {} }),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user