Compare commits

..

15 Commits

Author SHA1 Message Date
rstubryan 3b9bd3c5bd Revert "refactor(FE): Prevent adding recordings for kandangs in transition"
This reverts commit 9dc30c1f58.
2026-03-09 03:50:33 +07:00
rstubryan 9dc30c1f58 refactor(FE): Prevent adding recordings for kandangs in transition 2026-03-09 03:35:03 +07:00
rstubryan 671fd72141 refactor(FE): Make stock fields optional during transition to laying 2026-03-09 03:32:44 +07:00
rstubryan d236138aa7 refactor(FE): Update recording editability logic and extend
BaseRecording type
2026-03-09 03:18:41 +07:00
Adnan Zahir 3042b54577 Merge branch 'fix/product-select-include-all-param' into 'development'
fix: add include all param to adjustment stock products select

See merge request mbugroup/lti-web-client!343
2026-03-09 00:51:43 +07:00
Adnan Zahir e5a686e5ee Merge branch 'hotfix/adjustment-fifo-stock-ttl' into 'development'
[HOTFIX/FE] FIFO Stock Adjustment for TTL, Recording and Project Flock (Chickin)

See merge request mbugroup/lti-web-client!342
2026-03-09 00:50:11 +07:00
Adnan Zahir 37d5a6b675 fix: add include all param to adjustment stock products select 2026-03-09 00:49:39 +07:00
rstubryan 2ff32094ce chore(FE): Fix inconsistent indentation in ChickLogsView and
RecordingForm
2026-03-08 22:13:26 +07:00
rstubryan 7207f1ba75 refactor(FE): Add isRecordingEditable check to detail actions 2026-03-08 22:12:29 +07:00
rstubryan 41d2e8737b feat(FE): Add chickin delete functionality with modal confirmation 2026-03-08 21:54:55 +07:00
rstubryan b2016314f5 refactor(FE): Restrict edit and delete actions based on recording status 2026-03-08 16:40:36 +07:00
rstubryan 7366d6490c refactor(FE): Validate transferToLayingId before fetching data 2026-03-08 16:34:59 +07:00
rstubryan e5e9b517fd refactor(FE): Update button visibility logic in TransferToLayingsTable 2026-03-08 16:17:20 +07:00
rstubryan b6629b0bbb refactor(FE): Set maxSourceQuantity in edit mode using existing data 2026-03-08 16:12:08 +07:00
rstubryan bac6766fa2 refactor(FE): Refactor transfer logic to use maxSourceQuantity state 2026-03-08 16:06:13 +07:00
31 changed files with 1201 additions and 747 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, initialValues?.id] [router]
); );
const updateExpenseHandler = useCallback( const updateExpenseHandler = useCallback(
@@ -178,14 +178,12 @@ 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) => {
@@ -416,7 +414,6 @@ 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
@@ -460,7 +457,6 @@ 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'>
+555 -232
View File
@@ -1,154 +1,212 @@
'use client'; 'use client';
import React from 'react'; import {
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer'; Document,
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 styles = StyleSheet.create({ const ExpensePDFStyle = StyleSheet.create({
page: { page: {
fontSize: 10, paddingTop: 24,
fontFamily: 'Helvetica', paddingBottom: 64,
padding: 20, paddingHorizontal: 32,
backgroundColor: '#FFFFFF',
}, },
titleSection: {
marginBottom: 10, companyInfoHeader: {
}, width: '100%',
parameterContainer: { display: 'flex',
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8, marginBottom: 8,
}, },
infoTableSection: { companyLogo: {
marginBottom: 12, width: 64,
height: 'auto',
}, },
infoTableTitle: { companyInfoHeaderDate: {
fontSize: 10, paddingTop: 8,
fontSize: 12,
},
companyName: {
fontSize: 12,
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 6, marginBottom: 4,
color: '#333',
}, },
tableSection: { companyAddress: {
marginBottom: 12,
},
tableTitle: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
color: '#333',
},
emptyText: {
fontSize: 8, fontSize: 8,
color: '#666', maxWidth: 400,
fontStyle: 'italic', marginBottom: 10,
},
title: {
marginTop: 16,
fontSize: 16,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
footer: {
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 32,
position: 'absolute',
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',
},
generalInfoTableValueText: {},
// expense detail table
expenseDetailContainer: {
width: '100%',
marginTop: 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: {
fontSize: 10,
},
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',
}, },
}); });
type ExpenseKandang = Expense['kandangs'][number]; const ExpensePDF = ({ expense }: ExpensePDFProps) => {
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';
return [ const rows = [
{ 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:
@@ -156,9 +214,9 @@ const getInfoTableRows = (expense?: Expense) => {
? '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:
@@ -169,7 +227,7 @@ const getInfoTableRows = (expense?: Expense) => {
.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'),
@@ -180,12 +238,12 @@ const getInfoTableRows = (expense?: Expense) => {
? 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)
), ),
@@ -205,136 +263,401 @@ const getInfoTableRows = (expense?: Expense) => {
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={styles.page} size='A4'> <Page style={ExpensePDFStyle.page}>
{/* Title Section */} <View>
<View style={styles.titleSection}> <View style={ExpensePDFStyle.companyInfoHeader}>
<PdfTypography size='h1' variant='primary'> <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} />
</View>
</View>
<Text style={ExpensePDFStyle.title}>
Laporan{' '} Laporan{' '}
{expense?.category === 'BOP' {expense?.category === 'BOP'
? 'Biaya Operasional' ? 'Biaya Operasional'
: 'Non-Biaya Operasional'} : 'Non-Biaya Operasional'}{' '}
</PdfTypography> {expense?.po_number}
<PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography> </Text>
<View style={styles.parameterContainer}>
<PdfParamBadge> {/* General info table */}
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} <View style={ExpensePDFStyle.generalInfoTable}>
</PdfParamBadge> {rows.map((row) => (
<PdfParamBadge> <View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} <View style={ExpensePDFStyle.generalInfoTableColLabel}>
</PdfParamBadge> <Text style={ExpensePDFStyle.generalInfoTableLabelText}>
{row.label}
</Text>
</View> </View>
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
<Text>:</Text>
</View>
<View style={ExpensePDFStyle.generalInfoTableColValue}>
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
{row.value}
</Text>
</View>
</View>
))}
</View> </View>
{/* Info Table Section */} {/* Detail expense request */}
<View style={styles.infoTableSection}> <View
<Text style={styles.infoTableTitle}>Informasi Biaya</Text> minPresenceAhead={80}
<PdfTable columns={getInfoTableColumns()} data={infoRows} /> style={ExpensePDFStyle.expenseDetailContainer}
</View> >
<Text style={ExpensePDFStyle.expenseDetailTitle}>
Rincian Pengajuan Biaya Operasional
</Text>
{/* Rincian Pengajuan Section */} {expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
<View style={styles.tableSection}> let expenseRequestTotal = 0;
<Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
{kandangs.length === 0 ? ( kandangExpense.pengajuans?.forEach(
<Text style={styles.emptyText}>Tidak ada data pengajuan.</Text> (item) => (expenseRequestTotal += item.qty * item.price)
) : ( );
kandangs.map((kandang, idx) => {
const pengajuans = kandang.pengajuans || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
return ( return (
<View key={idx} style={{ marginBottom: 12 }}> <View
<PdfTypography size='h3' style={{ paddingLeft: 12 }}> key={kandangExpenseIdx}
{idx + 1}) {kandangName} style={ExpensePDFStyle.kandangExpenseContainer}
</PdfTypography> >
{pengajuans.length > 0 ? ( <Text style={ExpensePDFStyle.kandangExpenseTitle}>
<PdfTable {kandangExpense.kandang_id && kandangExpense.name
columns={getPengajuanColumns()} ? `Biaya ${kandangExpense.name}`
data={pengajuans} : `Biaya ${expense?.location.name || 'Umum'}`}
showFooter={true} </Text>
footerLabel='Total'
/> <View style={ExpensePDFStyle.kandangExpenseTable}>
) : ( <View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<Text style={styles.emptyText}> <View
Tidak ada item pengajuan untuk kandang ini. 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> </Text>
)}
</View> </View>
);
})
)}
</View> </View>
{/* Rincian Realisasi Section */} {kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
<View style={styles.tableSection}> <View
<Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text> key={pengajuanIdx}
{kandangs.length === 0 ? ( style={ExpensePDFStyle.kandangExpenseTableRow}
<Text style={styles.emptyText}>Tidak ada data realisasi.</Text> >
) : ( <View
kandangs.map((kandang, idx) => { style={[
const realisasi = kandang.realisasi || []; ExpensePDFStyle.kandangExpenseTableColLabel,
const kandangName = ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
kandang.kandang_id && kandang.name ExpensePDFStyle.kandangExpenseTableColNonstock,
? kandang.name ]}
: expense?.location?.name || 'Umum'; >
<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>
{/* Detail expense realization */}
<View
minPresenceAhead={80}
style={ExpensePDFStyle.expenseDetailContainer}
>
<Text style={ExpensePDFStyle.expenseDetailTitle}>
Rincian Realisasi Biaya Operasional
</Text>
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
let expenseRealizationTotal = 0;
kandangExpense.realisasi?.forEach(
(item) => (expenseRealizationTotal += item.qty * item.price)
);
return ( return (
<View key={idx} style={{ marginBottom: 12 }}> <View
<PdfTypography size='h3' style={{ paddingLeft: 12 }}> key={kandangExpenseIdx}
{idx + 1}) {kandangName} style={ExpensePDFStyle.kandangExpenseContainer}
</PdfTypography> >
{realisasi.length > 0 ? ( <Text style={ExpensePDFStyle.kandangExpenseTitle}>
<PdfTable {kandangExpense.kandang_id && kandangExpense.name
columns={getRealisasiColumns()} ? `Biaya ${kandangExpense.name}`
data={realisasi} : `Biaya ${expense?.location.name || 'Umum'}`}
showFooter={true} </Text>
footerLabel='Total'
/> <View style={ExpensePDFStyle.kandangExpenseTable}>
) : ( <View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<Text style={styles.emptyText}> <View
Tidak ada item realisasi untuk kandang ini. 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> </Text>
)}
</View> </View>
);
})
)}
</View> </View>
<PdfPageNumber /> {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 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>
</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(
@@ -185,7 +185,9 @@ const InventoryAdjustmentForm = ({
isLoadingOptions: isLoadingProductOptions, isLoadingOptions: isLoadingProductOptions,
loadMore: loadMoreProducts, loadMore: loadMoreProducts,
rawData: products, rawData: products,
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search'); } = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
include_all: 'true',
});
const { const {
setInputValue: setDepletionProductInputValue, setInputValue: setDepletionProductInputValue,
@@ -323,8 +323,6 @@ 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
); );
@@ -338,14 +336,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
prevSourceWarehouseId !== currentSourceWarehouseId && prevSourceWarehouseId !== currentSourceWarehouseId &&
prevSourceWarehouseId !== null prevSourceWarehouseId !== null
) { ) {
setFieldValue('products', [ formik.setFieldValue('products', [
{ {
product: null, product: null,
product_id: 0, product_id: 0,
product_qty: '', product_qty: '',
}, },
]); ]);
setFieldTouched('products', false); formik.setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map( const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({ (delivery: DeliverySchema) => ({
@@ -359,17 +357,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}) })
); );
setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
setFieldTouched('deliveries', false); formik.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 {
@@ -462,9 +455,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>) => {
setFieldValue('transfer_date', e.target.value); formik.setFieldValue('transfer_date', e.target.value);
}, },
[setFieldValue] []
); );
const handleSourceWarehouseChange = useCallback( const handleSourceWarehouseChange = useCallback(
@@ -484,16 +477,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
setFieldTouched('source_warehouse', true); formik.setFieldTouched('source_warehouse', true);
setFieldValue('source_warehouse', val); formik.setFieldValue('source_warehouse', val);
setFieldTouched('source_warehouse_id', true); formik.setFieldTouched('source_warehouse_id', true);
setFieldValue('source_warehouse_id', newSourceWarehouseId); formik.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,
] ]
); );
@@ -514,17 +505,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
setFieldTouched('destination_warehouse', true); formik.setFieldTouched('destination_warehouse', true);
setFieldValue('destination_warehouse', val); formik.setFieldValue('destination_warehouse', val);
setFieldTouched('destination_warehouse_id', true); formik.setFieldTouched('destination_warehouse_id', true);
setFieldValue('destination_warehouse_id', newDestinationWarehouseId); formik.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(() => { const addProduct = useCallback(() => {
@@ -536,15 +525,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
product_qty: '', product_qty: '',
}, },
]; ];
setFieldValue('products', newProducts); formik.setFieldValue('products', newProducts);
}, [formik.values.products, setFieldValue]); }, [formik.values.products]);
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
); );
setFieldValue('products', updatedProducts); formik.setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
@@ -553,12 +542,7 @@ 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(() => {
@@ -566,32 +550,26 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.products?.filter( formik.values.products?.filter(
(_, idx) => !selectedProducts.includes(idx) (_, idx) => !selectedProducts.includes(idx)
) ?? []; ) ?? [];
setFieldValue('products', updatedProducts); formik.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) => {
setFieldTouched(`products.${idx}.product`, true); formik.setFieldTouched(`products.${idx}.product`, true);
setFieldValue(`products.${idx}.product`, val); formik.setFieldValue(`products.${idx}.product`, val);
setFieldTouched(`products.${idx}.product_id`, true); formik.setFieldTouched(`products.${idx}.product_id`, true);
setFieldValue( formik.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(
@@ -618,7 +596,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
const addDelivery = useCallback(() => { const addDelivery = useCallback(() => {
setFieldValue('deliveries', [ formik.setFieldValue('deliveries', [
...(formik.values.deliveries || []), ...(formik.values.deliveries || []),
{ {
delivery_cost: '', delivery_cost: '',
@@ -637,14 +615,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}, },
]); ]);
}, [formik.values.deliveries, setFieldValue]); }, [formik.values.deliveries]);
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
); );
setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
@@ -653,12 +631,7 @@ 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(() => {
@@ -666,7 +639,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.deliveries?.filter( formik.values.deliveries?.filter(
(_, idx) => !selectedDeliveries.includes(idx) (_, idx) => !selectedDeliveries.includes(idx)
) ?? []; ) ?? [];
setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
if (deliveryQtyErrorShown) { if (deliveryQtyErrorShown) {
@@ -674,11 +647,10 @@ 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(
@@ -708,28 +680,34 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const handleDeliveryProductChange = useCallback( const handleDeliveryProductChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => { (deliveryIdx: number, val: OptionType | OptionType[] | null) => {
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true); formik.setFieldTouched(
setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val); `deliveries.${deliveryIdx}.products.0.product`,
setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true); 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) => {
setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true); formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
setFieldValue(`deliveries.${deliveryIdx}.supplier`, val); formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true); formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
setFieldValue( formik.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(
@@ -741,15 +719,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
e.target.value = ''; e.target.value = '';
return; return;
} }
setFieldValue(`deliveries.${deliveryIdx}.document`, file); formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
} }
}, },
[setFieldValue] []
); );
const handleDeliveryCostChange = useCallback( const handleDeliveryCostChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
setFieldValue(`deliveries.${idx}.delivery_cost`, value); formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx]; const delivery = formik.values.deliveries?.[idx];
if (delivery) { if (delivery) {
@@ -759,18 +737,21 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (productQty > 0 && value > 0) { if (productQty > 0 && value > 0) {
const perItem = value / productQty; const perItem = value / productQty;
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem); formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) { } else if (value === 0) {
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0); formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
} }
} }
}, },
[formik.values.deliveries, setFieldValue] [formik.values.deliveries]
); );
const handleDeliveryCostPerItemChange = useCallback( const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value); formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
const delivery = formik.values.deliveries?.[idx]; const delivery = formik.values.deliveries?.[idx];
if (delivery) { if (delivery) {
@@ -780,13 +761,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;
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
} else if (value === 0) { } else if (value === 0) {
setFieldValue(`deliveries.${idx}.delivery_cost`, 0); formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
} }
} }
}, },
[formik.values.deliveries, setFieldValue] [formik.values.deliveries]
); );
const handleDeliveryCostChangeWrapper = useCallback( const handleDeliveryCostChangeWrapper = useCallback(
@@ -1063,7 +1044,12 @@ 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(
@@ -1080,27 +1066,6 @@ 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) => {
@@ -1117,16 +1082,36 @@ 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) {
setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem); formik.setFieldValue(
`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) {
setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); formik.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 (
@@ -1136,7 +1121,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
!isInitialized !isInitialized
) { ) {
if (formik.values.products.length === 0) { if (formik.values.products.length === 0) {
setFieldValue('products', [ formik.setFieldValue('products', [
{ {
product: null, product: null,
product_id: 0, product_id: 0,
@@ -1145,7 +1130,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
]); ]);
} }
if (formik.values.deliveries.length === 0) { if (formik.values.deliveries.length === 0) {
setFieldValue('deliveries', [ formik.setFieldValue('deliveries', [
{ {
delivery_cost: undefined, delivery_cost: undefined,
delivery_cost_per_item: undefined, delivery_cost_per_item: undefined,
@@ -1167,14 +1152,7 @@ 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 (
@@ -1183,7 +1161,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
) { ) {
setFieldError( formik.setFieldError(
'destination_warehouse_id', 'destination_warehouse_id',
'Gudang tujuan tidak boleh sama dengan gudang asal!' 'Gudang tujuan tidak boleh sama dengan gudang asal!'
); );
@@ -1192,14 +1170,13 @@ 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!'
) { ) {
setFieldError('destination_warehouse_id', undefined); formik.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(() => {
@@ -1235,37 +1212,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (hasChanges) { if (hasChanges) {
setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
} }
} }
}, [formik.values.products, formik.values.deliveries, setFieldValue]); }, [formik.values.products]);
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);
} }
}, [productQtyErrorShown]); }, [formik.values.products?.map((p) => p.product_qty).join(',')]);
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,13 +536,9 @@ 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
); );
@@ -586,15 +582,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
setFormErrorMessage(''); setFormErrorMessage('');
}, [step]); }, [step]);
const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues); // sync delivery order values to formik
useEffect(() => { useEffect(() => {
if (
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
JSON.stringify(deliveryOrderValues)
) {
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
formik.setFieldValue('delivery_order', deliveryOrderValues); formik.setFieldValue('delivery_order', deliveryOrderValues);
}
}, [deliveryOrderValues]); }, [deliveryOrderValues]);
const grandTotal = useMemo(() => { const grandTotal = useMemo(() => {
@@ -226,6 +226,11 @@ 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
@@ -445,11 +450,6 @@ 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,13 +458,9 @@ 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { import {
DeliveryOrderProductFormValues, DeliveryOrderProductFormValues,
DeliveryOrderProductSchema, DeliveryOrderProductSchema,
@@ -224,8 +224,6 @@ 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') {
@@ -245,9 +243,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 = useCallback(() => { const handleResetForm = () => {
setFormErrorMessage(''); setFormErrorMessage('');
resetForm({ formik.resetForm({
values: { values: {
delivery_date: '', delivery_date: '',
vehicle_number: '', vehicle_number: '',
@@ -271,10 +269,9 @@ const DeliveryOrderProductForm = ({
}, },
}); });
// setSelectedProduct(null); // setSelectedProduct(null);
}, [resetForm]); };
const handleBlurField = useCallback( const handleBlurField = (field: string) => {
(field: string) => {
setCurrentInput(field); setCurrentInput(field);
handleMarketingCalculation(field, { handleMarketingCalculation(field, {
@@ -282,9 +279,7 @@ const DeliveryOrderProductForm = ({
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 = (
@@ -329,12 +324,8 @@ 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 {
@@ -347,7 +338,7 @@ const DeliveryOrderProductForm = ({
} }
} }
} }
}, [handleResetForm, initialValues, setFormikValues]); }, [initialValues]);
// ===== Formik Error List ===== // ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList( const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
@@ -365,10 +356,8 @@ const DeliveryOrderProductForm = ({
); );
useEffect(() => { useEffect(() => {
if (formik.values.week) {
handleBlurField('week'); handleBlurField('week');
} }, [formik.values.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, useCallback, useEffect, useMemo, useState } from 'react'; import { RefObject, 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,8 +240,7 @@ const SalesOrderProductForm = ({
}); });
}; };
const handleBlurField = useCallback( const handleBlurField = (field: string) => {
(field: string) => {
setCurrentInput(field); setCurrentInput(field);
handleMarketingCalculation(field, { handleMarketingCalculation(field, {
@@ -249,9 +248,7 @@ const SalesOrderProductForm = ({
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 = (
@@ -310,10 +307,8 @@ const SalesOrderProductForm = ({
); );
useEffect(() => { useEffect(() => {
if (formik.values.week) {
handleBlurField('week'); handleBlurField('week');
} }, [formik.values.week]);
}, [formik.values.week, handleBlurField]);
return ( return (
<> <>
@@ -6,6 +6,7 @@ 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;
@@ -19,6 +20,9 @@ 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) {
@@ -49,6 +53,7 @@ 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}`;
} }
}; };
@@ -87,7 +92,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,6 +6,7 @@ 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;
@@ -15,6 +16,9 @@ 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) {
@@ -43,6 +47,7 @@ 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,9 +44,7 @@ const ChickinFormKandang = ({
const afterSubmitFormChickin = () => { const afterSubmitFormChickin = () => {
setOpenChickin(true); setOpenChickin(true);
if (afterSubmit) { afterSubmit && afterSubmit();
afterSubmit();
}
refreshApprovals(); refreshApprovals();
}; };
@@ -23,7 +23,7 @@ const ChickinLogsView = ({
rawDataApprovals: BaseApproval[]; rawDataApprovals: BaseApproval[];
}) => { }) => {
const [chickinErrorMessage, setChickinErrorMessage] = useState(''); const [chickinErrorMessage, setChickinErrorMessage] = useState('');
const { openChickinApproveModal } = useChickinStore(); const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore();
const handleClickApprove = () => { const handleClickApprove = () => {
openChickinApproveModal(initialValues, async (notes?: string) => { openChickinApproveModal(initialValues, async (notes?: string) => {
@@ -40,8 +40,21 @@ const ChickinLogsView = ({
toast.error(approveChickinRes?.message as string); toast.error(approveChickinRes?.message as string);
setChickinErrorMessage(approveChickinRes?.message as string); setChickinErrorMessage(approveChickinRes?.message as string);
} }
if (afterSubmit) { afterSubmit && afterSubmit();
afterSubmit(); });
};
const handleDeleteChickin = (chickinId: number) => {
openChickinDeleteModal(chickinId, async () => {
const deleteRes = await ChickinApi.delete(chickinId);
if (isResponseSuccess(deleteRes)) {
toast.success(deleteRes?.message || 'Chickin berhasil dihapus');
afterSubmit && afterSubmit();
}
if (isResponseError(deleteRes)) {
toast.error(deleteRes?.message || 'Gagal menghapus chickin');
} }
}); });
}; };
@@ -88,6 +101,7 @@ const ChickinLogsView = ({
<div className='text-lg font-semibold'> <div className='text-lg font-semibold'>
Chick In #{index + 1} - {latestApproval?.step_number} Chick In #{index + 1} - {latestApproval?.step_number}
</div> </div>
<div className='flex flex-row gap-2 items-center'>
<PillBadge <PillBadge
content={ content={
isApproved ? 'Disetujui' : isPending ? 'Pending' : '-' isApproved ? 'Disetujui' : isPending ? 'Pending' : '-'
@@ -96,6 +110,21 @@ const ChickinLogsView = ({
isApproved ? 'green' : isPending ? 'yellow' : 'gray' isApproved ? 'green' : isPending ? 'yellow' : 'gray'
} }
/> />
{isApproved && (
<Button
color='error'
className='w-fit text-sm text-base-100 rounded-lg shadow-sm btn-xs!'
onClick={() => handleDeleteChickin(chickin.id)}
>
<Icon
icon='heroicons:trash-solid'
width={10}
height={10}
/>
</Button>
)}
</div>
</div> </div>
{/* Tanggal Chick In */} {/* Tanggal Chick In */}
@@ -200,6 +200,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
const confirmModal = useModal(); const confirmModal = useModal();
const successModal = useModal(); const successModal = useModal();
const chickinApproveModal = useModal(); const chickinApproveModal = useModal();
const chickinDeleteModal = useModal();
const closingModal = useModal(); const closingModal = useModal();
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'APPROVED' 'APPROVED'
@@ -214,6 +215,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
chickinApproveCallback, chickinApproveCallback,
closeChickinApproveModal, closeChickinApproveModal,
setChickinApproveLoading, setChickinApproveLoading,
isChickinDeleteModalOpen,
isChickinDeleteLoading,
chickinDeleteCallback,
closeChickinDeleteModal,
setChickinDeleteLoading,
} = useChickinStore(); } = useChickinStore();
const { const {
@@ -478,6 +484,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
} }
}, [isChickinApproveModalOpen, chickinApproveModal]); }, [isChickinApproveModalOpen, chickinApproveModal]);
useEffect(() => {
if (isChickinDeleteModalOpen) {
chickinDeleteModal.openModal();
} else {
chickinDeleteModal.closeModal();
}
}, [isChickinDeleteModalOpen, chickinDeleteModal]);
useEffect(() => { useEffect(() => {
if (isClosingModalOpen) { if (isClosingModalOpen) {
closingModal.openModal(); closingModal.openModal();
@@ -1208,6 +1222,38 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
}} }}
/> />
{/* Chickin Delete Modal */}
<ConfirmationModal
ref={chickinDeleteModal.ref}
type='error'
text='Apakah anda yakin ingin menghapus data chick in ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
closeChickinDeleteModal();
},
}}
className={{
modal: 'z-9999',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isChickinDeleteLoading,
onClick: async () => {
if (chickinDeleteCallback) {
setChickinDeleteLoading(true);
try {
await chickinDeleteCallback();
} finally {
setChickinDeleteLoading(false);
closeChickinDeleteModal();
}
}
},
}}
/>
{/* Filter Modal */} {/* Filter Modal */}
<Modal <Modal
ref={filterModal.ref} ref={filterModal.ref}
@@ -104,8 +104,19 @@ const RowOptionsMenu = ({
return recording.approval?.action === 'REJECTED'; return recording.approval?.action === 'REJECTED';
}; };
const isRecordingEditable = (recording: Recording) => {
if (recording.project_flock?.project_flock_category === 'GROWING') {
if (recording.transfer_executed) {
return false;
}
return recording.population_can_change === true;
}
return true;
};
const isApproved = isRecordingApproved(props.row.original); const isApproved = isRecordingApproved(props.row.original);
const isRejected = isRecordingRejected(props.row.original); const isRejected = isRecordingRejected(props.row.original);
const isEditable = isRecordingEditable(props.row.original);
return ( return (
<div className='relative'> <div className='relative'>
@@ -138,6 +149,7 @@ const RowOptionsMenu = ({
View Details View Details
</Button> </Button>
</RequirePermission> </RequirePermission>
{isEditable && (
<RequirePermission permissions='lti.production.recording.update'> <RequirePermission permissions='lti.production.recording.update'>
<Button <Button
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`} href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
@@ -150,6 +162,7 @@ const RowOptionsMenu = ({
Edit Edit
</Button> </Button>
</RequirePermission> </RequirePermission>
)}
{!isApproved && !isRejected && ( {!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'> <RequirePermission permissions='lti.production.recording.approve'>
<Button <Button
@@ -182,6 +195,7 @@ const RowOptionsMenu = ({
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{isEditable && (
<RequirePermission permissions='lti.production.recording.delete'> <RequirePermission permissions='lti.production.recording.delete'>
<Button <Button
onClick={() => { onClick={() => {
@@ -196,6 +210,7 @@ const RowOptionsMenu = ({
Delete Delete
</Button> </Button>
</RequirePermission> </RequirePermission>
)}
</div> </div>
</PopoverContent> </PopoverContent>
</div> </div>
@@ -29,8 +29,8 @@ type RecordingGrowingFormSchemaType = {
} | null; } | null;
project_flock_kandang_id: number; project_flock_kandang_id: number;
stocks: { stocks: {
product_warehouse_id: number; product_warehouse_id?: number;
qty: number | string; qty?: number | string;
}[]; }[];
depletions: { depletions: {
product_warehouse_id?: number; product_warehouse_id?: number;
@@ -73,6 +73,18 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
.typeError('Jumlah penggunaan harus berupa angka!'), .typeError('Jumlah penggunaan harus berupa angka!'),
}); });
const OptionalStockObjectSchema: Yup.ObjectSchema<{
product_warehouse_id?: number;
qty?: number | string;
}> = Yup.object({
product_warehouse_id: Yup.number()
.optional()
.typeError('Produk harus berupa angka!'),
qty: Yup.number()
.optional()
.typeError('Jumlah penggunaan harus berupa angka!'),
});
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({ const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number() product_warehouse_id: Yup.number()
.optional() .optional()
@@ -90,7 +102,9 @@ const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'), weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
}); });
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> = export const RecordingGrowingFormSchema = (
isTransitioningToLaying = false
): Yup.ObjectSchema<RecordingGrowingFormSchemaType> =>
Yup.object({ Yup.object({
record_date: Yup.string() record_date: Yup.string()
.required('Tanggal recording wajib diisi!') .required('Tanggal recording wajib diisi!')
@@ -150,7 +164,9 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
return true; return true;
} }
), ),
stocks: Yup.array() stocks: isTransitioningToLaying
? Yup.array().of(OptionalStockObjectSchema).default([])
: Yup.array()
.of(StockObjectSchema) .of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!') .min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'), .required('Data stok wajib diisi!'),
@@ -158,12 +174,14 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
}); });
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> = export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({ RecordingGrowingFormSchema().shape({
eggs: Yup.array().of(EggObjectSchema).default([]), eggs: Yup.array().of(EggObjectSchema).default([]),
}); });
export const UpdateRecordingGrowingFormSchema = export const UpdateRecordingGrowingFormSchema = (
RecordingGrowingFormSchema.shape({ isTransitioningToLaying = false
) =>
RecordingGrowingFormSchema(isTransitioningToLaying).shape({
location_id: Yup.number().nullable().optional(), location_id: Yup.number().nullable().optional(),
project_flock_id: Yup.number().nullable().optional(), project_flock_id: Yup.number().nullable().optional(),
kandang_id: Yup.number().nullable().optional(), kandang_id: Yup.number().nullable().optional(),
@@ -193,10 +211,13 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({
.required('Project Flock Kandang wajib diisi!'), .required('Project Flock Kandang wajib diisi!'),
}); });
export type RecordingGrowingFormValues = Yup.InferType< type RecordingGrowingFormSchemaFn = ReturnType<
typeof RecordingGrowingFormSchema typeof RecordingGrowingFormSchema
>; >;
export type RecordingGrowingFormValues =
Yup.InferType<RecordingGrowingFormSchemaFn>;
export type RecordingLayingFormValues = Yup.InferType< export type RecordingLayingFormValues = Yup.InferType<
typeof RecordingLayingFormSchema typeof RecordingLayingFormSchema
>; >;
@@ -272,6 +272,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
return recording?.approval?.action === 'REJECTED'; return recording?.approval?.action === 'REJECTED';
}, []); }, []);
const isRecordingEditable = useCallback((recording?: Recording) => {
if (recording?.project_flock?.project_flock_category === 'GROWING') {
if (recording?.transfer_executed) {
return false;
}
return recording?.population_can_change === true;
}
return true;
}, []);
// ===== PAYLOAD CREATION HELPERS ===== // ===== PAYLOAD CREATION HELPERS =====
const createGrowingPayload = useCallback( const createGrowingPayload = useCallback(
(values: RecordingGrowingFormValues) => { (values: RecordingGrowingFormValues) => {
@@ -581,6 +591,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
projectFlockKandangLookup?.project_flock?.category === 'GROWING' || projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
projectFlockKandangDetail?.project_flock?.category === 'GROWING'; projectFlockKandangDetail?.project_flock?.category === 'GROWING';
const isTransitioningToLaying = useMemo(() => {
if (!isGrowingCategory) return false;
return (
initialValues?.population_can_change === true ||
initialValues?.transfer_executed === true
);
}, [initialValues, isGrowingCategory]);
const recordingApprovalLines = useMemo(() => { const recordingApprovalLines = useMemo(() => {
if (isLayingCategory) { if (isLayingCategory) {
return LAYING_RECORDING_APPROVAL_LINE; return LAYING_RECORDING_APPROVAL_LINE;
@@ -941,8 +959,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
} else { } else {
schema = schema =
type === 'edit' type === 'edit'
? UpdateRecordingGrowingFormSchema ? UpdateRecordingGrowingFormSchema(isTransitioningToLaying)
: RecordingGrowingFormSchema; : RecordingGrowingFormSchema(isTransitioningToLaying);
} }
return schema.clone().concat( return schema.clone().concat(
Yup.object().shape({ Yup.object().shape({
@@ -2323,21 +2341,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
<th> <th>
Persediaan Persediaan
{!isTransitioningToLaying && (
<span <span
className='tooltip tooltip-error tooltip-bottom ' className='tooltip tooltip-error tooltip-bottom '
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
)}
</th> </th>
<th> <th>
Jumlah Pakai Jumlah Pakai
{!isTransitioningToLaying && (
<span <span
className='tooltip tooltip-error tooltip-bottom ' className='tooltip tooltip-error tooltip-bottom '
data-tip='required' data-tip='required'
> >
<span className='text-error'>*</span> <span className='text-error'>*</span>
</span> </span>
)}
</th> </th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th> <th>Action</th>
@@ -2372,7 +2394,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} )}
<td> <td>
<SelectInput <SelectInput
required required={!isTransitioningToLaying}
key={`stock-product-${idx}-${stock.product_warehouse_id}`} key={`stock-product-${idx}-${stock.product_warehouse_id}`}
value={ value={
unifiedStockProducts.find( unifiedStockProducts.find(
@@ -2430,7 +2452,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<td> <td>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<NumberInput <NumberInput
required required={!isTransitioningToLaying}
name={`stocks.${idx}.qty`} name={`stocks.${idx}.qty`}
value={stock.qty ?? ''} value={stock.qty ?? ''}
onChange={handleStockUsageQtyChangeWrapper(idx)} onChange={handleStockUsageQtyChangeWrapper(idx)}
@@ -2990,7 +3012,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div className='flex flex-col sm:flex-row sm:justify-between gap-2'> <div className='flex flex-col sm:flex-row sm:justify-between gap-2'>
{/* Left side - Detail & Edit actions */} {/* Left side - Detail & Edit actions */}
<div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'> <div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'>
{type === 'detail' && deleteRecordingClickHandler && ( {type === 'detail' &&
deleteRecordingClickHandler &&
isRecordingEditable(initialValues) && (
<RequirePermission permissions='lti.production.recording.delete'> <RequirePermission permissions='lti.production.recording.delete'>
<Button <Button
type='button' type='button'
@@ -3008,7 +3032,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{type === 'detail' && initialValues && ( {type === 'detail' &&
initialValues &&
isRecordingEditable(initialValues) && (
<RequirePermission permissions='lti.production.recording.update'> <RequirePermission permissions='lti.production.recording.update'>
<Button <Button
type='button' type='button'
@@ -50,12 +50,18 @@ const TransferToLayingConfirmationModalTable = ({
transferToLayingForm?: TransferToLayingFormValues; transferToLayingForm?: TransferToLayingFormValues;
transferToLayingId?: number; transferToLayingId?: number;
}) => { }) => {
const isValidId =
transferToLayingId !== undefined &&
transferToLayingId !== null &&
!isNaN(transferToLayingId) &&
transferToLayingId > 0;
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } = const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
useSWR( useSWR(
transferToLayingId isValidId
? ['detail-transfer-to-laying', String(transferToLayingId)] ? ['detail-transfer-to-laying', String(transferToLayingId)]
: undefined, : undefined,
([id]) => TransferToLayingApi.getSingle(Number(id)) ([, id]) => TransferToLayingApi.getSingle(Number(id))
); );
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] = const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
@@ -273,7 +279,11 @@ const TransferToLayingConfirmationModal = ({
{transferToLayingIds && {transferToLayingIds &&
!transferToLayingForm && !transferToLayingForm &&
transferToLayingIds.map((transferToLayingId, idx) => ( transferToLayingIds
.filter(
(id) => id !== undefined && id !== null && !isNaN(id) && id > 0
)
.map((transferToLayingId, idx) => (
<TransferToLayingConfirmationModalTable <TransferToLayingConfirmationModalTable
key={idx} key={idx}
transferToLayingId={transferToLayingId} transferToLayingId={transferToLayingId}
@@ -82,7 +82,7 @@ const TransferToLayingDetailModal = () => {
if (modalAction === 'detail') { if (modalAction === 'detail') {
detailModal.openModal(); detailModal.openModal();
} }
}, [modalAction, detailModal]); }, [modalAction]);
return ( return (
<Modal <Modal
@@ -229,6 +229,8 @@ const TransferToLayingFormModal = () => {
ProjectFlock | undefined ProjectFlock | undefined
>(undefined); >(undefined);
const [maxSourceQuantity, setMaxSourceQuantity] = useState<number>(0);
const selectedFlockDestinationRawData = isResponseSuccess( const selectedFlockDestinationRawData = isResponseSuccess(
flockDestinationRawData flockDestinationRawData
) )
@@ -353,19 +355,14 @@ const TransferToLayingFormModal = () => {
return { available: countAvailable, unavailable: countUnavailable }; return { available: countAvailable, unavailable: countUnavailable };
}, [mappedFlockDestinationKandangsMaxTargetQty]); }, [mappedFlockDestinationKandangsMaxTargetQty]);
const totalEnteredChickenForTransfer =
formik.values.flockSourceKandangs.reduce(
(acc, item) => acc + Number(item.quantity),
0
);
const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce( const totalTransferedChicken = formik.values.flockDestinationKandangs.reduce(
(acc, item) => acc + Number(item.quantity), (acc, item) => acc + Number(item.quantity),
0 0
); );
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
const totalAvailableChickenForTransfer = const totalAvailableChickenForTransfer =
totalEnteredChickenForTransfer - totalTransferedChicken; maxSourceQuantity - totalTransferedChicken;
const isNextButtonDisabled = useMemo(() => { const isNextButtonDisabled = useMemo(() => {
if (step === 1) { if (step === 1) {
@@ -397,6 +394,7 @@ const TransferToLayingFormModal = () => {
formik.setFieldValue('maxTotalQuantity', ''); formik.setFieldValue('maxTotalQuantity', '');
formik.setFieldValue('reason', ''); formik.setFieldValue('reason', '');
formik.setFieldTouched('reason', false); formik.setFieldTouched('reason', false);
setMaxSourceQuantity(0);
setStep(2); setStep(2);
}; };
@@ -404,6 +402,7 @@ const TransferToLayingFormModal = () => {
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => { const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('flockSource', val); formik.setFieldValue('flockSource', val);
formik.setFieldValue('flockSourceKandangs', []); formik.setFieldValue('flockSourceKandangs', []);
setMaxSourceQuantity(0);
}; };
const flockDestinationChangeHandler = ( const flockDestinationChangeHandler = (
@@ -469,6 +468,26 @@ const TransferToLayingFormModal = () => {
formik.setFieldValue('maxTotalQuantity', totalTransferedChicken); formik.setFieldValue('maxTotalQuantity', totalTransferedChicken);
}, [totalTransferedChicken, formik.values.flockDestinationKandangs]); }, [totalTransferedChicken, formik.values.flockDestinationKandangs]);
// Auto-fill source kandang quantity from total destination quantity
useEffect(() => {
if (formik.values.flockSourceKandangs.length > 0) {
formik.setFieldValue(
'flockSourceKandangs.0.quantity',
totalTransferedChicken
);
}
}, [totalTransferedChicken]);
useEffect(() => {
if (
formik.values.flockSourceKandangs.length > 0 &&
formik.values.flockSourceKandangs[0].maxQuantity &&
maxSourceQuantity === 0
) {
setMaxSourceQuantity(formik.values.flockSourceKandangs[0].maxQuantity);
}
}, [formik.values.flockSourceKandangs, maxSourceQuantity]);
return ( return (
<> <>
<Modal <Modal
@@ -583,14 +602,9 @@ const TransferToLayingFormModal = () => {
k.kandang.value === item.project_flock_kandang_id k.kandang.value === item.project_flock_kandang_id
); );
const flockSourceKandangCheckboxChangeHandler: FormEventHandler< const flockSourceKandangRadioChangeHandler = () => {
HTMLInputElement if (isAvailable) {
> = (e) => {
const checked = (e.target as HTMLInputElement)
.checked;
if (checked) {
formik.setFieldValue('flockSourceKandangs', [ formik.setFieldValue('flockSourceKandangs', [
...formik.values.flockSourceKandangs,
{ {
kandang: { kandang: {
value: item.project_flock_kandang_id, value: item.project_flock_kandang_id,
@@ -600,15 +614,7 @@ const TransferToLayingFormModal = () => {
maxQuantity: item.available_qty, maxQuantity: item.available_qty,
}, },
]); ]);
} else { setMaxSourceQuantity(item.available_qty);
formik.setFieldValue(
'flockSourceKandangs',
formik.values.flockSourceKandangs.filter(
(k) =>
k.kandang.value !==
item.project_flock_kandang_id
)
);
} }
}; };
@@ -618,28 +624,22 @@ const TransferToLayingFormModal = () => {
className='w-full p-3 flex flex-row items-center justify-between' className='w-full p-3 flex flex-row items-center justify-between'
> >
<div className='flex flex-row items-center gap-3'> <div className='flex flex-row items-center gap-3'>
<CheckboxInput <input
name={`flockSourceKandang.${itemIdx}.value`} type='radio'
name='flockSourceKandang'
value={item.project_flock_kandang_id} value={item.project_flock_kandang_id}
checked={isChecked} checked={isChecked}
onChange={ onChange={flockSourceKandangRadioChangeHandler}
flockSourceKandangCheckboxChangeHandler
}
size='md'
disabled={!isAvailable} disabled={!isAvailable}
classNames={{ className={cn('radio radio-md radio-primary', {
checkbox: cn({ 'opacity-50 cursor-not-allowed': !isAvailable,
'bg-base-200 border border-base-content/10 opacity-100': })}
!isAvailable,
}),
}}
/> />
<label <label
htmlFor={`flockSourceKandang.${itemIdx}.value`}
className={cn('text-sm text-base-content/50', { className={cn('text-sm text-base-content/50', {
'cursor-pointer': isAvailable, 'cursor-pointer': isAvailable,
'cursor-not-allowed': !isAvailable, 'cursor-not-allowed opacity-50': !isAvailable,
})} })}
> >
{item.kandang_name}{' '} {item.kandang_name}{' '}
@@ -858,7 +858,7 @@ const TransferToLayingFormModal = () => {
<NumberInput <NumberInput
key={`flockSourceKandangs-${item.kandang.value}-${index}`} key={`flockSourceKandangs-${item.kandang.value}-${index}`}
name={`flockSourceKandangs.${index}.quantity`} name={`flockSourceKandangs.${index}.quantity`}
placeholder='Masukkan Kuantitas' placeholder='Masukkan Kuantitas pada Kandang Tujuan'
value={item.quantity} value={item.quantity}
onChange={formik.handleChange} onChange={formik.handleChange}
isError={isInvalid} isError={isInvalid}
@@ -875,6 +875,8 @@ const TransferToLayingFormModal = () => {
<div className='w-px bg-base-content/10' /> <div className='w-px bg-base-content/10' />
</div> </div>
} }
readOnly
disabled
className={{ className={{
inputPrefix: inputPrefix:
'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0', 'py-0 px-0 pl-3 text-base-content/50 bg-transparent border-r-0',
@@ -1000,7 +1002,7 @@ const TransferToLayingFormModal = () => {
isError={totalAvailableChickenForTransfer < 0} isError={totalAvailableChickenForTransfer < 0}
errorMessage={ errorMessage={
totalAvailableChickenForTransfer < 0 totalAvailableChickenForTransfer < 0
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)` ? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
: '' : ''
} }
disabled disabled
@@ -48,11 +48,11 @@ const RowOptionsMenu = ({
popoverPosition: 'bottom' | 'top'; popoverPosition: 'bottom' | 'top';
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const showEditButton = const showEditButton = props.row.original.approval.action !== 'APPROVED';
props.row.original.approval.action !== 'APPROVED' &&
props.row.original.approval.action !== 'REJECTED';
const showDeleteButton = showEditButton; const showDeleteButton =
props.row.original.approval.action === 'APPROVED' ||
props.row.original.approval.step_name.toLowerCase() === 'pengajuan';
const popoverId = `transferToLaying#${props.row.original.id}`; const popoverId = `transferToLaying#${props.row.original.id}`;
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`; const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
@@ -463,7 +463,7 @@ const TransferToLayingsTable = () => {
updateFilter('filter_by', ''); updateFilter('filter_by', '');
updateFilter('sort_by', ''); updateFilter('sort_by', '');
} }
}, [sorting, updateFilter]); }, [sorting]);
return ( return (
<> <>
@@ -60,6 +60,25 @@ 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 &&
@@ -183,25 +202,6 @@ 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,14 +231,7 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
}, },
}, },
], ],
[ [initialValues, handleViewUniformityDetails, isLoading]
initialValues,
isLoading,
uniformity_details,
setShouldFetchDetails,
setExpandedDrawerContent,
setExpandedDrawerOpen,
]
); );
const samplingTableData: DetailOptionType[] = useMemo(() => { const samplingTableData: DetailOptionType[] = useMemo(() => {
@@ -301,93 +301,8 @@ 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({});
@@ -462,6 +377,87 @@ 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, useCallback } from 'react'; import { useState, useEffect } 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,7 +137,15 @@ export function DetailDailyChecklistContent() {
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const fetchChecklistDetail = useCallback(async () => { useEffect(() => {
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);
@@ -312,15 +320,7 @@ 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,17 +305,3 @@ 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;
}
@@ -4,13 +4,16 @@ import { create } from 'zustand';
import { devtools } from 'zustand/middleware'; import { devtools } from 'zustand/middleware';
import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice';
import { createChickinDeleteSlice } from '@/stores/production/chickin/slices/chickin-delete.slice';
import { ChickinDeleteSlice } from '@/stores/production/chickin/slices/chickin-delete.slice';
export type ChickinStore = ChickinApprovalSlice; export type ChickinStore = ChickinApprovalSlice & ChickinDeleteSlice;
export const useChickinStore = create<ChickinStore>()( export const useChickinStore = create<ChickinStore>()(
devtools( devtools(
(...args) => ({ (...args) => ({
...createChickinApprovalSlice(...args), ...createChickinApprovalSlice(...args),
...createChickinDeleteSlice(...args),
}), }),
{ {
name: 'ChickinStore', name: 'ChickinStore',
@@ -0,0 +1,57 @@
import { StateCreator } from 'zustand';
export type ChickinDeleteSlice = {
// State
isChickinDeleteModalOpen: boolean;
selectedChickinIdForDelete: number | null;
isChickinDeleteLoading: boolean;
chickinDeleteCallback: (() => Promise<void>) | null;
// Actions
openChickinDeleteModal: (
chickinId: number,
callback: () => Promise<void>
) => void;
closeChickinDeleteModal: () => void;
setChickinDeleteLoading: (loading: boolean) => void;
resetChickinDelete: () => void;
};
export const createChickinDeleteSlice: StateCreator<
ChickinDeleteSlice,
[],
[],
ChickinDeleteSlice
> = (set) => ({
// Initial state
isChickinDeleteModalOpen: false,
selectedChickinIdForDelete: null,
isChickinDeleteLoading: false,
chickinDeleteCallback: null,
// Actions
openChickinDeleteModal: (chickinId, callback) =>
set({
isChickinDeleteModalOpen: true,
selectedChickinIdForDelete: chickinId,
chickinDeleteCallback: callback,
}),
closeChickinDeleteModal: () =>
set({
isChickinDeleteModalOpen: false,
selectedChickinIdForDelete: null,
chickinDeleteCallback: null,
}),
setChickinDeleteLoading: (loading) =>
set({ isChickinDeleteLoading: loading }),
resetChickinDelete: () =>
set({
isChickinDeleteModalOpen: false,
selectedChickinIdForDelete: null,
isChickinDeleteLoading: false,
chickinDeleteCallback: null,
}),
});
@@ -1,5 +1,4 @@
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<
@@ -21,9 +20,10 @@ export const createTabActionsSlice: StateCreator<
})), })),
clearTabActions: (tabId) => clearTabActions: (tabId) =>
set((state) => ({ set((state) => {
tabActions: omit(state.tabActions, tabId), const { [tabId]: _, ...rest } = state.tabActions;
})), return { tabActions: rest };
}),
clearAllTabActions: () => set({ tabActions: {} }), clearAllTabActions: () => set({ tabActions: {} }),
}); });
+2
View File
@@ -49,6 +49,8 @@ export type BaseRecording = {
project_flock: ProjectFlock; project_flock: ProjectFlock;
record_datetime: string; record_datetime: string;
day: number; day: number;
population_can_change: boolean;
transfer_executed: boolean;
} & ProductionMetrics; } & ProductionMetrics;
export type RecordingDepletion = { export type RecordingDepletion = {