Compare commits

..

18 Commits

Author SHA1 Message Date
rstubryan 584b495e4b refactor(FE): Fix missing dependency in useEffect hook 2026-03-07 12:01:46 +07:00
rstubryan c8e76a8558 refactor(FE): Refactor ExpensePDF component for improved readability 2026-03-07 10:14:10 +07:00
rstubryan a468a5948b refactor(FE): Refactor useEffect hooks and loadAssignments function 2026-03-07 09:34:55 +07:00
rstubryan e05e1a4121 chore(FE): Fix missing dependency in useEffect dependency array 2026-03-07 09:20:14 +07:00
rstubryan e3b86e3033 refactor(FE): Add omit utility and refactor clearTabActions to use
it
2026-03-07 09:19:11 +07:00
rstubryan d467c56ea6 refactor(FE): Refactor form reset logic to use useCallback with
dependencies
2026-03-06 15:03:37 +07:00
rstubryan 784d9f26ab refactor(FE): Refactor fetchChecklistDetail with useCallback 2026-03-06 15:00:33 +07:00
rstubryan 978ef764ea refactor(FE): Refactor productsClickHandler to be defined inline 2026-03-06 14:45:27 +07:00
rstubryan 928136ff18 refactor(FE): Refactor handleViewUniformityDetails to inline definition 2026-03-06 14:29:21 +07:00
rstubryan 79567e4f1b refactor(FE): Remove unnecessary dependencies from useMemo in table
columns
2026-03-06 14:07:24 +07:00
rstubryan 633deece21 refactor(FE): Fix dependency array in ExpenseRealizationForm useCallback 2026-03-06 14:05:11 +07:00
rstubryan 46483af4c2 refactor(FE): Refactor formik field methods to use destructured helpers 2026-03-06 13:53:15 +07:00
rstubryan c2653e5068 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into fix/cleanup-warning 2026-03-06 13:41:30 +07:00
rstubryan 8569bda7d6 refactor(FE): Refactor afterSubmit calls to improve readability 2026-03-06 13:41:29 +07:00
rstubryan 7da63dc542 refactor(FE): Prevent duplicate initial value loading in form modals 2026-03-06 13:37:35 +07:00
rstubryan aeceef4361 refactor(FE): Refactor handleBlurField to use useCallback 2026-03-06 13:35:10 +07:00
rstubryan ff6955be54 refactor(FE): Remove unused useSearchParams and related code 2026-03-06 13:33:46 +07:00
rstubryan eccab314b3 refactor(FE): Fix missing dependencies in MarketingTable useMemo 2026-03-06 11:23:01 +07:00
55 changed files with 1232 additions and 3228 deletions
+2 -30
View File
@@ -15,7 +15,7 @@ default:
# ========================================================== # ==========================================================
.build_template: &build_template .build_template: &build_template
stage: build stage: build
image: public.ecr.aws/docker/library/node:20-alpine image: node:20-alpine
cache: cache:
key: npm-cache key: npm-cache
paths: paths:
@@ -56,7 +56,7 @@ default:
.deploy_template: &deploy_template .deploy_template: &deploy_template
stage: deploy stage: deploy
image: image:
name: public.ecr.aws/aws-cli/aws-cli:latest name: amazon/aws-cli:latest
entrypoint: ['/bin/sh', '-c'] entrypoint: ['/bin/sh', '-c']
script: script:
- set -e - set -e
@@ -183,31 +183,3 @@ deploy:staging:
environment: environment:
name: staging name: staging
url: https://stg-lti-erp.mbugroup.id url: https://stg-lti-erp.mbugroup.id
# ==========================================================
# ====== STAGING (Branch production) ======
# ==========================================================
build:production:
<<: *build_template
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
environment:
name: staging
variables:
NEXT_PUBLIC_LTI_URL: 'https://lti-erp.mbugroup.id'
NEXT_PUBLIC_SSO_LOGIN_URL: 'https://auth-erp.mbugroup.id'
NEXT_PUBLIC_API_BASE_URL: 'https://api-lti.mbugroup.id/api'
NEXT_PUBLIC_CLIENT_ID: 'Lumbung-Telur-Indonesia'
deploy:production:
<<: *deploy_template
needs: ['build:production']
rules:
- if: '$CI_COMMIT_BRANCH == "production"'
variables:
S3_BUCKET: 'production-lti-erp.mbugroup.id'
AWS_REGION: 'ap-southeast-3'
CLOUDFRONT_DISTRIBUTION_ID: 'E1SSLXKYYITASJ'
environment:
name: staging
url: https://lti-erp.mbugroup.id
+1 -1
View File
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/node:20-alpine FROM node:20-alpine
RUN apk add --no-cache git bash build-base curl RUN apk add --no-cache git bash build-base curl
+1 -2
View File
@@ -8,8 +8,7 @@
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"prepare": "husky", "prepare": "husky",
"format": "prettier --write .", "format": "prettier --write ."
"pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build"
}, },
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.3.1", "@react-pdf/renderer": "^4.3.1",
@@ -1,11 +0,0 @@
import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent';
const MasterKandangPage = () => {
return (
<section className='w-full'>
<MasterKandangContent />
</section>
);
};
export default MasterKandangPage;
+2 -2
View File
@@ -24,8 +24,8 @@ import {
} from '@/types/api/api-general'; } from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
export interface OptionType<T = string | number> { export interface OptionType {
value: T; value: string | number;
label: string; label: string;
className?: string; className?: string;
labelClassName?: string; labelClassName?: string;
@@ -66,7 +66,7 @@ const ExpenseRealizationForm = ({
toast.success(createExpenseRes?.message as string); toast.success(createExpenseRes?.message as string);
router.push('/expense'); router.push('/expense');
}, },
[router] [router, initialValues?.id]
); );
const updateExpenseHandler = useCallback( const updateExpenseHandler = useCallback(
@@ -178,12 +178,14 @@ const ExpenseRequestForm = ({
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocationOptions,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name'); } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const { const {
setInputValue: setVendorInputValue, setInputValue: setVendorInputValue,
options: supplierOptions, options: supplierOptions,
isLoadingOptions: isLoadingVendorOptions, isLoadingOptions: isLoadingVendorOptions,
loadMore: loadMoreVendorOptions,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name'); } = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -414,6 +416,7 @@ const ExpenseRequestForm = ({
errorMessage={formik.errors.location_id as string} errorMessage={formik.errors.location_id as string}
isClearable isClearable
className={{ wrapper: 'col-span-12 sm:col-span-4' }} className={{ wrapper: 'col-span-12 sm:col-span-4' }}
onMenuScrollToBottom={loadMoreLocationOptions}
/> />
<DateInput <DateInput
@@ -457,6 +460,7 @@ const ExpenseRequestForm = ({
} }
errorMessage={formik.errors.supplier_id as string} errorMessage={formik.errors.supplier_id as string}
className={{ wrapper: 'col-span-12' }} className={{ wrapper: 'col-span-12' }}
onMenuScrollToBottom={loadMoreVendorOptions}
/> />
<RequirePermission permissions='lti.expense.document'> <RequirePermission permissions='lti.expense.document'>
+236 -559
View File
@@ -1,212 +1,154 @@
'use client'; 'use client';
import { import React from 'react';
Document, import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
Image,
Link,
Page,
StyleSheet,
Text,
View,
} from '@react-pdf/renderer';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
interface ExpensePDFProps { interface ExpensePDFProps {
expense?: Expense; expense?: Expense;
} }
const ExpensePDFStyle = StyleSheet.create({ const styles = StyleSheet.create({
page: { page: {
paddingTop: 24, fontSize: 10,
paddingBottom: 64, fontFamily: 'Helvetica',
paddingHorizontal: 32, padding: 20,
backgroundColor: '#FFFFFF',
}, },
titleSection: {
companyInfoHeader: {
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
},
companyLogo: {
width: 64,
height: 'auto',
},
companyInfoHeaderDate: {
paddingTop: 8,
fontSize: 12,
},
companyName: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 4,
},
companyAddress: {
fontSize: 8,
maxWidth: 400,
marginBottom: 10, marginBottom: 10,
}, },
parameterContainer: {
title: {
marginTop: 16,
fontSize: 16,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
footer: {
width: '100%',
display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', flexWrap: 'wrap',
alignItems: 'center', marginBottom: 8,
paddingHorizontal: 32, },
infoTableSection: {
position: 'absolute', marginBottom: 12,
},
infoTableTitle: {
fontSize: 10, fontSize: 10,
bottom: 30,
left: 0,
right: 0,
textAlign: 'center',
color: 'grey',
},
// wrapper
generalInfoTable: {
width: '100%',
marginTop: 8,
borderWidth: 1,
borderColor: '#000000',
borderBottomWidth: 0,
fontSize: 12,
},
generalInfoTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000000',
},
// columns
generalInfoTableColLabel: {
width: '30%',
paddingVertical: 6,
paddingHorizontal: 8,
},
generalInfoTableColSeparator: {
width: '3%',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 6,
},
generalInfoTableColValue: {
width: '67%',
paddingVertical: 6,
paddingHorizontal: 8,
},
generalInfoTableLabelText: {
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: 6,
color: '#333',
}, },
generalInfoTableValueText: {}, tableSection: {
marginBottom: 12,
// expense detail table
expenseDetailContainer: {
width: '100%',
marginTop: 12,
}, },
expenseDetailTitle: { tableTitle: {
fontSize: 14,
lineHeight: '150%',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
textAlign: 'center',
},
kandangExpenseContainer: {
width: '100%',
marginTop: 8,
},
kandangExpenseTitle: {
fontSize: 14,
lineHeight: '150%',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
textAlign: 'center',
},
kandangExpenseTable: {
width: '100%',
marginTop: 8,
borderWidth: 1,
borderColor: '#000000',
borderBottomWidth: 0,
fontSize: 12,
},
kandangExpenseTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000000',
},
kandangExpenseTableColLabel: {
width: '20%',
paddingVertical: 6,
paddingHorizontal: 8,
},
kandangExpenseTableColLabelBorderRight: {
borderRight: '1px solid #000000',
},
kandangExpenseTableColNonstock: {
width: '20%',
},
kandangExpenseTableColNote: {
width: '40%',
},
kandangExpenseHeaderLabelText: {
fontWeight: 'bold',
},
kandangExpenseLabelText: {
fontSize: 10, fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
color: '#333',
}, },
kandangExpenseTableFooterColTotalExpenseCaption: { emptyText: {
width: '40%', fontSize: 8,
paddingVertical: 6, color: '#666',
paddingHorizontal: 8, fontStyle: 'italic',
textAlign: 'right',
},
kandangExpenseTableFooterColTotalExpenseValue: {
width: '60%',
paddingVertical: 6,
paddingHorizontal: 8,
},
// utils
doubleDivider: {
width: '100%',
height: 6,
borderTop: '2px solid black',
borderBottom: '2px solid black',
}, },
}); });
const ExpensePDF = ({ expense }: ExpensePDFProps) => { type ExpenseKandang = Expense['kandangs'][number];
type PengajuanItem = NonNullable<ExpenseKandang['pengajuans']>[number];
type RealisasiItem = NonNullable<ExpenseKandang['realisasi']>[number];
const valueText = (v: unknown) => {
if (v === null || v === undefined) return '-';
if (typeof v === 'number') return formatNumber(v);
return String(v);
};
const getPengajuanColumns = (): PdfColumn<PengajuanItem>[] => [
{
key: 'no',
header: 'No',
flex: 0.5,
align: 'center',
cell: ({ index }) => index + 1,
},
{
key: 'nonstock',
header: 'Nonstock',
flex: 1.5,
cell: ({ row }) => row.nonstock.name,
},
{
key: 'qty',
header: 'Kuantitas',
flex: 1,
align: 'right',
cell: ({ row }) => valueText(row.qty),
},
{
key: 'price',
header: 'Harga Satuan',
flex: 1.2,
align: 'right',
cell: ({ row }) => formatCurrency(row.price),
},
{
key: 'notes',
header: 'Catatan',
flex: 1.5,
cell: ({ row }) => row.notes || '-',
},
];
const getRealisasiColumns = (): PdfColumn<RealisasiItem>[] => [
{
key: 'no',
header: 'No',
flex: 0.5,
align: 'center',
cell: ({ index }) => index + 1,
},
{
key: 'nonstock',
header: 'Nonstock',
flex: 1.5,
cell: ({ row }) => row.nonstock.name,
},
{
key: 'qty',
header: 'Kuantitas',
flex: 1,
align: 'right',
cell: ({ row }) => valueText(row.qty),
},
{
key: 'price',
header: 'Harga Satuan',
flex: 1.2,
align: 'right',
cell: ({ row }) => formatCurrency(row.price),
},
{
key: 'notes',
header: 'Catatan',
flex: 1.5,
cell: ({ row }) => row.notes || '-',
},
];
const getInfoTableRows = (expense?: Expense) => {
const isLatestApprovalRejected = const isLatestApprovalRejected =
expense?.latest_approval?.action === 'REJECTED'; expense?.latest_approval?.action === 'REJECTED';
const isExpenseRealized = const isExpenseRealized =
expense?.latest_approval?.step_number && expense?.latest_approval?.step_number &&
expense?.latest_approval.step_number >= 5; expense?.latest_approval.step_number >= 5;
const realizationStatus = isExpenseRealized const realizationStatus = isExpenseRealized
? 'Sudah Realisasi' ? 'Sudah Realisasi'
: 'Belum Realisasi'; : 'Belum Realisasi';
const rows = [ return [
{ label: 'Nomor PO', value: expense?.po_number }, { label: 'Nomor PO', value: expense?.po_number || '-' },
{ label: 'Nomor Referensi', value: expense?.reference_number }, { label: 'Nomor Referensi', value: expense?.reference_number || '-' },
{ {
label: 'Kategori', label: 'Kategori',
value: value:
@@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? 'Biaya Operasional' ? 'Biaya Operasional'
: expense?.category === 'NON-BOP' : expense?.category === 'NON-BOP'
? 'Non Biaya Operasional' ? 'Non Biaya Operasional'
: '', : '-',
}, },
{ label: 'Lokasi', value: expense?.location.name }, { label: 'Lokasi', value: expense?.location?.name || '-' },
{ {
label: 'Kandang', label: 'Kandang',
value: value:
@@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
.join(', ') .join(', ')
: '-', : '-',
}, },
{ label: 'Vendor', value: expense?.supplier.name }, { label: 'Vendor', value: expense?.supplier?.name || '-' },
{ {
label: 'Tanggal Transaksi', label: 'Tanggal Transaksi',
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'), value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
@@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? formatDate(expense?.realization_date, 'DD MMMM YYYY') ? formatDate(expense?.realization_date, 'DD MMMM YYYY')
: '-', : '-',
}, },
{ label: 'Nama Pengaju', value: expense?.created_user.name }, { label: 'Nama Pengaju', value: expense?.created_user?.name || '-' },
{ {
label: 'Nominal Biaya', label: 'Nominal Biaya',
value: formatCurrency( value: formatCurrency(
expense?.latest_approval.step_number === 5 || expense?.latest_approval?.step_number === 5 ||
expense?.latest_approval.step_number === 6 expense?.latest_approval?.step_number === 6
? (expense?.total_realisasi ?? 0) ? (expense?.total_realisasi ?? 0)
: (expense?.total_pengajuan ?? 0) : (expense?.total_pengajuan ?? 0)
), ),
@@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
label: 'Status Biaya', label: 'Status Biaya',
value: isLatestApprovalRejected value: isLatestApprovalRejected
? 'Ditolak' ? 'Ditolak'
: expense?.latest_approval?.step_name, : expense?.latest_approval?.step_name || '-',
}, },
]; ];
};
interface InfoRow {
label: string;
value: string;
}
const getInfoTableColumns = (): PdfColumn<InfoRow>[] => [
{
key: 'label',
header: 'Field',
flex: 1,
cell: ({ row }) => row.label,
},
{
key: 'value',
header: 'Value',
flex: 2,
cell: ({ row }) => row.value,
},
];
const ExpensePDF = ({ expense }: ExpensePDFProps) => {
const kandangs = expense?.kandangs || [];
const infoRows = getInfoTableRows(expense);
return ( return (
<Document> <Document>
<Page style={ExpensePDFStyle.page}> <Page style={styles.page} size='A4'>
<View> {/* Title Section */}
<View style={ExpensePDFStyle.companyInfoHeader}> <View style={styles.titleSection}>
<Image <PdfTypography size='h1' variant='primary'>
style={ExpensePDFStyle.companyLogo} Laporan{' '}
src='/assets/img/lti-logo.png' {expense?.category === 'BOP'
/> ? 'Biaya Operasional'
: 'Non-Biaya Operasional'}
<Text style={ExpensePDFStyle.companyInfoHeaderDate}> </PdfTypography>
{formatDate(Date.now(), 'DD MMMM YYYY')} <PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
</Text> <View style={styles.parameterContainer}>
</View> <PdfParamBadge>
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
<View> </PdfParamBadge>
<Text style={ExpensePDFStyle.companyName}> <PdfParamBadge>
PT LUMBUNG TELUR INDONESIA Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text> </PdfParamBadge>
<Text style={ExpensePDFStyle.companyAddress}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={ExpensePDFStyle.doubleDivider} />
</View> </View>
</View> </View>
<Text style={ExpensePDFStyle.title}> {/* Info Table Section */}
Laporan{' '} <View style={styles.infoTableSection}>
{expense?.category === 'BOP' <Text style={styles.infoTableTitle}>Informasi Biaya</Text>
? 'Biaya Operasional' <PdfTable columns={getInfoTableColumns()} data={infoRows} />
: 'Non-Biaya Operasional'}{' '}
{expense?.po_number}
</Text>
{/* General info table */}
<View style={ExpensePDFStyle.generalInfoTable}>
{rows.map((row) => (
<View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}>
<View style={ExpensePDFStyle.generalInfoTableColLabel}>
<Text style={ExpensePDFStyle.generalInfoTableLabelText}>
{row.label}
</Text>
</View>
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
<Text>:</Text>
</View>
<View style={ExpensePDFStyle.generalInfoTableColValue}>
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
{row.value}
</Text>
</View>
</View>
))}
</View> </View>
{/* Detail expense request */} {/* Rincian Pengajuan Section */}
<View <View style={styles.tableSection}>
minPresenceAhead={80} <Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
style={ExpensePDFStyle.expenseDetailContainer} {kandangs.length === 0 ? (
> <Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
<Text style={ExpensePDFStyle.expenseDetailTitle}> ) : (
Rincian Pengajuan Biaya Operasional kandangs.map((kandang, idx) => {
</Text> const pengajuans = kandang.pengajuans || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { return (
let expenseRequestTotal = 0; <View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
kandangExpense.pengajuans?.forEach( {idx + 1}) {kandangName}
(item) => (expenseRequestTotal += item.qty * item.price) </PdfTypography>
); {pengajuans.length > 0 ? (
<PdfTable
return ( columns={getPengajuanColumns()}
<View data={pengajuans}
key={kandangExpenseIdx} showFooter={true}
style={ExpensePDFStyle.kandangExpenseContainer} footerLabel='Total'
> />
<Text style={ExpensePDFStyle.kandangExpenseTitle}> ) : (
{kandangExpense.kandang_id && kandangExpense.name <Text style={styles.emptyText}>
? `Biaya ${kandangExpense.name}` Tidak ada item pengajuan untuk kandang ini.
: `Biaya ${expense?.location.name || 'Umum'}`} </Text>
</Text> )}
<View style={ExpensePDFStyle.kandangExpenseTable}>
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Nonstock
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Kuantitas
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Harga Satuan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Catatan
</Text>
</View>
</View>
{kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
<View
key={pengajuanIdx}
style={ExpensePDFStyle.kandangExpenseTableRow}
>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.nonstock.name}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatNumber(pengajuan.qty)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(pengajuan.price)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.notes}
</Text>
</View>
</View>
))}
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Total Biaya Keseluruhan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
{formatCurrency(expenseRequestTotal)}
</Text>
</View>
</View>
</View> </View>
</View> );
); })
})} )}
</View> </View>
{/* Detail expense realization */} {/* Rincian Realisasi Section */}
<View <View style={styles.tableSection}>
minPresenceAhead={80} <Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
style={ExpensePDFStyle.expenseDetailContainer} {kandangs.length === 0 ? (
> <Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
<Text style={ExpensePDFStyle.expenseDetailTitle}> ) : (
Rincian Realisasi Biaya Operasional kandangs.map((kandang, idx) => {
</Text> const realisasi = kandang.realisasi || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { return (
let expenseRealizationTotal = 0; <View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
kandangExpense.realisasi?.forEach( {idx + 1}) {kandangName}
(item) => (expenseRealizationTotal += item.qty * item.price) </PdfTypography>
); {realisasi.length > 0 ? (
<PdfTable
return ( columns={getRealisasiColumns()}
<View data={realisasi}
key={kandangExpenseIdx} showFooter={true}
style={ExpensePDFStyle.kandangExpenseContainer} footerLabel='Total'
> />
<Text style={ExpensePDFStyle.kandangExpenseTitle}> ) : (
{kandangExpense.kandang_id && kandangExpense.name <Text style={styles.emptyText}>
? `Biaya ${kandangExpense.name}` Tidak ada item realisasi untuk kandang ini.
: `Biaya ${expense?.location.name || 'Umum'}`} </Text>
</Text> )}
<View style={ExpensePDFStyle.kandangExpenseTable}>
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Nonstock
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Kuantitas
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Harga Satuan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Catatan
</Text>
</View>
</View>
{kandangExpense.realisasi?.map((realisasi, realisasiIdx) => (
<View
key={realisasiIdx}
style={ExpensePDFStyle.kandangExpenseTableRow}
>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.nonstock.name}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatNumber(realisasi.qty)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(realisasi.price)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.notes}
</Text>
</View>
</View>
))}
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Total Biaya Keseluruhan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
{formatCurrency(expenseRealizationTotal)}
</Text>
</View>
</View>
</View> </View>
</View> );
); })
})} )}
</View> </View>
<View style={ExpensePDFStyle.footer} fixed> <PdfPageNumber />
<Link
src={`${process.env.NEXT_PUBLIC_LTI_URL}expense/detail?expenseId=${expense?.id}`}
>
{expense?.po_number}
</Link>
<Text
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
</View>
</Page> </Page>
</Document> </Document>
); );
@@ -8,7 +8,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import useSWR, { mutate } from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table'; import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
@@ -26,10 +26,6 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { WarehouseApi, ProductApi } from '@/services/api/master-data'; import { WarehouseApi, ProductApi } from '@/services/api/master-data';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent';
import toast from 'react-hot-toast';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant'; import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
@@ -42,62 +38,6 @@ import {
AdjustmentFilterType, AdjustmentFilterType,
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter'; } from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
import SelectInputRadio from '@/components/input/SelectInputRadio'; import SelectInputRadio from '@/components/input/SelectInputRadio';
import { CellContext } from '@tanstack/react-table';
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
deleteClickHandler,
}: {
popoverPosition: 'bottom' | 'top';
props: CellContext<InventoryAdjustment, unknown>;
deleteClickHandler: () => void;
}) => {
const popoverId = `adjustment#${props.row.original.id}`;
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
const closePopover = () => {
document.getElementById(popoverId)?.hidePopover();
};
return (
<div className='relative'>
<PopoverButton
tabIndex={0}
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
<PopoverContent
id={popoverId}
anchorName={popoverAnchorName}
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
>
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.inventory.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div>
</PopoverContent>
</div>
);
};
const InventoryAdjustmentTable = () => { const InventoryAdjustmentTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore(); const { searchValue, setSearchValue, setTableState } = useUiStore();
@@ -140,13 +80,13 @@ const InventoryAdjustmentTable = () => {
const formik = useFormik<AdjustmentFilterType>({ const formik = useFormik<AdjustmentFilterType>({
initialValues: { initialValues: {
product_id: null, product_id: null,
warehouse: null, warehouse_id: null,
transaction_type: null, transaction_type: null,
}, },
validationSchema: AdjustmentFilterSchema, validationSchema: AdjustmentFilterSchema,
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
updateFilter('productFilter', values.product_id || ''); updateFilter('productFilter', values.product_id || '');
updateFilter('warehouseFilter', String(values.warehouse?.value) || ''); updateFilter('warehouseFilter', values.warehouse_id || '');
updateFilter('transactionTypeFilter', values.transaction_type || ''); updateFilter('transactionTypeFilter', values.transaction_type || '');
filterModal.closeModal(); filterModal.closeModal();
setSubmitting(false); setSubmitting(false);
@@ -202,11 +142,14 @@ const InventoryAdjustmentTable = () => {
[formik] [formik]
); );
const handleFilterWarehouseChange = ( const handleFilterWarehouseChange = useCallback(
val: OptionType | OptionType[] | null (val: OptionType | OptionType[] | null) => {
) => { const warehouse = val as OptionType | null;
formik.setFieldValue('warehouse', val); const warehouseId = warehouse?.value ? String(warehouse.value) : null;
}; formik.setFieldValue('warehouse_id', warehouseId);
},
[formik]
);
const handleFilterTransactionTypeChange = useCallback( const handleFilterTransactionTypeChange = useCallback(
(val: OptionType | OptionType[] | null) => { (val: OptionType | OptionType[] | null) => {
@@ -227,6 +170,15 @@ const InventoryAdjustmentTable = () => {
); );
}, [formik.values.product_id, productOptions]); }, [formik.values.product_id, productOptions]);
const warehouseIdValue = useMemo(() => {
if (!formik.values.warehouse_id) return null;
return (
warehouseOptions.find(
(opt) => String(opt.value) === formik.values.warehouse_id
) || null
);
}, [formik.values.warehouse_id, warehouseOptions]);
const transactionTypeValue = useMemo(() => { const transactionTypeValue = useMemo(() => {
if (!formik.values.transaction_type) return null; if (!formik.values.transaction_type) return null;
return ( return (
@@ -242,39 +194,12 @@ const InventoryAdjustmentTable = () => {
formik.validateForm(); formik.validateForm();
}; };
const { const { data: inventoryAdjustments, isLoading } = useSWR(
data: inventoryAdjustments,
isLoading,
mutate: refreshAdjustments,
} = useSWR(
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
InventoryAdjustmentApi.getAllFetcher InventoryAdjustmentApi.getAllFetcher
); );
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await InventoryAdjustmentApi.delete(
selectedAdjustment?.id as number
);
singleDeleteModal.closeModal();
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Adjustment!');
refreshAdjustments();
} else {
toast.error(response?.message || 'Failed to delete Adjustment');
}
};
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [selectedAdjustment, setSelectedAdjustment] = useState<
InventoryAdjustment | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const singleDeleteModal = useModal();
useEffect(() => { useEffect(() => {
updateFilter('search', searchValue); updateFilter('search', searchValue);
@@ -389,39 +314,8 @@ const InventoryAdjustmentTable = () => {
header: 'Oleh', header: 'Oleh',
accessorFn: (row) => row.created_user?.name ?? '-', accessorFn: (row) => row.created_user?.name ?? '-',
}, },
{
id: 'actions',
header: 'Aksi',
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedAdjustment(props.row.original);
singleDeleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
);
},
},
], ],
[ []
tableFilterState.pageSize,
tableFilterState.page,
singleDeleteModal,
setSelectedAdjustment,
]
); );
const updateSortingFilter = useCallback( const updateSortingFilter = useCallback(
@@ -608,7 +502,7 @@ const InventoryAdjustmentTable = () => {
label='Gudang' label='Gudang'
placeholder='Pilih Gudang' placeholder='Pilih Gudang'
options={warehouseOptions} options={warehouseOptions}
value={formik.values.warehouse} value={warehouseIdValue}
onChange={handleFilterWarehouseChange} onChange={handleFilterWarehouseChange}
onInputChange={setWarehouseInputValue} onInputChange={setWarehouseInputValue}
isLoading={isLoadingWarehouseOptions} isLoading={isLoadingWarehouseOptions}
@@ -650,21 +544,6 @@ const InventoryAdjustmentTable = () => {
</div> </div>
</form> </form>
</Modal> </Modal>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</> </>
); );
}; };
@@ -1,5 +1,4 @@
import { string, object } from 'yup'; import { string, object } from 'yup';
import { OptionType } from '@/components/input/SelectInput';
export const AdjustmentFilterSchema = object().shape({ export const AdjustmentFilterSchema = object().shape({
product_id: string().nullable(), product_id: string().nullable(),
@@ -9,6 +8,6 @@ export const AdjustmentFilterSchema = object().shape({
export type AdjustmentFilterType = { export type AdjustmentFilterType = {
product_id: string | null; product_id: string | null;
warehouse_id: string | null;
transaction_type: string | null; transaction_type: string | null;
warehouse: OptionType<number> | null;
}; };
@@ -15,7 +15,7 @@ import {
InventoryAdjustmentFormSchema, InventoryAdjustmentFormSchema,
InventoryAdjustmentFormValues, InventoryAdjustmentFormValues,
} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
import { LocationApi } from '@/services/api/master-data'; import { KandangApi, LocationApi } from '@/services/api/master-data';
import { import {
ProjectFlockApi, ProjectFlockApi,
ProjectFlockKandangApi, ProjectFlockKandangApi,
@@ -32,6 +32,8 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { Location } from '@/types/api/master-data/location'; import { Location } from '@/types/api/master-data/location';
import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Kandang } from '@/types/api/master-data/kandang';
import { Product } from '@/types/api/master-data/product'; import { Product } from '@/types/api/master-data/product';
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
@@ -117,19 +119,40 @@ const InventoryAdjustmentForm = ({
} }
); );
const { rawData: approvedProjectFlockKandangsRawData } =
useSelect<ProjectFlockKandang>(
ProjectFlockKandangApi.basePath,
'id',
'id',
'search',
{
step_name: 'Disetujui',
limit: '100',
}
);
const approvedProjectFlockKandangs = useMemo(() => {
if (
approvedProjectFlockKandangsRawData &&
'data' in approvedProjectFlockKandangsRawData
) {
return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[];
}
return [];
}, [approvedProjectFlockKandangsRawData]);
const { const {
options: projectFlockKandangOptions, setInputValue: setKandangInputValue,
loadMore: loadMoreProjectFlockKandangs, options: kandangOptionsFromApi,
setInputValue: setProjectFlockKandangInputValue, isLoadingOptions: isLoadingKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangOptions, loadMore: loadMoreKandangs,
} = useSelect( } = useSelect<Kandang>(
selectedProjectFlock ? ProjectFlockKandangApi.basePath : '', selectedProjectFlock ? KandangApi.basePath : '',
'kandang.id', 'id',
'kandang.name', 'name',
'search', 'search',
{ {
step_name: 'Disetujui', location_id: selectedProjectFlockLocationId,
project_flock_id: String(selectedProjectFlock?.value),
} }
); );
@@ -162,9 +185,7 @@ 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,
@@ -199,6 +220,26 @@ const InventoryAdjustmentForm = ({
return (product?.flags as string[]) || []; return (product?.flags as string[]) || [];
}, [selectedProduct, productOptions]); }, [selectedProduct, productOptions]);
const kandangOptions = useMemo(() => {
let options: OptionType[] = [];
if (selectedProjectFlock) {
const approvedKandangIds = approvedProjectFlockKandangs
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
.map((pfk) => pfk.kandang_id);
options = kandangOptionsFromApi.filter((kandang) =>
approvedKandangIds.includes(kandang.value as number)
);
}
return options;
}, [
selectedProjectFlock,
kandangOptionsFromApi,
approvedProjectFlockKandangs,
]);
const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>( const formikInitialValues = useMemo<Partial<InventoryAdjustmentFormValues>>(
() => ({ () => ({
location: null, location: null,
@@ -650,10 +691,10 @@ const InventoryAdjustmentForm = ({
label='Kandang' label='Kandang'
value={selectedKandang} value={selectedKandang}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
onInputChange={setProjectFlockKandangInputValue} onInputChange={setKandangInputValue}
options={projectFlockKandangOptions} options={kandangOptions}
onMenuScrollToBottom={loadMoreProjectFlockKandangs} onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingProjectFlockKandangOptions} isLoading={isLoadingKandangOptions}
isError={ isError={
formik.touched.kandang_id && Boolean(formik.errors.kandang_id) formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
} }
@@ -8,7 +8,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import useSWR, { mutate } from 'swr'; import useSWR from 'swr';
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table'; import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
@@ -21,8 +21,6 @@ import { cn } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { useUiStore } from '@/stores/ui/ui.store'; import { useUiStore } from '@/stores/ui/ui.store';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import Button from '@/components/Button'; import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { useSelect } from '@/components/input/SelectInput'; import SelectInput, { useSelect } from '@/components/input/SelectInput';
@@ -43,11 +41,9 @@ import {
const RowOptionsMenu = ({ const RowOptionsMenu = ({
popoverPosition = 'bottom', popoverPosition = 'bottom',
props, props,
deleteClickHandler,
}: { }: {
popoverPosition: 'bottom' | 'top'; popoverPosition: 'bottom' | 'top';
props: CellContext<Movement, unknown>; props: CellContext<Movement, unknown>;
deleteClickHandler: () => void;
}) => { }) => {
const popoverId = `movement#${props.row.original.id}`; const popoverId = `movement#${props.row.original.id}`;
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`; const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
@@ -87,20 +83,6 @@ const RowOptionsMenu = ({
Detail Detail
</Button> </Button>
</RequirePermission> </RequirePermission>
<RequirePermission permissions='lti.inventory.transfer.delete'>
<Button
onClick={() => {
deleteClickHandler();
closePopover();
}}
variant='ghost'
color='error'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='mdi:delete-outline' width={20} height={20} />
Delete
</Button>
</RequirePermission>
</div> </div>
</PopoverContent> </PopoverContent>
</div> </div>
@@ -224,37 +206,12 @@ const MovementTable = () => {
}; };
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [selectedMovement, setSelectedMovement] = useState<
Movement | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const singleDeleteModal = useModal();
const { const { data: movements, isLoading } = useSWR(
data: movements,
isLoading,
mutate: refreshMovements,
} = useSWR(
`${MovementApi.basePath}${getTableFilterQueryString()}`, `${MovementApi.basePath}${getTableFilterQueryString()}`,
MovementApi.getAllFetcher MovementApi.getAllFetcher
); );
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
const response = await MovementApi.delete(selectedMovement?.id as number);
singleDeleteModal.closeModal();
setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Movement!');
refreshMovements();
} else {
toast.error(response?.message || 'Failed to delete Movement');
}
};
useEffect(() => { useEffect(() => {
updateFilter('search', searchValue); updateFilter('search', searchValue);
}, [searchValue, updateFilter]); }, [searchValue, updateFilter]);
@@ -318,27 +275,16 @@ const MovementTable = () => {
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedMovement(props.row.original);
singleDeleteModal.openModal();
};
return ( return (
<RowOptionsMenu <RowOptionsMenu
props={props} props={props}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'} popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/> />
); );
}, },
}, },
], ],
[ [tableFilterState.pageSize, tableFilterState.page]
tableFilterState.pageSize,
tableFilterState.page,
singleDeleteModal,
setSelectedMovement,
]
); );
return ( return (
@@ -509,21 +455,6 @@ const MovementTable = () => {
</div> </div>
</form> </form>
</Modal> </Modal>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</> </>
); );
}; };
@@ -82,7 +82,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: number; warehouse_id: number;
warehouse_name: string; warehouse_name: string;
quantity: number; quantity: number;
transfer_available_qty?: number;
} }
// ===== USE SELECT HOOKS ===== // ===== USE SELECT HOOKS =====
@@ -324,6 +323,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, },
}); });
const { setFieldValue, setFieldTouched, setFieldError } = formik;
const prevSourceWarehouseIdRef = useRef<number | null>( const prevSourceWarehouseIdRef = useRef<number | null>(
formik.values.source_warehouse_id formik.values.source_warehouse_id
); );
@@ -337,14 +338,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
prevSourceWarehouseId !== currentSourceWarehouseId && prevSourceWarehouseId !== currentSourceWarehouseId &&
prevSourceWarehouseId !== null prevSourceWarehouseId !== null
) { ) {
formik.setFieldValue('products', [ setFieldValue('products', [
{ {
product: null, product: null,
product_id: 0, product_id: 0,
product_qty: '', product_qty: '',
}, },
]); ]);
formik.setFieldTouched('products', false); setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map( const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({ (delivery: DeliverySchema) => ({
@@ -358,12 +359,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}) })
); );
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false); setFieldTouched('deliveries', false);
} }
prevSourceWarehouseIdRef.current = currentSourceWarehouseId; prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
}, [formik.values.source_warehouse_id, formik.values.deliveries]); }, [
formik.values.source_warehouse_id,
formik.values.deliveries,
setFieldValue,
setFieldTouched,
]);
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
const { const {
@@ -380,8 +386,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: formik.values.source_warehouse_id warehouse_id: formik.values.source_warehouse_id
? formik.values.source_warehouse_id.toString() ? formik.values.source_warehouse_id.toString()
: '', : '',
transfer_context: 'inventory_transfer',
stock_mode: 'exclude_chickin',
} }
); );
@@ -394,7 +398,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
warehouse_id: pw.warehouse.id, warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name, warehouse_name: pw.warehouse.name,
quantity: pw.quantity, quantity: pw.quantity,
transfer_available_qty: pw.transfer_available_qty,
})) }))
: []; : [];
}, [productWarehouses]); }, [productWarehouses]);
@@ -459,9 +462,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== EVENT HANDLERS ===== // ===== EVENT HANDLERS =====
const handleTransferDateChange = useCallback( const handleTransferDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value); setFieldValue('transfer_date', e.target.value);
}, },
[] [setFieldValue]
); );
const handleSourceWarehouseChange = useCallback( const handleSourceWarehouseChange = useCallback(
@@ -481,14 +484,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
formik.setFieldTouched('source_warehouse', true); setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val); setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true); setFieldTouched('source_warehouse_id', true);
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId); setFieldValue('source_warehouse_id', newSourceWarehouseId);
}, },
[ [
formik.values.destination_warehouse_id, formik.values.destination_warehouse_id,
formik.values.destination_warehouse, formik.values.destination_warehouse,
setFieldTouched,
setFieldValue,
] ]
); );
@@ -509,15 +514,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
formik.setFieldTouched('destination_warehouse', true); setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val); setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true); setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue( setFieldValue('destination_warehouse_id', newDestinationWarehouseId);
'destination_warehouse_id',
newDestinationWarehouseId
);
}, },
[formik.values.source_warehouse_id, formik.values.source_warehouse] [
formik.values.source_warehouse_id,
formik.values.source_warehouse,
setFieldTouched,
setFieldValue,
]
); );
const addProduct = useCallback(() => { const addProduct = useCallback(() => {
@@ -529,15 +536,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
product_qty: '', product_qty: '',
}, },
]; ];
formik.setFieldValue('products', newProducts); setFieldValue('products', newProducts);
}, [formik.values.products]); }, [formik.values.products, setFieldValue]);
const removeProduct = useCallback( const removeProduct = useCallback(
(i: number) => { (i: number) => {
const updatedProducts = formik.values.products?.filter( const updatedProducts = formik.values.products?.filter(
(_, idx) => idx !== i (_, idx) => idx !== i
); );
formik.setFieldValue('products', updatedProducts); setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
@@ -546,7 +553,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setProductQtyErrorShown(false); setProductQtyErrorShown(false);
} }
}, },
[formik.values.products, productQtyErrorShown, setSelectedProducts] [
formik.values.products,
productQtyErrorShown,
setSelectedProducts,
setFieldValue,
]
); );
const bulkRemoveProduct = useCallback(() => { const bulkRemoveProduct = useCallback(() => {
@@ -554,26 +566,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.products?.filter( formik.values.products?.filter(
(_, idx) => !selectedProducts.includes(idx) (_, idx) => !selectedProducts.includes(idx)
) ?? []; ) ?? [];
formik.setFieldValue('products', updatedProducts); setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
if (productQtyErrorShown) { if (productQtyErrorShown) {
toast.dismiss(); toast.dismiss();
setProductQtyErrorShown(false); setProductQtyErrorShown(false);
} }
}, [formik, selectedProducts, setSelectedProducts, productQtyErrorShown]); }, [
selectedProducts,
setSelectedProducts,
productQtyErrorShown,
setFieldValue,
formik.values.products,
]);
const handleProductChange = useCallback( const handleProductChange = useCallback(
(idx: number, val: OptionType | OptionType[] | null) => { (idx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`products.${idx}.product`, true); setFieldTouched(`products.${idx}.product`, true);
formik.setFieldValue(`products.${idx}.product`, val); setFieldValue(`products.${idx}.product`, val);
formik.setFieldTouched(`products.${idx}.product_id`, true); setFieldTouched(`products.${idx}.product_id`, true);
formik.setFieldValue( setFieldValue(
`products.${idx}.product_id`, `products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value (val as ProductWarehouseOptionType)?.value
); );
}, },
[] [setFieldTouched, setFieldValue]
); );
const handleProductSelectAllChange = useCallback( const handleProductSelectAllChange = useCallback(
@@ -600,7 +618,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
const addDelivery = useCallback(() => { const addDelivery = useCallback(() => {
formik.setFieldValue('deliveries', [ setFieldValue('deliveries', [
...(formik.values.deliveries || []), ...(formik.values.deliveries || []),
{ {
delivery_cost: '', delivery_cost: '',
@@ -619,14 +637,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}, },
]); ]);
}, [formik.values.deliveries]); }, [formik.values.deliveries, setFieldValue]);
const removeDelivery = useCallback( const removeDelivery = useCallback(
(i: number) => { (i: number) => {
const updatedDeliveries = formik.values.deliveries?.filter( const updatedDeliveries = formik.values.deliveries?.filter(
(_, idx) => idx !== i (_, idx) => idx !== i
); );
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
@@ -635,7 +653,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setDeliveryQtyErrorShown(false); setDeliveryQtyErrorShown(false);
} }
}, },
[formik.values.deliveries, deliveryQtyErrorShown, setSelectedDeliveries] [
formik.values.deliveries,
deliveryQtyErrorShown,
setSelectedDeliveries,
setFieldValue,
]
); );
const bulkRemoveDelivery = useCallback(() => { const bulkRemoveDelivery = useCallback(() => {
@@ -643,7 +666,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.deliveries?.filter( formik.values.deliveries?.filter(
(_, idx) => !selectedDeliveries.includes(idx) (_, idx) => !selectedDeliveries.includes(idx)
) ?? []; ) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
if (deliveryQtyErrorShown) { if (deliveryQtyErrorShown) {
@@ -651,10 +674,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
setDeliveryQtyErrorShown(false); setDeliveryQtyErrorShown(false);
} }
}, [ }, [
formik,
selectedDeliveries, selectedDeliveries,
setSelectedDeliveries, setSelectedDeliveries,
deliveryQtyErrorShown, deliveryQtyErrorShown,
setFieldValue,
formik.values.deliveries,
]); ]);
const handleDeliverySelectAllChange = useCallback( const handleDeliverySelectAllChange = useCallback(
@@ -684,34 +708,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
const handleDeliveryProductChange = useCallback( const handleDeliveryProductChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => { (deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched( setFieldTouched(`deliveries.${deliveryIdx}.products.0.product`, true);
`deliveries.${deliveryIdx}.products.0.product`, setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
true setFieldTouched(`deliveries.${deliveryIdx}.products.0.product_id`, true);
); setFieldValue(
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${deliveryIdx}.products.0.product_id`, `deliveries.${deliveryIdx}.products.0.product_id`,
(val as OptionType)?.value (val as OptionType)?.value
); );
}, },
[] [setFieldTouched, setFieldValue]
); );
const handleDeliverySupplierChange = useCallback( const handleDeliverySupplierChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => { (deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true); setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val); setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true); setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
formik.setFieldValue( setFieldValue(
`deliveries.${deliveryIdx}.supplier_id`, `deliveries.${deliveryIdx}.supplier_id`,
(val as OptionType)?.value (val as OptionType)?.value
); );
}, },
[] [setFieldTouched, setFieldValue]
); );
const handleDeliveryDocumentChange = useCallback( const handleDeliveryDocumentChange = useCallback(
@@ -723,15 +741,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
e.target.value = ''; e.target.value = '';
return; return;
} }
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file); setFieldValue(`deliveries.${deliveryIdx}.document`, file);
} }
}, },
[] [setFieldValue]
); );
const handleDeliveryCostChange = useCallback( const handleDeliveryCostChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx]; const delivery = formik.values.deliveries?.[idx];
if (delivery) { if (delivery) {
@@ -741,21 +759,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (productQty > 0 && value > 0) { if (productQty > 0 && value > 0) {
const perItem = value / productQty; const perItem = value / productQty;
formik.setFieldValue( setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) { } else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0); setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
} }
} }
}, },
[formik.values.deliveries] [formik.values.deliveries, setFieldValue]
); );
const handleDeliveryCostPerItemChange = useCallback( const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value); setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
const delivery = formik.values.deliveries?.[idx]; const delivery = formik.values.deliveries?.[idx];
if (delivery) { if (delivery) {
@@ -765,13 +780,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (productQty > 0 && value > 0) { if (productQty > 0 && value > 0) {
const totalCost = value * productQty; const totalCost = value * productQty;
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
} else if (value === 0) { } else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0); setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
} }
} }
}, },
[formik.values.deliveries] [formik.values.deliveries, setFieldValue]
); );
const handleDeliveryCostChangeWrapper = useCallback( const handleDeliveryCostChangeWrapper = useCallback(
@@ -838,22 +853,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, [formik.values.products, formik.values.deliveries]); }, [formik.values.products, formik.values.deliveries]);
const getAvailableStock = useCallback( const getAvailableStock = useCallback(
(productId: number) => {
if (type === 'detail') return 0;
const productWarehouse = productWarehouseOptions.find(
(pw) => pw.product_id === productId
);
return (
productWarehouse?.transfer_available_qty ??
productWarehouse?.quantity ??
0
);
},
[productWarehouseOptions, type]
);
const getTotalStock = useCallback(
(productId: number) => { (productId: number) => {
if (type === 'detail') return 0; if (type === 'detail') return 0;
const productWarehouse = productWarehouseOptions.find( const productWarehouse = productWarehouseOptions.find(
@@ -864,16 +863,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[productWarehouseOptions, type] [productWarehouseOptions, type]
); );
const hasAvailableQty = useCallback(
(productId: number) => {
const productWarehouse = productWarehouseOptions.find(
(pw) => pw.product_id === productId
);
return productWarehouse?.transfer_available_qty !== undefined;
},
[productWarehouseOptions]
);
const getProductQtyBottomLabel = useCallback( const getProductQtyBottomLabel = useCallback(
(productIdx: number) => { (productIdx: number) => {
if (type === 'detail') return undefined; if (type === 'detail') return undefined;
@@ -881,31 +870,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!product || !product.product_id) return undefined; if (!product || !product.product_id) return undefined;
const availableStock = getAvailableStock(product.product_id); const availableStock = getAvailableStock(product.product_id);
const totalStock = getTotalStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0; const requestedQty = Number(product.product_qty) || 0;
const remainingStock = availableStock - requestedQty; const remainingStock = availableStock - requestedQty;
const isAyamProduct = hasAvailableQty(product.product_id);
if (requestedQty > 0) { if (requestedQty > 0) {
if (isAyamProduct) {
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
}
return `Sisa: ${formatNumber(remainingStock)}`; return `Sisa: ${formatNumber(remainingStock)}`;
} }
if (isAyamProduct) {
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
}
return `Tersedia: ${formatNumber(availableStock)}`; return `Tersedia: ${formatNumber(availableStock)}`;
}, },
[ [formik.values.products, getAvailableStock, type]
formik.values.products,
getAvailableStock,
getTotalStock,
hasAvailableQty,
type,
]
); );
const getDeliveryProductQtyBottomLabel = useCallback( const getDeliveryProductQtyBottomLabel = useCallback(
@@ -967,26 +941,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!product || !product.product_id) return null; if (!product || !product.product_id) return null;
const availableStock = getAvailableStock(product.product_id); const availableStock = getAvailableStock(product.product_id);
const totalStock = getTotalStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0; const requestedQty = Number(product.product_qty) || 0;
const isAyamProduct = hasAvailableQty(product.product_id);
if (requestedQty > availableStock) { if (requestedQty > availableStock) {
if (isAyamProduct) {
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
}
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`; return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
} }
return null; return null;
}, },
[ [formik.values.products, getAvailableStock, type]
formik.values.products,
getAvailableStock,
getTotalStock,
hasAvailableQty,
type,
]
); );
const validateDeliveryQty = useCallback( const validateDeliveryQty = useCallback(
@@ -1100,12 +1063,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return !validateDeliveryQty(deliveryIdx, productIdx, qty); return !validateDeliveryQty(deliveryIdx, productIdx, qty);
}) })
) ?? []), ) ?? []),
[ [formik.values.deliveries, validateDeliveryQty, type]
formik.values.deliveries,
formik.values.products,
validateDeliveryQty,
type,
]
); );
const hasInvalidQty = useMemo( const hasInvalidQty = useMemo(
@@ -1122,6 +1080,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
}, [formik.values.products, getProductQtyError, type]); }, [formik.values.products, getProductQtyError, type]);
const deliveryCostDepString = useMemo(
() =>
formik.values.deliveries
?.map((d, idx) => ({
idx,
productQty: d.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
),
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
deliveryCostPerItem:
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
}))
.map(
(item) =>
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
)
.join('|'),
[formik.values.deliveries]
);
// ===== EFFECTS ===== // ===== EFFECTS =====
useEffect(() => { useEffect(() => {
formik.values.deliveries?.forEach((delivery, idx) => { formik.values.deliveries?.forEach((delivery, idx) => {
@@ -1138,36 +1117,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (deliveryCost > 0 && productQty > 0) { if (deliveryCost > 0 && productQty > 0) {
const perItem = deliveryCost / productQty; const perItem = deliveryCost / productQty;
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) { if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
formik.setFieldValue( setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, perItem);
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} }
} else if (deliveryCostPerItem > 0 && productQty > 0) { } else if (deliveryCostPerItem > 0 && productQty > 0) {
const totalCost = deliveryCostPerItem * productQty; const totalCost = deliveryCostPerItem * productQty;
if (Math.abs(deliveryCost - totalCost) > 0.01) { if (Math.abs(deliveryCost - totalCost) > 0.01) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
} }
} }
}); });
}, [ }, [deliveryCostDepString, setFieldValue, formik.values.deliveries]);
formik.values.deliveries
?.map((d, idx) => ({
idx,
productQty: d.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
),
deliveryCost: parseInt((d.delivery_cost || '').toString()) || 0,
deliveryCostPerItem:
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
}))
.map(
(item) =>
`${item.idx}:${item.productQty}:${item.deliveryCost}:${item.deliveryCostPerItem}`
)
.join('|'),
]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -1177,7 +1136,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
!isInitialized !isInitialized
) { ) {
if (formik.values.products.length === 0) { if (formik.values.products.length === 0) {
formik.setFieldValue('products', [ setFieldValue('products', [
{ {
product: null, product: null,
product_id: 0, product_id: 0,
@@ -1186,7 +1145,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
]); ]);
} }
if (formik.values.deliveries.length === 0) { if (formik.values.deliveries.length === 0) {
formik.setFieldValue('deliveries', [ setFieldValue('deliveries', [
{ {
delivery_cost: undefined, delivery_cost: undefined,
delivery_cost_per_item: undefined, delivery_cost_per_item: undefined,
@@ -1208,7 +1167,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
} }
setIsInitialized(true); setIsInitialized(true);
} }
}, [formik.values.source_warehouse_id, isInitialized, type]); }, [
formik.values.source_warehouse_id,
isInitialized,
type,
setFieldValue,
formik.values.products.length,
formik.values.deliveries.length,
]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -1217,7 +1183,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.values.source_warehouse_id === formik.values.source_warehouse_id ===
formik.values.destination_warehouse_id formik.values.destination_warehouse_id
) { ) {
formik.setFieldError( setFieldError(
'destination_warehouse_id', 'destination_warehouse_id',
'Gudang tujuan tidak boleh sama dengan gudang asal!' 'Gudang tujuan tidak boleh sama dengan gudang asal!'
); );
@@ -1226,13 +1192,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
formik.errors.destination_warehouse_id === formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!' 'Gudang tujuan tidak boleh sama dengan gudang asal!'
) { ) {
formik.setFieldError('destination_warehouse_id', undefined); setFieldError('destination_warehouse_id', undefined);
} }
} }
}, [ }, [
formik.values.source_warehouse_id, formik.values.source_warehouse_id,
formik.values.destination_warehouse_id, formik.values.destination_warehouse_id,
formik.errors.destination_warehouse_id, formik.errors.destination_warehouse_id,
setFieldError,
]); ]);
useEffect(() => { useEffect(() => {
@@ -1268,29 +1235,37 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
if (hasChanges) { if (hasChanges) {
formik.setFieldValue('deliveries', updatedDeliveries); setFieldValue('deliveries', updatedDeliveries);
} }
} }
}, [formik.values.products]); }, [formik.values.products, formik.values.deliveries, setFieldValue]);
const productQtyDepString = useMemo(
() => formik.values.products?.map((p) => p.product_qty).join(','),
[formik.values.products]
);
useEffect(() => { useEffect(() => {
if (productQtyErrorShown) { if (productQtyErrorShown) {
toast.dismiss(); toast.dismiss();
setProductQtyErrorShown(false); setProductQtyErrorShown(false);
} }
}, [formik.values.products?.map((p) => p.product_qty).join(',')]); }, [productQtyErrorShown]);
const deliveryProductQtyDepString = useMemo(
() =>
formik.values.deliveries
?.map((d) => d.products.map((p) => p.product_qty).join(','))
.join('|'),
[formik.values.deliveries]
);
useEffect(() => { useEffect(() => {
if (deliveryQtyErrorShown) { if (deliveryQtyErrorShown) {
toast.dismiss(); toast.dismiss();
setDeliveryQtyErrorShown(false); setDeliveryQtyErrorShown(false);
} }
}, [ }, [deliveryProductQtyDepString, productQtyDepString, deliveryQtyErrorShown]);
formik.values.deliveries
?.map((d) => d.products.map((p) => p.product_qty).join(','))
.join('|'),
formik.values.products?.map((p) => p.product_qty).join(','),
]);
useEffect(() => { useEffect(() => {
if (hasExceededStock && !productQtyErrorShown && type !== 'detail') { if (hasExceededStock && !productQtyErrorShown && type !== 'detail') {
@@ -536,9 +536,13 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
formModal.closeModal(); formModal.closeModal();
}; };
const hasLoadedInitialValues = useRef(false);
useEffect(() => { useEffect(() => {
const getFilledInitialValues = async () => { const getFilledInitialValues = async () => {
if (marketingId && isResponseSuccess(marketing)) { if (marketingId && isResponseSuccess(marketing)) {
if (hasLoadedInitialValues.current) return;
hasLoadedInitialValues.current = true;
const filledInitialValues = await getFilledMarketingFormInitialValues( const filledInitialValues = await getFilledMarketingFormInitialValues(
marketing.data marketing.data
); );
@@ -582,9 +586,15 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => {
setFormErrorMessage(''); setFormErrorMessage('');
}, [step]); }, [step]);
// sync delivery order values to formik const prevDeliveryOrderValuesRef = useRef(deliveryOrderValues);
useEffect(() => { useEffect(() => {
formik.setFieldValue('delivery_order', deliveryOrderValues); if (
JSON.stringify(prevDeliveryOrderValuesRef.current) !==
JSON.stringify(deliveryOrderValues)
) {
prevDeliveryOrderValuesRef.current = deliveryOrderValues;
formik.setFieldValue('delivery_order', deliveryOrderValues);
}
}, [deliveryOrderValues]); }, [deliveryOrderValues]);
const grandTotal = useMemo(() => { const grandTotal = useMemo(() => {
@@ -226,11 +226,6 @@ const MarketingTable = () => {
confirmationModal.openModal(); confirmationModal.openModal();
}; };
const productsClickHandler = (item: Marketing) => {
setSelectedItem(item);
productsModal.openModal();
};
const deleteMarketingHandler = async () => { const deleteMarketingHandler = async () => {
const deleteMarketingRes = await MarketingApi.delete( const deleteMarketingRes = await MarketingApi.delete(
selectedItem?.id as number selectedItem?.id as number
@@ -450,6 +445,11 @@ const MarketingTable = () => {
accessorKey: 'marketing_products.length', accessorKey: 'marketing_products.length',
header: 'Product Details', header: 'Product Details',
cell: (props) => { cell: (props) => {
const productsClickHandler = (item: Marketing) => {
setSelectedItem(item);
productsModal.openModal();
};
if (props?.row?.original?.sales_order?.length) { if (props?.row?.original?.sales_order?.length) {
if (props?.row?.original?.sales_order?.length > 1) { if (props?.row?.original?.sales_order?.length > 1) {
return ( return (
@@ -504,7 +504,7 @@ const MarketingTable = () => {
}, },
}, },
]; ];
}, []); }, [deleteModal, deliveryModal, setSelectedItem, productsModal]);
return ( return (
<> <>
@@ -458,9 +458,13 @@ const SalesOrderFormModal = ({
); );
}, [memoSalesOrder]); }, [memoSalesOrder]);
const hasLoadedInitialValues = useRef(false);
useEffect(() => { useEffect(() => {
const getFilledInitialValues = async () => { const getFilledInitialValues = async () => {
if (marketingId && isResponseSuccess(marketing)) { if (marketingId && isResponseSuccess(marketing)) {
if (hasLoadedInitialValues.current) return;
hasLoadedInitialValues.current = true;
const filledInitialValues = await getFilledMarketingFormInitialValues( const filledInitialValues = await getFilledMarketingFormInitialValues(
marketing.data marketing.data
); );
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { import {
DeliveryOrderProductFormValues, DeliveryOrderProductFormValues,
DeliveryOrderProductSchema, DeliveryOrderProductSchema,
@@ -224,6 +224,8 @@ const DeliveryOrderProductForm = ({
}, },
}); });
const { resetForm } = formik;
const hasWeekField = useMemo(() => { const hasWeekField = useMemo(() => {
const marketingType = formik.values.marketing_type?.value?.toLowerCase(); const marketingType = formik.values.marketing_type?.value?.toLowerCase();
if (marketingType === 'ayam_pullet') { if (marketingType === 'ayam_pullet') {
@@ -243,9 +245,9 @@ const DeliveryOrderProductForm = ({
return false; return false;
}, [formik.values.marketing_product, formik.values.marketing_type]); }, [formik.values.marketing_product, formik.values.marketing_type]);
const handleResetForm = () => { const handleResetForm = useCallback(() => {
setFormErrorMessage(''); setFormErrorMessage('');
formik.resetForm({ resetForm({
values: { values: {
delivery_date: '', delivery_date: '',
vehicle_number: '', vehicle_number: '',
@@ -269,17 +271,20 @@ const DeliveryOrderProductForm = ({
}, },
}); });
// setSelectedProduct(null); // setSelectedProduct(null);
}; }, [resetForm]);
const handleBlurField = (field: string) => { const handleBlurField = useCallback(
setCurrentInput(field); (field: string) => {
setCurrentInput(field);
handleMarketingCalculation(field, { handleMarketingCalculation(field, {
values: formik.values, values: formik.values,
setFieldValue: formik.setFieldValue, setFieldValue: formik.setFieldValue,
hasSisaBerat, hasSisaBerat,
}); });
}; },
[formik.values, formik.setFieldValue, hasSisaBerat]
);
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty) // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
const handleFieldChange = ( const handleFieldChange = (
@@ -324,8 +329,12 @@ const DeliveryOrderProductForm = ({
const { setValues: setFormikValues } = formik; const { setValues: setFormikValues } = formik;
const processedInitialValuesRef = useRef<number | null>(null);
useEffect(() => { useEffect(() => {
if (initialValues) { if (initialValues) {
if (processedInitialValuesRef.current === initialValues.id) return;
processedInitialValuesRef.current = initialValues.id as number;
if (!Boolean(initialValues.qty)) { if (!Boolean(initialValues.qty)) {
handleResetForm(); handleResetForm();
} else { } else {
@@ -338,7 +347,7 @@ const DeliveryOrderProductForm = ({
} }
} }
} }
}, [initialValues]); }, [handleResetForm, initialValues, setFormikValues]);
// ===== Formik Error List ===== // ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList( const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
@@ -356,8 +365,10 @@ const DeliveryOrderProductForm = ({
); );
useEffect(() => { useEffect(() => {
handleBlurField('week'); if (formik.values.week) {
}, [formik.values.week]); handleBlurField('week');
}
}, [formik.values.week, handleBlurField]);
return ( return (
<> <>
@@ -5,7 +5,7 @@ import {
SalesOrderProductFormValues, SalesOrderProductFormValues,
SalesOrderProductSchema, SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useEffect, useMemo, useState } from 'react'; import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import { OptionType, useSelect } from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { WarehouseApi } from '@/services/api/master-data'; import { WarehouseApi } from '@/services/api/master-data';
@@ -240,15 +240,18 @@ const SalesOrderProductForm = ({
}); });
}; };
const handleBlurField = (field: string) => { const handleBlurField = useCallback(
setCurrentInput(field); (field: string) => {
setCurrentInput(field);
handleMarketingCalculation(field, { handleMarketingCalculation(field, {
values: formik.values, values: formik.values,
setFieldValue: formik.setFieldValue, setFieldValue: formik.setFieldValue,
hasSisaBerat, hasSisaBerat,
}); });
}; },
[formik.values, formik.setFieldValue, hasSisaBerat]
);
// Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty) // Handler untuk onChange - auto calculation real-time untuk field yang mempengaruhi total_price (total_peti, weight_per_convertion, price_per_convertion, sisa_berat, price_sisa_berat, price_per_qty, qty)
const handleFieldChange = ( const handleFieldChange = (
@@ -307,8 +310,10 @@ const SalesOrderProductForm = ({
); );
useEffect(() => { useEffect(() => {
handleBlurField('week'); if (formik.values.week) {
}, [formik.values.week]); handleBlurField('week');
}
}, [formik.values.week, handleBlurField]);
return ( return (
<> <>
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper'; import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
interface DeliveryOrderExportProps { interface DeliveryOrderExportProps {
data?: Marketing; data?: Marketing;
@@ -20,9 +19,6 @@ const DeliveryOrderExport = ({
}: DeliveryOrderExportProps) => { }: DeliveryOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data; const salesData = data;
const searchParams = useSearchParams();
const action = searchParams.get('action');
const id = searchParams.get('id');
const handleDownloadPDF = async () => { const handleDownloadPDF = async () => {
if (!salesData) { if (!salesData) {
@@ -53,7 +49,6 @@ const DeliveryOrderExport = ({
toast.error('Failed to generate PDF. Please try again.'); toast.error('Failed to generate PDF. Please try again.');
} finally { } finally {
setIsGeneratingPDF(false); setIsGeneratingPDF(false);
window.location.href = `/marketing?action=${action}&id=${id}`;
} }
}; };
@@ -92,7 +87,7 @@ const PDFDocument = ({
return ( return (
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0 deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
); );
}, []); }, [deliveryOrder.deliveries]);
return ( return (
<Document> <Document>
@@ -6,7 +6,6 @@ import { useMemo, useState } from 'react';
import { formatDate, formatNumber } from '@/lib/helper'; import { formatDate, formatNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
interface SalesOrderExportProps { interface SalesOrderExportProps {
data?: Marketing; data?: Marketing;
@@ -16,9 +15,6 @@ interface SalesOrderExportProps {
const SalesOrderExport = ({ data }: SalesOrderExportProps) => { const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data; const salesData = data;
const searchParams = useSearchParams();
const action = searchParams.get('action');
const id = searchParams.get('id');
const handleDownloadPDF = async () => { const handleDownloadPDF = async () => {
if (!salesData) { if (!salesData) {
@@ -47,7 +43,6 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
toast.error('Failed to generate PDF. Please try again.'); toast.error('Failed to generate PDF. Please try again.');
} finally { } finally {
setIsGeneratingPDF(false); setIsGeneratingPDF(false);
window.location.href = `/marketing?action=${action}&id=${id}`;
} }
}; };
@@ -314,10 +314,6 @@ const KandangsTable = () => {
accessorFn: (row) => row.pic?.name ?? '-', accessorFn: (row) => row.pic?.name ?? '-',
header: 'PIC', header: 'PIC',
}, },
{
accessorFn: (row) => row.kandang_group?.name ?? '-',
header: 'Kandang Group',
},
{ {
header: 'Aksi', header: 'Aksi',
cell: (props: CellContext<Kandang, unknown>) => { cell: (props: CellContext<Kandang, unknown>) => {
@@ -1,4 +1,3 @@
import { OptionType } from '@/components/input/SelectInput';
import * as Yup from 'yup'; import * as Yup from 'yup';
type KandangFormSchemaType = { type KandangFormSchemaType = {
@@ -20,7 +19,6 @@ type KandangFormSchemaType = {
} }
| undefined | undefined
| null; | null;
group?: OptionType;
}; };
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> = export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
@@ -44,11 +42,6 @@ export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
group: Yup.object({
value: Yup.number().min(1).required('Kandang Grup wajib diisi!'),
label: Yup.string().required('Kandang Grup wajib diisi!'),
}).required('Kandang Grup wajib diisi!'),
}); });
export const UpdateKandangFormSchema = KandangFormSchema; export const UpdateKandangFormSchema = KandangFormSchema;
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getIn, useFormik } from 'formik'; import { useFormik } from 'formik';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -34,8 +34,6 @@ import NumberInput from '@/components/input/NumberInput';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { User } from '@/types/api/api-general'; import { User } from '@/types/api/api-general';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
interface KandangFormProps { interface KandangFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -98,12 +96,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
label: initialValues.pic.name, label: initialValues.pic.name,
} }
: null, : null,
group: initialValues?.kandang_group
? {
value: initialValues.kandang_group.id,
label: initialValues.kandang_group.name,
}
: undefined,
}; };
}, [initialValues]); }, [initialValues]);
@@ -119,7 +111,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
location_id: values.locationId!, location_id: values.locationId!,
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0, capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
pic_id: values.picId!, pic_id: values.picId!,
group_id: values.group?.value as number,
}; };
switch (type) { switch (type) {
@@ -171,23 +162,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
formik.setFieldValue('picId', (val as OptionType)?.value); formik.setFieldValue('picId', (val as OptionType)?.value);
}; };
// Kandang Group
const {
setInputValue: setKandangGroupSelectInputValue,
options: kandangGroupOptions,
isLoadingOptions: isLoadingKandangGroupOptions,
loadMore: loadMoreKandangGroups,
} = useSelect<DailyChecklistKandang>(
DailyChecklistKandangApi.basePath,
'id',
'name'
);
const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('group', true);
formik.setFieldValue('group', val);
};
const deleteKandangClickHandler = () => { const deleteKandangClickHandler = () => {
deleteModal.openModal(); deleteModal.openModal();
}; };
@@ -295,24 +269,6 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable
/> />
<SelectInput
required
label='Kandang Group'
value={formik.values.group ?? undefined}
onChange={kandangGroupChangeHandler}
options={kandangGroupOptions}
onInputChange={setKandangGroupSelectInputValue}
onMenuScrollToBottom={loadMoreKandangGroups}
isLoading={isLoadingKandangGroupOptions}
isError={formik.touched.group && Boolean(formik.errors.group)}
errorMessage={
getIn(formik.errors.group, 'value') ??
(formik.errors.group as string)
}
isDisabled={type === 'detail'}
isClearable
/>
</div> </div>
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
@@ -44,7 +44,9 @@ const ChickinFormKandang = ({
const afterSubmitFormChickin = () => { const afterSubmitFormChickin = () => {
setOpenChickin(true); setOpenChickin(true);
afterSubmit && afterSubmit(); if (afterSubmit) {
afterSubmit();
}
refreshApprovals(); refreshApprovals();
}; };
@@ -23,7 +23,7 @@ const ChickinLogsView = ({
rawDataApprovals: BaseApproval[]; rawDataApprovals: BaseApproval[];
}) => { }) => {
const [chickinErrorMessage, setChickinErrorMessage] = useState(''); const [chickinErrorMessage, setChickinErrorMessage] = useState('');
const { openChickinApproveModal, openChickinDeleteModal } = useChickinStore(); const { openChickinApproveModal } = useChickinStore();
const handleClickApprove = () => { const handleClickApprove = () => {
openChickinApproveModal(initialValues, async (notes?: string) => { openChickinApproveModal(initialValues, async (notes?: string) => {
@@ -40,21 +40,8 @@ const ChickinLogsView = ({
toast.error(approveChickinRes?.message as string); toast.error(approveChickinRes?.message as string);
setChickinErrorMessage(approveChickinRes?.message as string); setChickinErrorMessage(approveChickinRes?.message as string);
} }
afterSubmit && afterSubmit(); if (afterSubmit) {
}); afterSubmit();
};
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');
} }
}); });
}; };
@@ -101,30 +88,14 @@ 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' : '-' }
} color={
color={ 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,7 +200,6 @@ 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'
@@ -215,11 +214,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
chickinApproveCallback, chickinApproveCallback,
closeChickinApproveModal, closeChickinApproveModal,
setChickinApproveLoading, setChickinApproveLoading,
isChickinDeleteModalOpen,
isChickinDeleteLoading,
chickinDeleteCallback,
closeChickinDeleteModal,
setChickinDeleteLoading,
} = useChickinStore(); } = useChickinStore();
const { const {
@@ -484,14 +478,6 @@ 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();
@@ -1222,38 +1208,6 @@ 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}
@@ -21,7 +21,6 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import PopoverButton from '@/components/popover/PopoverButton'; import PopoverButton from '@/components/popover/PopoverButton';
import PopoverContent from '@/components/popover/PopoverContent'; import PopoverContent from '@/components/popover/PopoverContent';
import Tooltip from '@/components/Tooltip';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { AreaApi } from '@/services/api/master-data'; import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data';
@@ -37,7 +36,6 @@ import {
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton'; import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { type Recording } from '@/types/api/production/recording'; import { type Recording } from '@/types/api/production/recording';
import { getRecordingRestriction } from './recording-utils';
import { RecordingApi } from '@/services/api/production'; import { RecordingApi } from '@/services/api/production';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -106,76 +104,20 @@ const RowOptionsMenu = ({
return recording.approval?.action === 'REJECTED'; return recording.approval?.action === 'REJECTED';
}; };
const isRecordingEditable = (recording: Recording) => {
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return false;
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
const restriction = getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
if (restriction.isLocked) {
return false;
}
return true;
};
const getRecordingRestrictionInfo = (recording: Recording) => {
const isGrowingCategory =
recording.project_flock?.project_flock_category === 'GROWING';
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
if (isGrowingLockedByLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
};
}
const currentIsLaying =
recording.project_flock?.project_flock_category === 'LAYING';
return getRecordingRestriction(
recording.is_laying,
recording.is_transition,
currentIsLaying
);
};
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);
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
return ( return (
<div className='relative'> <div className='relative'>
<Tooltip <PopoverButton
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''} tabIndex={0}
position='top' variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
> >
<PopoverButton <Icon icon='material-symbols:more-vert' width={16} height={16} />
tabIndex={0} </PopoverButton>
variant='ghost'
color='none'
popoverTarget={popoverId}
anchorName={popoverAnchorName}
className={restrictionInfo.isLocked ? 'text-error' : ''}
>
<Icon icon='material-symbols:more-vert' width={16} height={16} />
</PopoverButton>
</Tooltip>
<PopoverContent <PopoverContent
id={popoverId} id={popoverId}
@@ -196,20 +138,18 @@ 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}`} variant='ghost'
variant='ghost' color='none'
color='none' className='p-3 justify-start text-sm font-semibold w-full'
className='p-3 justify-start text-sm font-semibold w-full' onClick={closePopover}
onClick={closePopover} >
> <Icon icon='mdi:pencil-outline' width={20} height={20} />
<Icon icon='mdi:pencil-outline' width={20} height={20} /> Edit
Edit </Button>
</Button> </RequirePermission>
</RequirePermission>
)}
{!isApproved && !isRejected && ( {!isApproved && !isRejected && (
<RequirePermission permissions='lti.production.recording.approve'> <RequirePermission permissions='lti.production.recording.approve'>
<Button <Button
@@ -242,22 +182,20 @@ const RowOptionsMenu = ({
</Button> </Button>
</RequirePermission> </RequirePermission>
)} )}
{isEditable && ( <RequirePermission permissions='lti.production.recording.delete'>
<RequirePermission permissions='lti.production.recording.delete'> <Button
<Button onClick={() => {
onClick={() => { deleteClickHandler();
deleteClickHandler(); closePopover();
closePopover(); }}
}} variant='ghost'
variant='ghost' color='error'
color='error' className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content' >
> <Icon icon='mdi:delete-outline' width={20} height={20} />
<Icon icon='mdi:delete-outline' width={20} height={20} /> Delete
Delete </Button>
</Button> </RequirePermission>
</RequirePermission>
)}
</div> </div>
</PopoverContent> </PopoverContent>
</div> </div>
@@ -607,17 +545,12 @@ const RecordingTable = () => {
const singleDeleteHandler = async () => { const singleDeleteHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
const response = await RecordingApi.delete(selectedRecording?.id as number); await RecordingApi.delete(selectedRecording?.id as number);
refreshRecordings();
singleDeleteModal.closeModal(); singleDeleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false); setIsDeleteLoading(false);
if (isResponseSuccess(response)) {
toast.success(response?.message || 'Successfully delete Recording!');
refreshRecordings();
} else {
toast.error(response?.message || 'Failed to delete Recording');
}
}; };
const approveHandler = async (notes: string) => { const approveHandler = async (notes: string) => {
@@ -813,30 +746,11 @@ const RecordingTable = () => {
{ {
header: 'Kategori', header: 'Kategori',
cell: (props) => { cell: (props) => {
const isTransition = props.row.original.is_transition;
const category = const category =
props.row.original.project_flock?.project_flock_category || props.row.original.project_flock?.project_flock_category;
'GROWING'; if (!category) return '-';
const color = category === 'LAYING' ? 'info' : 'warning'; const color = category === 'LAYING' ? 'info' : 'warning';
return <StatusBadge color={color} text={formatTitleCase(category)} />;
const isGrowingLocked =
category === 'GROWING' && props.row.original.is_laying;
return (
<div className='flex flex-col gap-1'>
<StatusBadge color={color} text={formatTitleCase(category)} />
{isTransition && (
<span className='text-xs text-warning font-medium'>
(Transisi)
</span>
)}
{isGrowingLocked && (
<span className='text-xs text-error font-medium'>
(Penguncian)
</span>
)}
</div>
);
}, },
}, },
{ {
File diff suppressed because it is too large Load Diff
@@ -1,73 +0,0 @@
export type RecordingRestriction = {
canEditStock: boolean;
canEditDepletion: boolean;
canEditEgg: boolean;
isLocked: boolean;
lockReason?: string;
};
export const getRecordingRestriction = (
isLaying: boolean,
isTransition: boolean,
currentIsLaying?: boolean
): RecordingRestriction => {
if (isTransition && !isLaying) {
const isLayingKandangInTransition = currentIsLaying === true;
if (isLayingKandangInTransition) {
return {
canEditStock: false,
canEditDepletion: true,
canEditEgg: true,
isLocked: false,
lockReason: undefined,
};
} else {
return {
canEditStock: true,
canEditDepletion: false,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
}
if (!isLaying && !isTransition && currentIsLaying) {
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason:
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
};
}
if (!isLaying && !isTransition) {
return {
canEditStock: true,
canEditDepletion: true,
canEditEgg: false,
isLocked: false,
lockReason: undefined,
};
}
if (isLaying && !isTransition) {
return {
canEditStock: true,
canEditDepletion: true,
canEditEgg: true,
isLocked: false,
lockReason: undefined,
};
}
return {
canEditStock: false,
canEditDepletion: false,
canEditEgg: false,
isLocked: true,
lockReason: 'Kondisi transisi tidak valid',
};
};
@@ -50,18 +50,12 @@ 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(
isValidId transferToLayingId
? ['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>[] =
@@ -279,16 +273,12 @@ const TransferToLayingConfirmationModal = ({
{transferToLayingIds && {transferToLayingIds &&
!transferToLayingForm && !transferToLayingForm &&
transferToLayingIds transferToLayingIds.map((transferToLayingId, idx) => (
.filter( <TransferToLayingConfirmationModalTable
(id) => id !== undefined && id !== null && !isNaN(id) && id > 0 key={idx}
) transferToLayingId={transferToLayingId}
.map((transferToLayingId, idx) => ( />
<TransferToLayingConfirmationModalTable ))}
key={idx}
transferToLayingId={transferToLayingId}
/>
))}
{withNote && ( {withNote && (
<TextArea <TextArea
@@ -82,7 +82,7 @@ const TransferToLayingDetailModal = () => {
if (modalAction === 'detail') { if (modalAction === 'detail') {
detailModal.openModal(); detailModal.openModal();
} }
}, [modalAction]); }, [modalAction, detailModal]);
return ( return (
<Modal <Modal
@@ -229,8 +229,6 @@ const TransferToLayingFormModal = () => {
ProjectFlock | undefined ProjectFlock | undefined
>(undefined); >(undefined);
const [maxSourceQuantity, setMaxSourceQuantity] = useState<number>(0);
const selectedFlockDestinationRawData = isResponseSuccess( const selectedFlockDestinationRawData = isResponseSuccess(
flockDestinationRawData flockDestinationRawData
) )
@@ -355,14 +353,19 @@ 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 =
maxSourceQuantity - totalTransferedChicken; totalEnteredChickenForTransfer - totalTransferedChicken;
const isNextButtonDisabled = useMemo(() => { const isNextButtonDisabled = useMemo(() => {
if (step === 1) { if (step === 1) {
@@ -394,7 +397,6 @@ 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);
}; };
@@ -402,7 +404,6 @@ 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 = (
@@ -468,26 +469,6 @@ 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
@@ -602,9 +583,14 @@ const TransferToLayingFormModal = () => {
k.kandang.value === item.project_flock_kandang_id k.kandang.value === item.project_flock_kandang_id
); );
const flockSourceKandangRadioChangeHandler = () => { const flockSourceKandangCheckboxChangeHandler: FormEventHandler<
if (isAvailable) { HTMLInputElement
> = (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,
@@ -614,7 +600,15 @@ const TransferToLayingFormModal = () => {
maxQuantity: item.available_qty, maxQuantity: item.available_qty,
}, },
]); ]);
setMaxSourceQuantity(item.available_qty); } else {
formik.setFieldValue(
'flockSourceKandangs',
formik.values.flockSourceKandangs.filter(
(k) =>
k.kandang.value !==
item.project_flock_kandang_id
)
);
} }
}; };
@@ -624,22 +618,28 @@ 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'>
<input <CheckboxInput
type='radio' name={`flockSourceKandang.${itemIdx}.value`}
name='flockSourceKandang'
value={item.project_flock_kandang_id} value={item.project_flock_kandang_id}
checked={isChecked} checked={isChecked}
onChange={flockSourceKandangRadioChangeHandler} onChange={
flockSourceKandangCheckboxChangeHandler
}
size='md'
disabled={!isAvailable} disabled={!isAvailable}
className={cn('radio radio-md radio-primary', { classNames={{
'opacity-50 cursor-not-allowed': !isAvailable, checkbox: cn({
})} '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 opacity-50': !isAvailable, 'cursor-not-allowed': !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 pada Kandang Tujuan' placeholder='Masukkan Kuantitas'
value={item.quantity} value={item.quantity}
onChange={formik.handleChange} onChange={formik.handleChange}
isError={isInvalid} isError={isInvalid}
@@ -875,8 +875,6 @@ 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',
@@ -1002,7 +1000,7 @@ const TransferToLayingFormModal = () => {
isError={totalAvailableChickenForTransfer < 0} isError={totalAvailableChickenForTransfer < 0}
errorMessage={ errorMessage={
totalAvailableChickenForTransfer < 0 totalAvailableChickenForTransfer < 0
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)` ? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
: '' : ''
} }
disabled disabled
@@ -48,11 +48,11 @@ const RowOptionsMenu = ({
popoverPosition: 'bottom' | 'top'; popoverPosition: 'bottom' | 'top';
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const showEditButton = props.row.original.approval.action !== 'APPROVED'; const showEditButton =
props.row.original.approval.action !== 'APPROVED' &&
props.row.original.approval.action !== 'REJECTED';
const showDeleteButton = const showDeleteButton = showEditButton;
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]); }, [sorting, updateFilter]);
return ( return (
<> <>
@@ -60,25 +60,6 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`); router.push(`/production/uniformity?action=reject&id=${initialValues.id}`);
}; };
const handleViewUniformityDetails = () => {
if (!uniformity_details || uniformity_details.length === 0) {
setShouldFetchDetails(true);
return;
}
setExpandedDrawerContent(
<UniformityDetailsPreview
info_umum={initialValues.info_umum}
uniformity_details={uniformity_details}
uniformityId={initialValues.id}
/>
);
setTimeout(() => {
setExpandedDrawerOpen(true);
}, 0);
};
useEffect(() => { useEffect(() => {
if ( if (
shouldFetchDetails && shouldFetchDetails &&
@@ -202,6 +183,25 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
} }
if (id === 'document-name') { if (id === 'document-name') {
const handleViewUniformityDetails = () => {
if (!uniformity_details || uniformity_details.length === 0) {
setShouldFetchDetails(true);
return;
}
setExpandedDrawerContent(
<UniformityDetailsPreview
info_umum={initialValues.info_umum}
uniformity_details={uniformity_details}
uniformityId={initialValues.id}
/>
);
setTimeout(() => {
setExpandedDrawerOpen(true);
}, 0);
};
return ( return (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span>{valueMap[id]}</span> <span>{valueMap[id]}</span>
@@ -231,7 +231,14 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
}, },
}, },
], ],
[initialValues, handleViewUniformityDetails, isLoading] [
initialValues,
isLoading,
uniformity_details,
setShouldFetchDetails,
setExpandedDrawerContent,
setExpandedDrawerOpen,
]
); );
const samplingTableData: DetailOptionType[] = useMemo(() => { const samplingTableData: DetailOptionType[] = useMemo(() => {
-12
View File
@@ -20,7 +20,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
'lti.daily_checklist.master_data.employee', 'lti.daily_checklist.master_data.employee',
'lti.daily_checklist.master_data.activity', 'lti.daily_checklist.master_data.activity',
'lti.daily_checklist.master_data.configuration', 'lti.daily_checklist.master_data.configuration',
'lti.daily_checklist.master_data.kandang',
], ],
submenu: [ submenu: [
{ {
@@ -67,11 +66,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
link: '/daily-checklist/master-data/activity', link: '/daily-checklist/master-data/activity',
permission: ['lti.daily_checklist.master_data.activity'], permission: ['lti.daily_checklist.master_data.activity'],
}, },
{
text: 'Kandang',
link: '/daily-checklist/master-data/kandang',
permission: ['lti.daily_checklist.master_data.kandang'],
},
{ {
text: 'Konfigurasi', text: 'Konfigurasi',
link: '/daily-checklist/master-data/configuration', link: '/daily-checklist/master-data/configuration',
@@ -555,12 +549,6 @@ export const APPROVAL_WORKFLOWS = {
], ],
}; };
export const PROJECT_FLOCK_STATUS = {
PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name,
AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name,
SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name,
} as const;
export const ACCEPTED_FILE_TYPE = { export const ACCEPTED_FILE_TYPE = {
PDF: { PDF: {
'application/pdf': ['.pdf'], 'application/pdf': ['.pdf'],
-3
View File
@@ -21,9 +21,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/daily-checklist/master-data/configuration/': [ '/daily-checklist/master-data/configuration/': [
'lti.daily_checklist.master_data.configuration', 'lti.daily_checklist.master_data.configuration',
], ],
'/daily-checklist/master-data/kandang/': [
'lti.daily_checklist.master_data.kandang',
],
// Production // Production
// Production - Project Flock // Production - Project Flock
@@ -1,7 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react'; import { Check, ChevronsUpDown, X } from 'lucide-react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { import {
@@ -29,8 +29,6 @@ interface MultiSelectProps {
selected: string[]; selected: string[];
onChange: (selected: string[]) => void; onChange: (selected: string[]) => void;
onSearchChange?: (value: string) => void; onSearchChange?: (value: string) => void;
onLoadMore?: () => void;
isLoadingMore?: boolean;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
@@ -41,8 +39,6 @@ export function MultiSelect({
selected, selected,
onChange, onChange,
onSearchChange, onSearchChange,
onLoadMore,
isLoadingMore,
placeholder = 'Select items...', placeholder = 'Select items...',
className, className,
disabled, disabled,
@@ -119,18 +115,7 @@ export function MultiSelect({
onValueChange={onSearchChange} onValueChange={onSearchChange}
/> />
<CommandEmpty>No item found.</CommandEmpty> <CommandEmpty>No item found.</CommandEmpty>
<CommandList <CommandList className='max-h-[300px] overflow-y-auto'>
className='max-h-[300px] overflow-y-auto'
onScroll={(e) => {
const target = e.currentTarget;
if (
target.scrollHeight - target.scrollTop <=
target.clientHeight + 1
) {
onLoadMore?.();
}
}}
>
<CommandGroup className='overflow-visible'> <CommandGroup className='overflow-visible'>
{options.map((option) => ( {options.map((option) => (
<CommandItem <CommandItem
@@ -149,11 +134,6 @@ export function MultiSelect({
{option.label} {option.label}
</CommandItem> </CommandItem>
))} ))}
{isLoadingMore && (
<div className='py-4 flex justify-center w-full'>
<Loader2 className='h-4 w-4 animate-spin text-muted-foreground' />
</div>
)}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
+2 -7
View File
@@ -55,11 +55,7 @@ function SelectContent({
children, children,
position = 'popper', position = 'popper',
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content> & { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
onScroll?: React.UIEventHandler<HTMLDivElement>;
}) {
const { onScroll, ...restProps } = props;
return ( return (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
@@ -71,7 +67,7 @@ function SelectContent({
className className
)} )}
position={position} position={position}
{...restProps} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
@@ -80,7 +76,6 @@ function SelectContent({
position === 'popper' && position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1' 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
)} )}
onScroll={onScroll}
> >
{children} {children}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
@@ -2,16 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { import { Plus, X, Save, Send, Info, FilePlus, ListChecks } from 'lucide-react';
Plus,
X,
Save,
Send,
Info,
FilePlus,
ListChecks,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
@@ -35,6 +26,7 @@ import {
import { DatePicker } from '@/figma-make/components/base/date-picker'; import { DatePicker } from '@/figma-make/components/base/date-picker';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -51,7 +43,6 @@ import DropFileInput from '@/components/input/DropFileInput';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
// Static categories // Static categories
const CATEGORIES = [ const CATEGORIES = [
@@ -95,11 +86,16 @@ export function DailyChecklistContent() {
searchParams.get('category') || '' searchParams.get('category') || ''
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
isLoadingMore: isLoadingMoreKandang, 'id',
loadMore: loadMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
page: '1',
limit: '100',
}
);
const { data: phases } = useSWR< const { data: phases } = useSWR<
BaseApiResponse<Phase[] | undefined>, BaseApiResponse<Phase[] | undefined>,
@@ -172,16 +168,6 @@ export function DailyChecklistContent() {
const [documents, setDocuments] = useState<File[]>([]); const [documents, setDocuments] = useState<File[]>([]);
const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]); const [deletedDocumentIds, setDeletedDocumentIds] = useState<number[]>([]);
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Sync state to URL query params // Sync state to URL query params
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
@@ -315,8 +301,93 @@ export function DailyChecklistContent() {
checkAndLoadChecklist(); checkAndLoadChecklist();
}, [date, kandangId, selectedCategory]); }, [date, kandangId, selectedCategory]);
// Load employees when kandang changes
useEffect(() => {
if (kandangId) {
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
setSelectedEmployees([]);
setAssignments({});
} else {
setSelectedEmployees([]);
setAssignments({});
}
}, [kandangId]);
// Load activities and tasks when phases change // Load activities and tasks when phases change
useEffect(() => { useEffect(() => {
const loadAssignments = async (taskIds: string[]) => {
if (taskIds.length === 0) return;
try {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseError(existingDailyChecklist)) {
console.error(
'Error loading assignments:',
existingDailyChecklist.message
);
return;
}
// set existing document
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
// Build assignments map
const assignmentMap: {
[taskId: string]: {
[employeeId: string]: { checked: boolean; note: string };
};
} = {};
(existingDailyChecklist?.data.tasks || []).forEach(
(dailyChecklistTask) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
dailyChecklistTask.assignments.forEach((assignment) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
checked: assignment.checked,
note: assignment.note || '',
};
});
}
);
setAssignments(assignmentMap);
// Load employees from assignments
const employeeIds = Array.from(
new Set(
(existingDailyChecklist?.data.assigned_employees || []).map(
(a) => a.id
)
)
);
if (employeeIds.length > 0) {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseSuccess(existingDailyChecklist)) {
setSelectedEmployees(
existingDailyChecklist.data.assigned_employees
);
}
}
} catch (error) {
console.error('Error loading assignments:', error);
}
};
const loadActivitiesAndTasks = async () => { const loadActivitiesAndTasks = async () => {
if (!dailyChecklistId || selectedPhaseIds.length === 0) { if (!dailyChecklistId || selectedPhaseIds.length === 0) {
setActivitiesByPhase({}); setActivitiesByPhase({});
@@ -391,87 +462,6 @@ export function DailyChecklistContent() {
loadActivitiesAndTasks(); loadActivitiesAndTasks();
}, [dailyChecklistId, selectedPhaseIds]); }, [dailyChecklistId, selectedPhaseIds]);
// Load employees when kandang changes
useEffect(() => {
if (kandangId) {
// ✅ Clear selected employees ketika kandang berubah (reset ABK assignment)
setSelectedEmployees([]);
setAssignments({});
} else {
setSelectedEmployees([]);
setAssignments({});
}
}, [kandangId]);
const loadAssignments = async (taskIds: string[]) => {
if (taskIds.length === 0) return;
try {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(String(dailyChecklistId));
if (isResponseError(existingDailyChecklist)) {
console.error(
'Error loading assignments:',
existingDailyChecklist.message
);
return;
}
// set existing document
setExistingDocuments(existingDailyChecklist?.data.document_urls || []);
// Build assignments map
const assignmentMap: {
[taskId: string]: {
[employeeId: string]: { checked: boolean; note: string };
};
} = {};
(existingDailyChecklist?.data.tasks || []).forEach(
(dailyChecklistTask) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
dailyChecklistTask.assignments.forEach((assignment) => {
if (!assignmentMap[dailyChecklistTask.id]) {
assignmentMap[dailyChecklistTask.id] = {};
}
assignmentMap[dailyChecklistTask.id][assignment.employee.id] = {
checked: assignment.checked,
note: assignment.note || '',
};
});
}
);
setAssignments(assignmentMap);
// Load employees from assignments
const employeeIds = Array.from(
new Set(
(existingDailyChecklist?.data.assigned_employees || []).map(
(a) => a.id
)
)
);
if (employeeIds.length > 0) {
const existingDailyChecklist =
await DailyChecklistApi.getOneDailyChecklist(
String(dailyChecklistId)
);
if (isResponseSuccess(existingDailyChecklist)) {
setSelectedEmployees(existingDailyChecklist.data.assigned_employees);
}
}
} catch (error) {
console.error('Error loading assignments:', error);
}
};
// Phase selection modal // Phase selection modal
const handleAddPhase = () => { const handleAddPhase = () => {
if (!selectedCategory) { if (!selectedCategory) {
@@ -1008,7 +998,7 @@ export function DailyChecklistContent() {
> >
<SelectValue placeholder='Pilih kandang' /> <SelectValue placeholder='Pilih kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
key={kandang.value} key={kandang.value}
@@ -1017,12 +1007,6 @@ export function DailyChecklistContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -16,7 +16,7 @@ import {
SelectValue, SelectValue,
} from '@/figma-make/components/base/select'; } from '@/figma-make/components/base/select';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
import { Users, AlertCircle, Info, Loader2 } from 'lucide-react'; import { Users, AlertCircle, Info } from 'lucide-react';
import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { DateRangePicker } from '@/figma-make/components/base/date-range-picker';
import { import {
BarChart, BarChart,
@@ -36,10 +36,10 @@ import { DailyChecklistSummary } from '@/types/api/daily-checklist/daily-checkli
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist';
import { KandangApi } from '@/services/api/master-data';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper'; import { formatDate } from '@/lib/helper';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const KANDANG_COLORS = [ const KANDANG_COLORS = [
'#0069e0', // Blue (primary) '#0069e0', // Blue (primary)
@@ -77,20 +77,16 @@ export function Dashboard() {
httpClientFetcher httpClientFetcher
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
loadMore: loadMoreKandang, 'id',
isLoadingMore: isLoadingMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => { page: '1',
const target = e.target as HTMLDivElement; limit: '100',
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
} }
}; );
const kandangColorMap: { [key: string]: string } = {}; const kandangColorMap: { [key: string]: string } = {};
(kandangOptions || []).forEach((k, index) => { (kandangOptions || []).forEach((k, index) => {
@@ -168,7 +164,7 @@ export function Dashboard() {
> >
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -178,11 +174,6 @@ export function Dashboard() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -1,15 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { import { Eye, CheckCircle, XCircle, Search, Trash2, Edit } from 'lucide-react';
Eye,
CheckCircle,
XCircle,
Search,
Trash2,
Edit,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Badge } from '@/figma-make/components/base/badge'; import { Badge } from '@/figma-make/components/base/badge';
@@ -42,9 +34,9 @@ import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: 'ALL', label: 'Semua Status' }, { value: 'ALL', label: 'Semua Status' },
@@ -101,25 +93,21 @@ export function ListDailyChecklistContent() {
} }
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
isLoadingMore: isLoadingMoreKandang, 'id',
loadMore: loadMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
page: '1',
limit: '100',
}
);
const checklistList = isResponseSuccess(checklistListRes) const checklistList = isResponseSuccess(checklistListRes)
? checklistListRes.data || [] ? checklistListRes.data || []
: []; : [];
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}
};
// Modals // Modals
const [showApproveModal, setShowApproveModal] = useState(false); const [showApproveModal, setShowApproveModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false);
@@ -502,7 +490,7 @@ export function ListDailyChecklistContent() {
> >
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -512,11 +500,6 @@ export function ListDailyChecklistContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import * as React from 'react'; import * as React from 'react';
import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; import { ArrowLeft, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
@@ -137,15 +137,7 @@ export function DetailDailyChecklistContent() {
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
useEffect(() => { const fetchChecklistDetail = useCallback(async () => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [checklistId]);
const fetchChecklistDetail = async () => {
if (!checklistId) { if (!checklistId) {
console.warn('checklistId missing'); console.warn('checklistId missing');
setLoading(false); setLoading(false);
@@ -320,7 +312,15 @@ export function DetailDailyChecklistContent() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [checklistId, router]);
useEffect(() => {
if (checklistId) {
fetchChecklistDetail();
} else {
router.push('/404');
}
}, [checklistId, fetchChecklistDetail, router]);
const groupDetailData = (rows: ChecklistDetailRow[]) => { const groupDetailData = (rows: ChecklistDetailRow[]) => {
// Group by phase_id // Group by phase_id
@@ -1,14 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
Plus,
MoreVertical,
Pencil,
Trash2,
Search,
Loader2,
} from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card'; import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label'; import { Label } from '@/figma-make/components/base/label';
@@ -56,8 +49,8 @@ import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { KandangApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
export function MasterEmployeeContent() { export function MasterEmployeeContent() {
const { const {
@@ -92,20 +85,16 @@ export function MasterEmployeeContent() {
keepPreviousData: true, keepPreviousData: true,
} }
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
loadMore: loadMoreKandang, 'id',
isLoadingMore: isLoadingMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name'); 'search',
{
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => { page: '1',
const target = e.target as HTMLDivElement; limit: '100',
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
} }
}; );
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -362,7 +351,7 @@ export function MasterEmployeeContent() {
<SelectTrigger className='w-[180px] border-gray-200'> <SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='all'>Semua Kandang</SelectItem> <SelectItem value='all'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -372,11 +361,6 @@ export function MasterEmployeeContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -487,12 +471,6 @@ export function MasterEmployeeContent() {
kandang_ids: selected.map((id) => Number(id)), kandang_ids: selected.map((id) => Number(id)),
}) })
} }
onLoadMore={() => {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
}}
isLoadingMore={isLoadingMoreKandang}
placeholder='Pilih kandang' placeholder='Pilih kandang'
className='mt-1.5' className='mt-1.5'
/> />
@@ -1,585 +0,0 @@
'use client';
import { useState } from 'react';
import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react';
import { Card, CardContent } from '@/figma-make/components/base/card';
import { Button } from '@/figma-make/components/base/button';
import { Label } from '@/figma-make/components/base/label';
import { Input } from '@/figma-make/components/base/input';
import { Badge } from '@/figma-make/components/base/badge';
import { MultiSelect } from '@/figma-make/components/base/multi-select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/figma-make/components/base/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/figma-make/components/base/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/figma-make/components/base/alert-dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/figma-make/components/base/dropdown-menu';
import { toast } from 'sonner';
import useSWR from 'swr';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
import Table from '@/components/Table';
import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ColumnDef } from '@tanstack/react-table';
import { useSelect } from '@/components/input/SelectInput';
import { KandangApi, LocationApi } from '@/services/api/master-data';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
import { UserApi } from '@/services/api/user';
export function MasterKandangContent() {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
location_id: '',
status: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
location_id: 'location_id',
},
});
const {
data: dailyChecklistKandangs,
isLoading: isLoadingDailyChecklistKandangs,
mutate: refreshDailyChecklistKandangs,
} = useSWR(
`${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`,
DailyChecklistKandangApi.getAllFetcher,
{
keepPreviousData: true,
}
);
const { options: locationOptions } = useSelect(
LocationApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const { options: picOptions } = useSelect(
UserApi.basePath,
'id',
'name',
'search',
{
page: '1',
limit: '100',
}
);
const {
options: kandangOptions,
isLoadingMore: isLoadingKandangOptionsMore,
loadMore: loadMoreKandang,
} = useSelect(KandangApi.basePath, 'id', 'name');
const [showModal, setShowModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [kandangToDelete, setKandangToDelete] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
const [kandangForm, setKandangForm] = useState({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [] as number[],
});
const dailyChecklistKandangColumns: ColumnDef<DailyChecklistKandang>[] = [
{
id: 'name',
header: 'Nama',
accessorKey: 'name',
enableSorting: false,
},
{
id: 'location',
header: 'Lokasi',
accessorKey: 'location',
enableSorting: false,
cell: ({ row }) => row.original.location.name ?? '-',
},
{
id: 'pic',
header: 'PIC',
accessorKey: 'pic',
enableSorting: false,
cell: ({ row }) => row.original.pic.name ?? '-',
},
{
id: 'recording_kandangs',
header: 'Kandang Recording',
accessorKey: 'recording_kandangs',
enableSorting: false,
cell: ({ row }) =>
row.original.recording_kandangs?.length > 0
? row.original.recording_kandangs.map((item) => item.name).join(', ')
: '-',
},
{
id: 'action',
header: 'Aksi',
accessorKey: 'action',
enableSorting: false,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='ghost'
size='sm'
className='h-8 w-8 p-0 hover:bg-gray-100'
>
<MoreVertical className='h-4 w-4 text-gray-600' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => handleEdit(row.original)}>
<Pencil className='mr-2 h-4 w-4' />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(row.original.id)}
className='text-red-600'
>
<Trash2 className='mr-2 h-4 w-4' />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
const handleAdd = () => {
setModalMode('create');
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: []
});
setShowModal(true);
};
const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => {
setModalMode('edit');
setKandangForm({
id: dailyChecklistKandang.id,
name: dailyChecklistKandang.name,
location_id: dailyChecklistKandang.location.id,
pic_id: dailyChecklistKandang.pic.id,
// recording_kandangs:
// dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [],
});
setShowModal(true);
};
const handleSave = async () => {
if (!kandangForm.name.trim()) {
toast.error('Nama harus diisi');
return;
}
if (!kandangForm.location_id) {
toast.error('Lokasi wajib diisi');
return;
}
// if (!kandangForm.recording_kandangs.length) {
// toast.error('Kandang recording wajib diisi');
// return;
// }
setLoading(true);
try {
if (modalMode === 'create') {
const createDailyChecklistKandangResponse =
await DailyChecklistKandangApi.create({
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(createDailyChecklistKandangResponse)) {
console.error(
'Error creating kandang:',
createDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil ditambahkan');
} else {
const updateDailyChecklistKandangResponse =
await DailyChecklistKandangApi.update(kandangForm.id, {
name: kandangForm.name.trim(),
location_id: kandangForm.location_id,
pic_id: kandangForm.pic_id,
// recording_kandang_ids: kandangForm.recording_kandangs,
});
if (isResponseError(updateDailyChecklistKandangResponse)) {
console.error(
'Error updating kandang:',
updateDailyChecklistKandangResponse.message
);
toast.error('Gagal menambahkan Kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil diubah');
}
setShowModal(false);
setKandangForm({
id: 0,
name: '',
location_id: 0,
pic_id: 0,
// recording_kandangs: [],
});
} catch (error) {
console.error('Error saving kandang:', error);
toast.error('Terjadi kesalahan saat menyimpan kandang');
} finally {
setLoading(false);
}
};
const handleDeleteClick = (kandangId: number) => {
setKandangToDelete(kandangId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (!kandangToDelete) return;
setLoading(true);
try {
const deleteKandangResponse =
await DailyChecklistKandangApi.delete(kandangToDelete);
if (isResponseError(deleteKandangResponse)) {
console.error('Error deleting kandang:', deleteKandangResponse.message);
toast.error('Gagal menghapus kandang');
return;
}
refreshDailyChecklistKandangs();
toast.success('Kandang berhasil dihapus');
setShowDeleteConfirm(false);
setKandangToDelete(null);
} catch (error) {
console.error('Error deleting kandang:', error);
toast.error('Terjadi kesalahan saat menghapus kandang');
} finally {
setLoading(false);
}
};
if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) {
return (
<div className='min-h-screen'>
<div className='p-6'>
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-12 text-center text-gray-500'>
Memuat data...
</CardContent>
</Card>
</div>
</div>
);
}
return (
<div className='min-h-screen'>
<div className='p-6'>
{/* Page Title */}
<div className='mb-6'>
<h1 className='text-2xl font-semibold text-gray-900'>
Master Kandang
</h1>
<p className='text-sm text-gray-600 mt-1'>
Master Data <span className='text-[#0069e0]'>Kandang</span>
</p>
</div>
{/* Main Card */}
<Card className='border-gray-200/60 shadow-sm rounded-xl bg-white'>
<CardContent className='p-0'>
{/* Single Toolbar Row */}
<div className='flex flex-wrap items-center justify-between gap-4 p-6 border-b border-gray-200/60'>
{/* LEFT: Search + Filters */}
<div className='flex items-center gap-3 flex-wrap'>
<div className='relative'>
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4' />
<DebouncedTextInput
name='search'
placeholder='Cari kandang...'
value={tableFilterState.search}
onChange={(e) => updateFilter('search', e.target.value)}
className={{
wrapper: 'w-full sm:w-[280px] border-gray-200',
inputWrapper: 'px-3 py-2 h-fit rounded-md',
input: 'text-sm',
}}
startAdornment={
<Search className='text-gray-400 w-4 h-4' />
}
/>
</div>
<Select
value={tableFilterState.location_id}
onValueChange={(value) =>
updateFilter('location_id', value === 'all' ? '' : value)
}
>
<SelectTrigger className='w-[180px] border-gray-200'>
<SelectValue placeholder='Semua Lokasi' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>Semua Lokasi</SelectItem>
{locationOptions.map((kandang) => (
<SelectItem
key={kandang.value}
value={String(kandang.value)}
>
{kandang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* RIGHT: Export + Add */}
<div className='flex items-center gap-2 flex-wrap'>
<Button
onClick={handleAdd}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
<Plus className='w-4 h-4 mr-2' />
Tambah Kandang
</Button>
</div>
</div>
{/* Table */}
<Table<DailyChecklistKandang>
data={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.data
: []
}
columns={dailyChecklistKandangColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.page
: 0
}
totalItems={
isResponseSuccess(dailyChecklistKandangs)
? dailyChecklistKandangs?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingDailyChecklistKandangs}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(dailyChecklistKandangs) &&
dailyChecklistKandangs?.data?.length === 0,
}),
tableWrapperClassName:
'overflow-x-auto border border-solid border-base-content/10 rounded-none',
headerRowClassName: 'bg-gray-50/50',
headerColumnClassName:
'text-left py-3.5 px-6 text-sm font-semibold text-gray-700',
paginationClassName: 'px-4',
}}
/>
</CardContent>
</Card>
</div>
{/* Add/Edit Modal */}
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogContent className='sm:max-w-md bg-white rounded-xl shadow-lg'>
<DialogHeader>
<DialogTitle>
{modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'}
</DialogTitle>
<DialogDescription>
{modalMode === 'create'
? 'Masukkan detail Kandang baru'
: 'Ubah detail Kandang'}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div>
<Label htmlFor='nama-kandang'>
Nama Kandang <span className='text-red-500'>*</span>
</Label>
<Input
id='nama-kandang'
value={kandangForm.name}
onChange={(e) =>
setKandangForm({ ...kandangForm, name: e.target.value })
}
placeholder='Masukkan nama Kandang'
className='mt-1.5'
disabled={loading}
/>
</div>
<div>
<Label htmlFor='category'>
Lokasi <span className='text-red-500'>*</span>
</Label>
<Select
value={
kandangForm.location_id ? String(kandangForm.location_id) : ''
}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, location_id: Number(value) })
}
>
<SelectTrigger id='category' className='mt-1.5'>
<SelectValue placeholder='Pilih lokasi' />
</SelectTrigger>
<SelectContent>
{locationOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor='pic'>
PIC <span className='text-red-500'>*</span>
</Label>
<Select
value={kandangForm.pic_id ? String(kandangForm.pic_id) : ''}
onValueChange={(value) =>
setKandangForm({ ...kandangForm, pic_id: Number(value) })
}
>
<SelectTrigger id='pic' className='mt-1.5'>
<SelectValue placeholder='Pilih PIC' />
</SelectTrigger>
<SelectContent>
{picOptions.map((cat) => (
<SelectItem key={cat.value} value={String(cat.value)}>
{cat.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button
variant='outline'
onClick={() => setShowModal(false)}
disabled={loading}
>
Batal
</Button>
<Button
onClick={handleSave}
disabled={loading}
className='bg-[#0069e0] hover:bg-[#0052b3] text-white'
>
{loading ? 'Menyimpan...' : 'Simpan'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent className='bg-white rounded-xl shadow-lg sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>Hapus Kandang?</AlertDialogTitle>
<AlertDialogDescription>
Data Kandang akan dihapus secara permanen.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Batal</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={loading}
className='bg-red-600 hover:bg-red-700 text-white'
>
{loading ? 'Menghapus...' : 'Hapus'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -11,7 +11,7 @@ import {
SelectValue, SelectValue,
} from '@/figma-make/components/base/select'; } from '@/figma-make/components/base/select';
import { useSelect } from '@/components/input/SelectInput'; import { useSelect } from '@/components/input/SelectInput';
import { AreaApi, LocationApi } from '@/services/api/master-data'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import useSWR from 'swr'; import useSWR from 'swr';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist'; import { DailyChecklistReport } from '@/types/api/daily-checklist/daily-checklist';
@@ -26,8 +26,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { PhaseApi } from '@/services/api/daily-checklist/phase'; import { PhaseApi } from '@/services/api/daily-checklist/phase';
import { EmployeeApi } from '@/services/api/daily-checklist/employee'; import { EmployeeApi } from '@/services/api/daily-checklist/employee';
import { Button } from '@/figma-make/components/base/button'; import { Button } from '@/figma-make/components/base/button';
import { Download, Loader2 } from 'lucide-react'; import { Download } from 'lucide-react';
import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang';
const MONTH_OPTIONS = [ const MONTH_OPTIONS = [
{ value: '1', label: 'Januari' }, { value: '1', label: 'Januari' },
@@ -130,23 +129,18 @@ export function DailyChecklistReportsContent() {
} }
); );
const { const { options: kandangOptions } = useSelect(
options: kandangOptions, KandangApi.basePath,
loadMore: loadMoreKandang, 'id',
isLoadingMore: isLoadingMoreKandang, 'name',
} = useSelect(DailyChecklistKandangApi.basePath, 'id', 'name', 'search', { 'search',
area_id: tableFilterState.area_id, {
location_id: tableFilterState.location_id, page: '1',
}); limit: '100',
area_id: tableFilterState.area_id,
const handleKandangScroll = (e: React.UIEvent<HTMLDivElement>) => { location_id: tableFilterState.location_id,
const target = e.target as HTMLDivElement;
if (target.scrollHeight - target.scrollTop <= target.clientHeight + 10) {
if (!isLoadingMoreKandang) {
loadMoreKandang();
}
} }
}; );
const { options: phaseOptions } = useSelect( const { options: phaseOptions } = useSelect(
PhaseApi.basePath, PhaseApi.basePath,
@@ -441,7 +435,7 @@ export function DailyChecklistReportsContent() {
> >
<SelectValue placeholder='Semua Kandang' /> <SelectValue placeholder='Semua Kandang' />
</SelectTrigger> </SelectTrigger>
<SelectContent onScroll={handleKandangScroll}> <SelectContent>
<SelectItem value='ALL'>Semua Kandang</SelectItem> <SelectItem value='ALL'>Semua Kandang</SelectItem>
{kandangOptions.map((kandang) => ( {kandangOptions.map((kandang) => (
<SelectItem <SelectItem
@@ -451,11 +445,6 @@ export function DailyChecklistReportsContent() {
{kandang.label} {kandang.label}
</SelectItem> </SelectItem>
))} ))}
{isLoadingMoreKandang && (
<div className='flex justify-center p-2'>
<Loader2 className='h-4 w-4 animate-spin text-gray-500' />
</div>
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
+14
View File
@@ -305,3 +305,17 @@ export function transformConstants(
}, },
}; };
} }
export function omit<T extends Record<string, unknown>, K extends keyof T>(
obj: T,
keys: K | K[]
): Omit<T, K> {
const keysArray = Array.isArray(keys) ? keys : [keys];
const result = { ...obj };
keysArray.forEach((key) => {
delete result[key];
});
return result;
}
@@ -1,20 +0,0 @@
import { BaseApiService } from '@/services/api/base';
import {
DailyChecklistKandang,
CreateDailyChecklistKandangPayload,
UpdateDailyChecklistKandangPayload,
} from '@/types/api/daily-checklist/kandang';
export class DailyChecklistKandangApiService extends BaseApiService<
DailyChecklistKandang,
CreateDailyChecklistKandangPayload,
UpdateDailyChecklistKandangPayload
> {
constructor(basePath: string = '/master-data/kandang-groups') {
super(basePath);
}
}
export const DailyChecklistKandangApi = new DailyChecklistKandangApiService(
'/master-data/kandang-groups'
);
@@ -4,16 +4,13 @@ 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 & ChickinDeleteSlice; export type ChickinStore = ChickinApprovalSlice;
export const useChickinStore = create<ChickinStore>()( export const useChickinStore = create<ChickinStore>()(
devtools( devtools(
(...args) => ({ (...args) => ({
...createChickinApprovalSlice(...args), ...createChickinApprovalSlice(...args),
...createChickinDeleteSlice(...args),
}), }),
{ {
name: 'ChickinStore', name: 'ChickinStore',
@@ -1,57 +0,0 @@
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,4 +1,5 @@
import { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store'; import { TabActionsSlice } from '@/stores/tab-actions/tab-actions.store';
import { omit } from '@/lib/helper';
import { StateCreator } from 'zustand'; import { StateCreator } from 'zustand';
export const createTabActionsSlice: StateCreator< export const createTabActionsSlice: StateCreator<
@@ -20,10 +21,9 @@ export const createTabActionsSlice: StateCreator<
})), })),
clearTabActions: (tabId) => clearTabActions: (tabId) =>
set((state) => { set((state) => ({
const { [tabId]: _, ...rest } = state.tabActions; tabActions: omit(state.tabActions, tabId),
return { tabActions: rest }; })),
}),
clearAllTabActions: () => set({ tabActions: {} }), clearAllTabActions: () => set({ tabActions: {} }),
}); });
-24
View File
@@ -1,24 +0,0 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseKandang } from '@/types/api/master-data/kandang';
import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user';
export type BaseDailyChecklistKandang = {
id: number;
name: string;
location: BaseLocation;
recording_kandangs: Pick<BaseKandang, 'id' | 'name'>[];
pic: BaseUser;
};
export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang;
export type CreateDailyChecklistKandangPayload = {
name: string;
location_id: number;
pic_id: number;
// recording_kandang_ids: number[];
};
export type UpdateDailyChecklistKandangPayload =
CreateDailyChecklistKandangPayload;
-11
View File
@@ -9,19 +9,8 @@ export type BaseProductWarehouse = {
warehouse_id: number; warehouse_id: number;
uom: Uom; uom: Uom;
quantity: number; quantity: number;
transfer_available_qty?: number;
product: Product; product: Product;
warehouse: Warehouse; warehouse: Warehouse;
project_flock_kandang?: {
id: number;
project_flock_id: number;
kandang_id: number;
period: number;
project_flock?: {
id: number;
flock_name: string;
};
};
week?: number | null; week?: number | null;
}; };
-3
View File
@@ -1,7 +1,6 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { BaseLocation } from '@/types/api/master-data/location'; import { BaseLocation } from '@/types/api/master-data/location';
import { BaseUser } from '@/types/api/user'; import { BaseUser } from '@/types/api/user';
import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang';
export type BaseKandang = { export type BaseKandang = {
id: number; id: number;
@@ -11,7 +10,6 @@ export type BaseKandang = {
capacity: number; capacity: number;
pic: BaseUser; pic: BaseUser;
project_flock_kandang_id?: number; project_flock_kandang_id?: number;
kandang_group: Pick<BaseDailyChecklistKandang, 'id' | 'name'>;
}; };
export type Kandang = BaseMetadata & BaseKandang; export type Kandang = BaseMetadata & BaseKandang;
@@ -21,7 +19,6 @@ export type CreateKandangPayload = {
location_id: number; location_id: number;
capacity: number; capacity: number;
pic_id: number; pic_id: number;
group_id: number;
}; };
export type UpdateKandangPayload = CreateKandangPayload; export type UpdateKandangPayload = CreateKandangPayload;
-2
View File
@@ -74,8 +74,6 @@ export type ProjectFlockKandangLookup = {
available_quantity?: number; available_quantity?: number;
population: number; population: number;
chick_in_date: string; chick_in_date: string;
is_transition: boolean;
is_laying: boolean;
}; };
export type ProjectFlockAvailableQuantity = { export type ProjectFlockAvailableQuantity = {
-2
View File
@@ -49,8 +49,6 @@ export type BaseRecording = {
project_flock: ProjectFlock; project_flock: ProjectFlock;
record_datetime: string; record_datetime: string;
day: number; day: number;
is_transition: boolean;
is_laying: boolean;
} & ProductionMetrics; } & ProductionMetrics;
export type RecordingDepletion = { export type RecordingDepletion = {