Compare commits

...

18 Commits

Author SHA1 Message Date
rstubryan 584b495e4b refactor(FE): Fix missing dependency in useEffect hook 2026-03-07 12:01:46 +07:00
rstubryan c8e76a8558 refactor(FE): Refactor ExpensePDF component for improved readability 2026-03-07 10:14:10 +07:00
rstubryan a468a5948b refactor(FE): Refactor useEffect hooks and loadAssignments function 2026-03-07 09:34:55 +07:00
rstubryan e05e1a4121 chore(FE): Fix missing dependency in useEffect dependency array 2026-03-07 09:20:14 +07:00
rstubryan e3b86e3033 refactor(FE): Add omit utility and refactor clearTabActions to use
it
2026-03-07 09:19:11 +07:00
rstubryan d467c56ea6 refactor(FE): Refactor form reset logic to use useCallback with
dependencies
2026-03-06 15:03:37 +07:00
rstubryan 784d9f26ab refactor(FE): Refactor fetchChecklistDetail with useCallback 2026-03-06 15:00:33 +07:00
rstubryan 978ef764ea refactor(FE): Refactor productsClickHandler to be defined inline 2026-03-06 14:45:27 +07:00
rstubryan 928136ff18 refactor(FE): Refactor handleViewUniformityDetails to inline definition 2026-03-06 14:29:21 +07:00
rstubryan 79567e4f1b refactor(FE): Remove unnecessary dependencies from useMemo in table
columns
2026-03-06 14:07:24 +07:00
rstubryan 633deece21 refactor(FE): Fix dependency array in ExpenseRealizationForm useCallback 2026-03-06 14:05:11 +07:00
rstubryan 46483af4c2 refactor(FE): Refactor formik field methods to use destructured helpers 2026-03-06 13:53:15 +07:00
rstubryan c2653e5068 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into fix/cleanup-warning 2026-03-06 13:41:30 +07:00
rstubryan 8569bda7d6 refactor(FE): Refactor afterSubmit calls to improve readability 2026-03-06 13:41:29 +07:00
rstubryan 7da63dc542 refactor(FE): Prevent duplicate initial value loading in form modals 2026-03-06 13:37:35 +07:00
rstubryan aeceef4361 refactor(FE): Refactor handleBlurField to use useCallback 2026-03-06 13:35:10 +07:00
rstubryan ff6955be54 refactor(FE): Remove unused useSearchParams and related code 2026-03-06 13:33:46 +07:00
rstubryan eccab314b3 refactor(FE): Fix missing dependencies in MarketingTable useMemo 2026-03-06 11:23:01 +07:00
21 changed files with 595 additions and 834 deletions
@@ -66,7 +66,7 @@ const ExpenseRealizationForm = ({
toast.success(createExpenseRes?.message as string); toast.success(createExpenseRes?.message as string);
router.push('/expense'); router.push('/expense');
}, },
[router] [router, initialValues?.id]
); );
const updateExpenseHandler = useCallback( const updateExpenseHandler = useCallback(
@@ -178,12 +178,14 @@ const ExpenseRequestForm = ({
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocationOptions,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name'); } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const { const {
setInputValue: setVendorInputValue, setInputValue: setVendorInputValue,
options: supplierOptions, options: supplierOptions,
isLoadingOptions: isLoadingVendorOptions, isLoadingOptions: isLoadingVendorOptions,
loadMore: loadMoreVendorOptions,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name'); } = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -414,6 +416,7 @@ const ExpenseRequestForm = ({
errorMessage={formik.errors.location_id as string} errorMessage={formik.errors.location_id as string}
isClearable isClearable
className={{ wrapper: 'col-span-12 sm:col-span-4' }} className={{ wrapper: 'col-span-12 sm:col-span-4' }}
onMenuScrollToBottom={loadMoreLocationOptions}
/> />
<DateInput <DateInput
@@ -457,6 +460,7 @@ const ExpenseRequestForm = ({
} }
errorMessage={formik.errors.supplier_id as string} errorMessage={formik.errors.supplier_id as string}
className={{ wrapper: 'col-span-12' }} className={{ wrapper: 'col-span-12' }}
onMenuScrollToBottom={loadMoreVendorOptions}
/> />
<RequirePermission permissions='lti.expense.document'> <RequirePermission permissions='lti.expense.document'>
+236 -559
View File
@@ -1,212 +1,154 @@
'use client'; 'use client';
import { import React from 'react';
Document, import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
Image,
Link,
Page,
StyleSheet,
Text,
View,
} from '@react-pdf/renderer';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; 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 { interface ExpensePDFProps {
expense?: Expense; expense?: Expense;
} }
const ExpensePDFStyle = StyleSheet.create({ const styles = StyleSheet.create({
page: { page: {
paddingTop: 24, fontSize: 10,
paddingBottom: 64, fontFamily: 'Helvetica',
paddingHorizontal: 32, padding: 20,
backgroundColor: '#FFFFFF',
}, },
titleSection: {
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,
marginBottom: 10, marginBottom: 10,
}, },
parameterContainer: {
title: {
marginTop: 16,
fontSize: 16,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
footer: {
width: '100%',
display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', flexWrap: 'wrap',
alignItems: 'center', marginBottom: 8,
paddingHorizontal: 32, },
infoTableSection: {
position: 'absolute', marginBottom: 12,
},
infoTableTitle: {
fontSize: 10, 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', fontWeight: 'bold',
marginBottom: 6,
color: '#333',
}, },
generalInfoTableValueText: {}, tableSection: {
marginBottom: 12,
// expense detail table
expenseDetailContainer: {
width: '100%',
marginTop: 12,
}, },
expenseDetailTitle: { tableTitle: {
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: {
fontSize: 10, fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
color: '#333',
}, },
kandangExpenseTableFooterColTotalExpenseCaption: { emptyText: {
width: '40%', fontSize: 8,
paddingVertical: 6, color: '#666',
paddingHorizontal: 8, fontStyle: 'italic',
textAlign: 'right',
},
kandangExpenseTableFooterColTotalExpenseValue: {
width: '60%',
paddingVertical: 6,
paddingHorizontal: 8,
},
// utils
doubleDivider: {
width: '100%',
height: 6,
borderTop: '2px solid black',
borderBottom: '2px solid black',
}, },
}); });
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 = const isLatestApprovalRejected =
expense?.latest_approval?.action === 'REJECTED'; expense?.latest_approval?.action === 'REJECTED';
const isExpenseRealized = const isExpenseRealized =
expense?.latest_approval?.step_number && expense?.latest_approval?.step_number &&
expense?.latest_approval.step_number >= 5; expense?.latest_approval.step_number >= 5;
const realizationStatus = isExpenseRealized const realizationStatus = isExpenseRealized
? 'Sudah Realisasi' ? 'Sudah Realisasi'
: 'Belum Realisasi'; : 'Belum Realisasi';
const rows = [ return [
{ label: 'Nomor PO', value: expense?.po_number }, { label: 'Nomor PO', value: expense?.po_number || '-' },
{ label: 'Nomor Referensi', value: expense?.reference_number }, { label: 'Nomor Referensi', value: expense?.reference_number || '-' },
{ {
label: 'Kategori', label: 'Kategori',
value: value:
@@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? 'Biaya Operasional' ? 'Biaya Operasional'
: expense?.category === 'NON-BOP' : expense?.category === 'NON-BOP'
? 'Non Biaya Operasional' ? 'Non Biaya Operasional'
: '', : '-',
}, },
{ label: 'Lokasi', value: expense?.location.name }, { label: 'Lokasi', value: expense?.location?.name || '-' },
{ {
label: 'Kandang', label: 'Kandang',
value: value:
@@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
.join(', ') .join(', ')
: '-', : '-',
}, },
{ label: 'Vendor', value: expense?.supplier.name }, { label: 'Vendor', value: expense?.supplier?.name || '-' },
{ {
label: 'Tanggal Transaksi', label: 'Tanggal Transaksi',
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'), value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
@@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? formatDate(expense?.realization_date, 'DD MMMM YYYY') ? 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', label: 'Nominal Biaya',
value: formatCurrency( value: formatCurrency(
expense?.latest_approval.step_number === 5 || expense?.latest_approval?.step_number === 5 ||
expense?.latest_approval.step_number === 6 expense?.latest_approval?.step_number === 6
? (expense?.total_realisasi ?? 0) ? (expense?.total_realisasi ?? 0)
: (expense?.total_pengajuan ?? 0) : (expense?.total_pengajuan ?? 0)
), ),
@@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
label: 'Status Biaya', label: 'Status Biaya',
value: isLatestApprovalRejected value: isLatestApprovalRejected
? 'Ditolak' ? '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 ( return (
<Document> <Document>
<Page style={ExpensePDFStyle.page}> <Page style={styles.page} size='A4'>
<View> {/* Title Section */}
<View style={ExpensePDFStyle.companyInfoHeader}> <View style={styles.titleSection}>
<Image <PdfTypography size='h1' variant='primary'>
style={ExpensePDFStyle.companyLogo} Laporan{' '}
src='/assets/img/lti-logo.png' {expense?.category === 'BOP'
/> ? 'Biaya Operasional'
: 'Non-Biaya Operasional'}
<Text style={ExpensePDFStyle.companyInfoHeaderDate}> </PdfTypography>
{formatDate(Date.now(), 'DD MMMM YYYY')} <PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
</Text> <View style={styles.parameterContainer}>
</View> <PdfParamBadge>
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
<View> </PdfParamBadge>
<Text style={ExpensePDFStyle.companyName}> <PdfParamBadge>
PT LUMBUNG TELUR INDONESIA Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text> </PdfParamBadge>
<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} />
</View> </View>
</View> </View>
<Text style={ExpensePDFStyle.title}> {/* Info Table Section */}
Laporan{' '} <View style={styles.infoTableSection}>
{expense?.category === 'BOP' <Text style={styles.infoTableTitle}>Informasi Biaya</Text>
? 'Biaya Operasional' <PdfTable columns={getInfoTableColumns()} data={infoRows} />
: '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>
))}
</View> </View>
{/* Detail expense request */} {/* Rincian Pengajuan Section */}
<View <View style={styles.tableSection}>
minPresenceAhead={80} <Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
style={ExpensePDFStyle.expenseDetailContainer} {kandangs.length === 0 ? (
> <Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
<Text style={ExpensePDFStyle.expenseDetailTitle}> ) : (
Rincian Pengajuan Biaya Operasional kandangs.map((kandang, idx) => {
</Text> const pengajuans = kandang.pengajuans || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { return (
let expenseRequestTotal = 0; <View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
kandangExpense.pengajuans?.forEach( {idx + 1}) {kandangName}
(item) => (expenseRequestTotal += item.qty * item.price) </PdfTypography>
); {pengajuans.length > 0 ? (
<PdfTable
return ( columns={getPengajuanColumns()}
<View data={pengajuans}
key={kandangExpenseIdx} showFooter={true}
style={ExpensePDFStyle.kandangExpenseContainer} footerLabel='Total'
> />
<Text style={ExpensePDFStyle.kandangExpenseTitle}> ) : (
{kandangExpense.kandang_id && kandangExpense.name <Text style={styles.emptyText}>
? `Biaya ${kandangExpense.name}` Tidak ada item pengajuan untuk kandang ini.
: `Biaya ${expense?.location.name || 'Umum'}`} </Text>
</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>
</View> </View>
</View> );
); })
})} )}
</View> </View>
{/* Detail expense realization */} {/* Rincian Realisasi Section */}
<View <View style={styles.tableSection}>
minPresenceAhead={80} <Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
style={ExpensePDFStyle.expenseDetailContainer} {kandangs.length === 0 ? (
> <Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
<Text style={ExpensePDFStyle.expenseDetailTitle}> ) : (
Rincian Realisasi Biaya Operasional kandangs.map((kandang, idx) => {
</Text> const realisasi = kandang.realisasi || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { return (
let expenseRealizationTotal = 0; <View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
kandangExpense.realisasi?.forEach( {idx + 1}) {kandangName}
(item) => (expenseRealizationTotal += item.qty * item.price) </PdfTypography>
); {realisasi.length > 0 ? (
<PdfTable
return ( columns={getRealisasiColumns()}
<View data={realisasi}
key={kandangExpenseIdx} showFooter={true}
style={ExpensePDFStyle.kandangExpenseContainer} footerLabel='Total'
> />
<Text style={ExpensePDFStyle.kandangExpenseTitle}> ) : (
{kandangExpense.kandang_id && kandangExpense.name <Text style={styles.emptyText}>
? `Biaya ${kandangExpense.name}` Tidak ada item realisasi untuk kandang ini.
: `Biaya ${expense?.location.name || 'Umum'}`} </Text>
</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>
</View> </View>
</View> );
); })
})} )}
</View> </View>
<View style={ExpensePDFStyle.footer} fixed> <PdfPageNumber />
<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>
</Page> </Page>
</Document> </Document>
); );
@@ -315,7 +315,7 @@ const InventoryAdjustmentTable = () => {
accessorFn: (row) => row.created_user?.name ?? '-', accessorFn: (row) => row.created_user?.name ?? '-',
}, },
], ],
[tableFilterState.pageSize, tableFilterState.page] []
); );
const updateSortingFilter = useCallback( const updateSortingFilter = useCallback(
@@ -323,6 +323,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, },
}); });
const { setFieldValue, setFieldTouched, setFieldError } = formik;
const prevSourceWarehouseIdRef = useRef<number | null>( const prevSourceWarehouseIdRef = useRef<number | null>(
formik.values.source_warehouse_id formik.values.source_warehouse_id
); );
@@ -336,14 +338,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
prevSourceWarehouseId !== currentSourceWarehouseId && prevSourceWarehouseId !== currentSourceWarehouseId &&
prevSourceWarehouseId !== null prevSourceWarehouseId !== null
) { ) {
formik.setFieldValue('products', [ setFieldValue('products', [
{ {
product: null, product: null,
product_id: 0, product_id: 0,
product_qty: '', product_qty: '',
}, },
]); ]);
formik.setFieldTouched('products', false); setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map( const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({ (delivery: DeliverySchema) => ({
@@ -357,12 +359,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}) })
); );
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false); setFieldTouched('deliveries', false);
} }
prevSourceWarehouseIdRef.current = currentSourceWarehouseId; 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) ===== // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
const { const {
@@ -455,9 +462,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== EVENT HANDLERS ===== // ===== EVENT HANDLERS =====
const handleTransferDateChange = useCallback( const handleTransferDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value); setFieldValue('transfer_date', e.target.value);
}, },
[] [setFieldValue]
); );
const handleSourceWarehouseChange = useCallback( const handleSourceWarehouseChange = useCallback(
@@ -477,14 +484,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
formik.setFieldTouched('source_warehouse', true); setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val); setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true); setFieldTouched('source_warehouse_id', true);
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId); setFieldValue('source_warehouse_id', newSourceWarehouseId);
}, },
[ [
formik.values.destination_warehouse_id, formik.values.destination_warehouse_id,
formik.values.destination_warehouse, formik.values.destination_warehouse,
setFieldTouched,
setFieldValue,
] ]
); );
@@ -505,15 +514,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
formik.setFieldTouched('destination_warehouse', true); setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val); setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true); setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue( setFieldValue('destination_warehouse_id', newDestinationWarehouseId);
'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(() => { const addProduct = useCallback(() => {
@@ -525,15 +536,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
product_qty: '', product_qty: '',
}, },
]; ];
formik.setFieldValue('products', newProducts); setFieldValue('products', newProducts);
}, [formik.values.products]); }, [formik.values.products, setFieldValue]);
const removeProduct = useCallback( const removeProduct = useCallback(
(i: number) => { (i: number) => {
const updatedProducts = formik.values.products?.filter( const updatedProducts = formik.values.products?.filter(
(_, idx) => idx !== i (_, idx) => idx !== i
); );
formik.setFieldValue('products', updatedProducts); setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
@@ -542,7 +553,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setProductQtyErrorShown(false); setProductQtyErrorShown(false);
} }
}, },
[formik.values.products, productQtyErrorShown, setSelectedProducts] [
formik.values.products,
productQtyErrorShown,
setSelectedProducts,
setFieldValue,
]
); );
const bulkRemoveProduct = useCallback(() => { const bulkRemoveProduct = useCallback(() => {
@@ -550,26 +566,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.products?.filter( formik.values.products?.filter(
(_, idx) => !selectedProducts.includes(idx) (_, idx) => !selectedProducts.includes(idx)
) ?? []; ) ?? [];
formik.setFieldValue('products', updatedProducts); setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
if (productQtyErrorShown) { if (productQtyErrorShown) {
toast.dismiss(); toast.dismiss();
setProductQtyErrorShown(false); setProductQtyErrorShown(false);
} }
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]); }, [
selectedProducts,
setSelectedProducts,
productQtyErrorShown,
setFieldValue,
formik.values.products,
]);
const handleProductChange = useCallback( const handleProductChange = useCallback(
(idx: number, val: OptionType | OptionType[] | null) => { (idx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`products.${idx}.product`, true); setFieldTouched(`products.${idx}.product`, true);
formik.setFieldValue(`products.${idx}.product`, val); setFieldValue(`products.${idx}.product`, val);
formik.setFieldTouched(`products.${idx}.product_id`, true); setFieldTouched(`products.${idx}.product_id`, true);
formik.setFieldValue( setFieldValue(
`products.${idx}.product_id`, `products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value (val as ProductWarehouseOptionType)?.value
); );
}, },
[] [setFieldTouched, setFieldValue]
); );
const handleProductSelectAllChange = useCallback( const handleProductSelectAllChange = useCallback(
@@ -596,7 +618,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
const addDelivery = useCallback(() => { const addDelivery = useCallback(() => {
formik.setFieldValue('deliveries', [ setFieldValue('deliveries', [
...(formik.values.deliveries || []), ...(formik.values.deliveries || []),
{ {
delivery_cost: '', delivery_cost: '',
@@ -615,14 +637,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}, },
]); ]);
}, [formik.values.deliveries]); }, [formik.values.deliveries, setFieldValue]);
const removeDelivery = useCallback( const removeDelivery = useCallback(
(i: number) => { (i: number) => {
const updatedDeliveries = formik.values.deliveries?.filter( const updatedDeliveries = formik.values.deliveries?.filter(
(_, idx) => idx !== i (_, idx) => idx !== i
); );
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
@@ -631,7 +653,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setDeliveryQtyErrorShown(false); setDeliveryQtyErrorShown(false);
} }
}, },
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries] [
formik.values.deliveries,
deliveryQtyErrorShown,
setSelectedDeliveries,
setFieldValue,
]
); );
const bulkRemoveDelivery = useCallback(() => { const bulkRemoveDelivery = useCallback(() => {
@@ -639,7 +666,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.deliveries?.filter( formik.values.deliveries?.filter(
(_, idx) => !selectedDeliveries.includes(idx) (_, idx) => !selectedDeliveries.includes(idx)
) ?? []; ) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
if (deliveryQtyErrorShown) { if (deliveryQtyErrorShown) {
@@ -647,10 +674,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setDeliveryQtyErrorShown(false); setDeliveryQtyErrorShown(false);
} }
}, [ }, [
formik,
selectedDeliveries, selectedDeliveries,
setSelectedDeliveries, setSelectedDeliveries,
deliveryQtyErrorShown, deliveryQtyErrorShown,
setFieldValue,
formik.values.deliveries,
]); ]);
const handleDeliverySelectAllChange = useCallback( const handleDeliverySelectAllChange = useCallback(
@@ -680,34 +708,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const handleDeliveryProductChange = useCallback( const handleDeliveryProductChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => { (deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched( setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true);
`deliveries.${deliveryIdx}.products.0.product`, setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
true setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true);
); setFieldValue(
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${deliveryIdx}.products.0.product_id`, `deliveries.${deliveryIdx}.products.0.product_id`,
(val as OptionType)?.value (val as OptionType)?.value
); );
}, },
[] [setFieldTouched, setFieldValue]
); );
const handleDeliverySupplierChange = useCallback( const handleDeliverySupplierChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => { (deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true); setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val); setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true); setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
formik.setFieldValue( setFieldValue(
`deliveries.${deliveryIdx}.supplier_id`, `deliveries.${deliveryIdx}.supplier_id`,
(val as OptionType)?.value (val as OptionType)?.value
); );
}, },
[] [setFieldTouched, setFieldValue]
); );
const handleDeliveryDocumentChange = useCallback( const handleDeliveryDocumentChange = useCallback(
@@ -719,15 +741,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
e.target.value = ''; e.target.value = '';
return; return;
} }
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file); setFieldValue(`deliveries.${deliveryIdx}.document`, file);
} }
}, },
[] [setFieldValue]
); );
const handleDeliveryCostChange = useCallback( const handleDeliveryCostChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx]; const delivery = formik.values.deliveries?.[idx];
if (delivery) { if (delivery) {
@@ -737,21 +759,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (productQty > 0 && value > 0) { if (productQty > 0 && value > 0) {
const perItem = value / productQty; const perItem = value / productQty;
formik.setFieldValue( setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) { } 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( const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => { (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]; const delivery = formik.values.deliveries?.[idx];
if (delivery) { if (delivery) {
@@ -761,13 +780,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (productQty > 0 && value > 0) { if (productQty > 0 && value > 0) {
const totalCost = value * productQty; const totalCost = value * productQty;
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
} else if (value === 0) { } 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( const handleDeliveryCostChangeWrapper = useCallback(
@@ -1044,12 +1063,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return !validateDeliveryQty(deliveryIdx, productIdx, qty); return !validateDeliveryQty(deliveryIdx, productIdx, qty);
}) })
) ?? []), ) ?? []),
[ [formik.values.deliveries, validateDeliveryQty, type]
formik.values.deliveries,
formik.values.products,
validateDeliveryQty,
type,
]
); );
const hasInvalidQty = useMemo( const hasInvalidQty = useMemo(
@@ -1066,6 +1080,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
}, [formik.values.products, getProductQtyError, type]); }, [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 ===== // ===== EFFECTS =====
useEffect(() => { useEffect(() => {
formik.values.deliveries?.forEach((delivery, idx) => { formik.values.deliveries?.forEach((delivery, idx) => {
@@ -1082,36 +1117,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (deliveryCost > 0 && productQty > 0) { if (deliveryCost > 0 && productQty > 0) {
const perItem = deliveryCost / productQty; const perItem = deliveryCost / productQty;
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) { if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
formik.setFieldValue( setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} }
} else if (deliveryCostPerItem > 0 && productQty > 0) { } else if (deliveryCostPerItem > 0 && productQty > 0) {
const totalCost = deliveryCostPerItem * productQty; const totalCost = deliveryCostPerItem * productQty;
if (Math.abs(deliveryCost - totalCost) > 0.01) { if (Math.abs(deliveryCost - totalCost) > 0.01) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
} }
} }
}); });
}, [ }, [deliveryCostDepString, setFieldValue, formik.values.deliveries]);
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('|'),
]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -1121,7 +1136,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
!isInitialized !isInitialized
) { ) {
if (formik.values.products.length === 0) { if (formik.values.products.length === 0) {
formik.setFieldValue('products', [ setFieldValue('products', [
{ {
product: null, product: null,
product_id: 0, product_id: 0,
@@ -1130,7 +1145,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
]); ]);
} }
if (formik.values.deliveries.length === 0) { if (formik.values.deliveries.length === 0) {
formik.setFieldValue('deliveries', [ setFieldValue('deliveries', [
{ {
delivery_cost: undefined, delivery_cost: undefined,
delivery_cost_per_item: undefined, delivery_cost_per_item: undefined,
@@ -1152,7 +1167,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
} }
setIsInitialized(true); 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(() => { useEffect(() => {
if ( if (
@@ -1161,7 +1183,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.source_warehouse_id === formik.values.source_warehouse_id ===
formik.values.destination_warehouse_id formik.values.destination_warehouse_id
) { ) {
formik.setFieldError( setFieldError(
'destination_warehouse_id', 'destination_warehouse_id',
'Gudang tujuan tidak boleh sama dengan gudang asal!' 'Gudang tujuan tidak boleh sama dengan gudang asal!'
); );
@@ -1170,13 +1192,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.errors.destination_warehouse_id === formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!' '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.source_warehouse_id,
formik.values.destination_warehouse_id, formik.values.destination_warehouse_id,
formik.errors.destination_warehouse_id, formik.errors.destination_warehouse_id,
setFieldError,
]); ]);
useEffect(() => { useEffect(() => {
@@ -1212,29 +1235,37 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (hasChanges) { 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(() => { useEffect(() => {
if (productQtyErrorShown) { if (productQtyErrorShown) {
toast.dismiss(); toast.dismiss();
setProductQtyErrorShown(false); 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(() => { useEffect(() => {
if (deliveryQtyErrorShown) { if (deliveryQtyErrorShown) {
toast.dismiss(); toast.dismiss();
setDeliveryQtyErrorShown(false); setDeliveryQtyErrorShown(false);
} }
}, [ }, [deliveryProductQtyDepString, productQtyDepString, deliveryQtyErrorShown]);
formik.values.deliveries
?.map((d) => d.products.map((p) => p.product_qty).join(','))
.join('|'),
formik.values.products?.map((p) => p.product_qty).join(','),
]);
useEffect(() => { useEffect(() => {
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') { if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
@@ -536,9 +536,13 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
formModal.closeModal(); formModal.closeModal();
}; };
const hasLoadedInitialValues = useRef(false);
useEffect(() => { useEffect(() => {
const getFilledInitialValues = async () => { const getFilledInitialValues = async () => {
if (marketingId && isResponseSuccess(marketing)) { if (marketingId && isResponseSuccess(marketing)) {
if (hasLoadedInitialValues.current) return;
hasLoadedInitialValues.current = true;
const filledInitialValues = await getFilledMarketingFormInitialValues( const filledInitialValues = await getFilledMarketingFormInitialValues(
marketing.data marketing.data
); );
@@ -582,9 +586,15 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
setFormErrorMessage(''); setFormErrorMessage('');
}, [step]); }, [step]);
// sync delivery order values to formik const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues);
useEffect(() => { useEffect(() => {
formik.setFieldValue('delivery_order', deliveryOrderValues); if (
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
JSON.stringify(deliveryOrderValues)
) {
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
formik.setFieldValue('delivery_order', deliveryOrderValues);
}
}, [deliveryOrderValues]); }, [deliveryOrderValues]);
const grandTotal = useMemo(() => { const grandTotal = useMemo(() => {
@@ -226,11 +226,6 @@ const MarketingTable = () => {
confirmationModal.openModal(); confirmationModal.openModal();
}; };
const productsClickHandler = (item: Marketing) => {
setSelectedItem(item);
productsModal.openModal();
};
const deleteMarketingHandler = async () => { const deleteMarketingHandler = async () => {
const deleteMarketingRes = await MarketingApi.delete( const deleteMarketingRes = await MarketingApi.delete(
selectedItem?.id as number selectedItem?.id as number
@@ -450,6 +445,11 @@ const MarketingTable = () => {
accessorKey: 'marketing_products.length', accessorKey: 'marketing_products.length',
header: 'Product Details', header: 'Product Details',
cell: (props) => { cell: (props) => {
const productsClickHandler = (item: Marketing) => {
setSelectedItem(item);
productsModal.openModal();
};
if (props?.row?.original?.sales_order?.length) { if (props?.row?.original?.sales_order?.length) {
if (props?.row?.original?.sales_order?.length > 1) { if (props?.row?.original?.sales_order?.length > 1) {
return ( return (
@@ -504,7 +504,7 @@ const MarketingTable = () => {
}, },
}, },
]; ];
}, []); }, [deleteModal, deliveryModal, setSelectedItem, productsModal]);
return ( return (
<> <>
@@ -458,9 +458,13 @@ const SalesOrderFormModal = ({
); );
}, [memoSalesOrder]); }, [memoSalesOrder]);
const hasLoadedInitialValues = useRef(false);
useEffect(() => { useEffect(() => {
const getFilledInitialValues = async () => { const getFilledInitialValues = async () => {
if (marketingId && isResponseSuccess(marketing)) { if (marketingId && isResponseSuccess(marketing)) {
if (hasLoadedInitialValues.current) return;
hasLoadedInitialValues.current = true;
const filledInitialValues = await getFilledMarketingFormInitialValues( const filledInitialValues = await getFilledMarketingFormInitialValues(
marketing.data marketing.data
); );
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { import {
DeliveryOrderProductFormValues, DeliveryOrderProductFormValues,
DeliveryOrderProductSchema, DeliveryOrderProductSchema,
@@ -224,6 +224,8 @@ const DeliveryOrderProductForm = ({
}, },
}); });
const { resetForm } = formik;
const hasWeekField = useMemo(() => { const hasWeekField = useMemo(() => {
const marketingType = formik.values.marketing_type?.value?.toLowerCase(); const marketingType = formik.values.marketing_type?.value?.toLowerCase();
if (marketingType === 'ayam_pullet') { if (marketingType === 'ayam_pullet') {
@@ -243,9 +245,9 @@ const DeliveryOrderProductForm = ({
return false; return false;
}, [formik.values.marketing_product, formik.values.marketing_type]); }, [formik.values.marketing_product, formik.values.marketing_type]);
const handleResetForm = () => { const handleResetForm = useCallback(() => {
setFormErrorMessage(''); setFormErrorMessage('');
formik.resetForm({ resetForm({
values: { values: {
delivery_date: '', delivery_date: '',
vehicle_number: '', vehicle_number: '',
@@ -269,17 +271,20 @@ const DeliveryOrderProductForm = ({
}, },
}); });
// setSelectedProduct(null); // setSelectedProduct(null);
}; }, [resetForm]);
const handleBlurField = (field: string) => { const handleBlurField = useCallback(
setCurrentInput(field); (field: string) => {
setCurrentInput(field);
handleMarketingCalculation(field, { handleMarketingCalculation(field, {
values: formik.values, values: formik.values,
setFieldValue: formik.setFieldValue, setFieldValue: formik.setFieldValue,
hasSisaBerat, 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) // 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 = ( const handleFieldChange = (
@@ -324,8 +329,12 @@ const DeliveryOrderProductForm = ({
const { setValues: setFormikValues } = formik; const { setValues: setFormikValues } = formik;
const processedInitialValuesRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
if (initialValues) { if (initialValues) {
if (processedInitialValuesRef.current === initialValues.id) return;
processedInitialValuesRef.current = initialValues.id as number;
if (!Boolean(initialValues.qty)) { if (!Boolean(initialValues.qty)) {
handleResetForm(); handleResetForm();
} else { } else {
@@ -338,7 +347,7 @@ const DeliveryOrderProductForm = ({
} }
} }
} }
}, [initialValues]); }, [handleResetForm, initialValues, setFormikValues]);
// ===== Formik Error List ===== // ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList( const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
@@ -356,8 +365,10 @@ const DeliveryOrderProductForm = ({
); );
useEffect(() => { useEffect(() => {
handleBlurField('week'); if (formik.values.week) {
}, [formik.values.week]); handleBlurField('week');
}
}, [formik.values.week, handleBlurField]);
return ( return (
<> <>
@@ -5,7 +5,7 @@ import {
SalesOrderProductFormValues, SalesOrderProductFormValues,
SalesOrderProductSchema, SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; } 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 { OptionType, useSelect } from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { WarehouseApi } from '@/services/api/master-data'; import { WarehouseApi } from '@/services/api/master-data';
@@ -240,15 +240,18 @@ const SalesOrderProductForm = ({
}); });
}; };
const handleBlurField = (field: string) => { const handleBlurField = useCallback(
setCurrentInput(field); (field: string) => {
setCurrentInput(field);
handleMarketingCalculation(field, { handleMarketingCalculation(field, {
values: formik.values, values: formik.values,
setFieldValue: formik.setFieldValue, setFieldValue: formik.setFieldValue,
hasSisaBerat, 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) // 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 = ( const handleFieldChange = (
@@ -307,8 +310,10 @@ const SalesOrderProductForm = ({
); );
useEffect(() => { useEffect(() => {
handleBlurField('week'); if (formik.values.week) {
}, [formik.values.week]); handleBlurField('week');
}
}, [formik.values.week, handleBlurField]);
return ( return (
<> <>
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper'; import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
interface DeliveryOrderExportProps { interface DeliveryOrderExportProps {
data?: Marketing; data?: Marketing;
@@ -20,9 +19,6 @@ const DeliveryOrderExport = ({
}: DeliveryOrderExportProps) => { }: DeliveryOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data; const salesData = data;
const searchParams = useSearchParams();
const action = searchParams.get('action');
const id = searchParams.get('id');
const handleDownloadPDF = async () => { const handleDownloadPDF = async () => {
if (!salesData) { if (!salesData) {
@@ -53,7 +49,6 @@ const DeliveryOrderExport = ({
toast.error('Failed to generate PDF. Please try again.'); toast.error('Failed to generate PDF. Please try again.');
} finally { } finally {
setIsGeneratingPDF(false); setIsGeneratingPDF(false);
window.location.href = `/marketing?action=${action}&id=${id}`;
} }
}; };
@@ -92,7 +87,7 @@ const PDFDocument = ({
return ( return (
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0 deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
); );
}, []); }, [deliveryOrder.deliveries]);
return ( return (
<Document> <Document>
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
import { formatDate, formatNumber } from '@/lib/helper'; import { formatDate, formatNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
interface SalesOrderExportProps { interface SalesOrderExportProps {
data?: Marketing; data?: Marketing;
@@ -16,9 +15,6 @@ interface SalesOrderExportProps {
const SalesOrderExport = ({ data }: SalesOrderExportProps) => { const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data; const salesData = data;
const searchParams = useSearchParams();
const action = searchParams.get('action');
const id = searchParams.get('id');
const handleDownloadPDF = async () => { const handleDownloadPDF = async () => {
if (!salesData) { if (!salesData) {
@@ -47,7 +43,6 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
toast.error('Failed to generate PDF. Please try again.'); toast.error('Failed to generate PDF. Please try again.');
} finally { } finally {
setIsGeneratingPDF(false); setIsGeneratingPDF(false);
window.location.href = `/marketing?action=${action}&id=${id}`;
} }
}; };
@@ -44,7 +44,9 @@ const ChickinFormKandang = ({
const afterSubmitFormChickin = () => { const afterSubmitFormChickin = () => {
setOpenChickin(true); setOpenChickin(true);
afterSubmit && afterSubmit(); if (afterSubmit) {
afterSubmit();
}
refreshApprovals(); refreshApprovals();
}; };
@@ -40,7 +40,9 @@ const ChickinLogsView = ({
toast.error(approveChickinRes?.message as string); toast.error(approveChickinRes?.message as string);
setChickinErrorMessage(approveChickinRes?.message as string); setChickinErrorMessage(approveChickinRes?.message as string);
} }
afterSubmit && afterSubmit(); if (afterSubmit) {
afterSubmit();
}
}); });
}; };
@@ -82,7 +82,7 @@ const TransferToLayingDetailModal = () => {
if (modalAction === 'detail') { if (modalAction === 'detail') {
detailModal.openModal(); detailModal.openModal();
} }
}, [modalAction]); }, [modalAction, detailModal]);
return ( return (
<Modal <Modal
@@ -463,7 +463,7 @@ const TransferToLayingsTable = () => {
updateFilter('filter_by', ''); updateFilter('filter_by', '');
updateFilter('sort_by', ''); updateFilter('sort_by', '');
} }
}, [sorting]); }, [sorting, updateFilter]);
return ( return (
<> <>
@@ -60,25 +60,6 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`); 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(() => { useEffect(() => {
if ( if (
shouldFetchDetails && shouldFetchDetails &&
@@ -202,6 +183,25 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
} }
if (id === 'document-name') { 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 ( return (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span>{valueMap[id]}</span> <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(() => { const samplingTableData: DetailOptionType[] = useMemo(() => {
@@ -301,8 +301,93 @@ export function DailyChecklistContent() {
checkAndLoadChecklist(); checkAndLoadChecklist();
}, [date, kandangId, selectedCategory]); }, [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 // Load activities and tasks when phases change
useEffect(() => { 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 () => { const loadActivitiesAndTasks = async () => {
if (!dailyChecklistId || selectedPhaseIds.length === 0) { if (!dailyChecklistId || selectedPhaseIds.length === 0) {
setActivitiesByPhase({}); setActivitiesByPhase({});
@@ -377,87 +462,6 @@ export function DailyChecklistContent() {
loadActivitiesAndTasks(); loadActivitiesAndTasks();
}, [dailyChecklistId, selectedPhaseIds]); }, [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 // Phase selection modal
const handleAddPhase = () => { const handleAddPhase = () => {
if (!selectedCategory) { if (!selectedCategory) {
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import * as React from 'react'; import * as React from 'react';
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
@@ -137,15 +137,7 @@ export function DetailDailyChecklistContent() {
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
useEffect(() => { const fetchChecklistDetail = useCallback(async () => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [checklistId]);
const fetchChecklistDetail = async () => {
if (!checklistId) { if (!checklistId) {
console.warn('checklistId missing'); console.warn('checklistId missing');
setLoading(false); setLoading(false);
@@ -320,7 +312,15 @@ export function DetailDailyChecklistContent() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [checklistId, router]);
useEffect(() => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [checklistId, fetchChecklistDetail, router]);
const groupDetailData = (rows: ChecklistDetailRow[]) => { const groupDetailData = (rows: ChecklistDetailRow[]) => {
// Group by phase_id // Group by phase_id
+14
View File
@@ -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 { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store';
import { omit } from '@/lib/helper';
import { StateCreator } from 'zustand'; import { StateCreator } from 'zustand';
export const createTabActionsSlice: StateCreator< export const createTabActionsSlice: StateCreator<
@@ -20,10 +21,9 @@ export const createTabActionsSlice: StateCreator<
})), })),
clearTabActions: (tabId) => clearTabActions: (tabId) =>
set((state) => { set((state) => ({
const { [tabId]: _, ...rest } = state.tabActions; tabActions: omit(state.tabActions, tabId),
return { tabActions: rest }; })),
}),
clearAllTabActions: () => set({ tabActions: {} }), clearAllTabActions: () => set({ tabActions: {} }),
}); });