mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/FE/US-388/master-data-uniformity-standard' into 'development'
[FEAT/FE][US#388] Master Data Standar Produksi See merge request mbugroup/lti-web-client!118
This commit is contained in:
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run build
|
npx tsc --noEmit
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const FinanceAdjust = () => {
|
||||||
|
return <div>Finance Adjust</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAdjust;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
|
const FinanceAddInitialBalancePage = () => {
|
||||||
|
return <FormFinanceAddInitialBalance type='add' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAddInitialBalancePage;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
|
||||||
|
|
||||||
|
const FinanceAddInjectionPage = () => {
|
||||||
|
return <FormFinanceInjection type='add' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAddInjectionPage;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||||
|
|
||||||
|
const FinanceAddPage = () => {
|
||||||
|
return <FormFinanceAdd />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceAddPage;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
|
const EditFinanceInitialBalancePage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const financeId = searchParams.get('financeId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading: isLoadingFinance } = useSWR(
|
||||||
|
financeId,
|
||||||
|
(id: number) => FinanceApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingFinance && (!finance || isResponseError(finance))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFinance && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingFinance && (
|
||||||
|
<FormFinanceAddInitialBalance
|
||||||
|
type='edit'
|
||||||
|
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFinanceInitialBalancePage;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
|
||||||
|
|
||||||
|
const EditFinanceInjectionPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const financeId = searchParams.get('financeId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading: isLoadingFinance } = useSWR(
|
||||||
|
financeId,
|
||||||
|
(id: number) => FinanceApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingFinance && (!finance || isResponseError(finance))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFinance && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingFinance && (
|
||||||
|
<FormFinanceInjection
|
||||||
|
type='edit'
|
||||||
|
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFinanceInjectionPage;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
|
||||||
|
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
|
||||||
|
|
||||||
|
const EditFinanceTransactionPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const financeId = searchParams.get('financeId');
|
||||||
|
|
||||||
|
const { data: finance, isLoading: isLoadingFinance } = useSWR(
|
||||||
|
financeId,
|
||||||
|
(id: number) => FinanceApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingFinance && (!finance || isResponseError(finance))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
{isLoadingFinance && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingFinance && (
|
||||||
|
<FormFinanceAdd
|
||||||
|
type='edit'
|
||||||
|
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFinanceTransactionPage;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FinanceDetail from '@/components/pages/finance/FinanceDetail';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
const FinanceDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const financeId = useSearchParams().get('financeId');
|
||||||
|
|
||||||
|
const { data: finance } = useSWR(financeId, () =>
|
||||||
|
FinanceApi.getSingle(Number(financeId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!financeId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(finance);
|
||||||
|
|
||||||
|
// if (!finance || isResponseError(finance)) {
|
||||||
|
// router.replace('/404');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isResponseSuccess(finance) && <FinanceDetail finance={finance.data} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceDetailPage;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import FinanceTable from '@/components/pages/finance/FinanceTable';
|
||||||
|
|
||||||
|
const Finance = () => {
|
||||||
|
return (
|
||||||
|
<section className='size-full p-6'>
|
||||||
|
<div className='flex flex-row gap-4'></div>
|
||||||
|
<FinanceTable />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Finance;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
|
||||||
|
|
||||||
|
const AddProductionStandardPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProductionStandardForm formType='add' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddProductionStandardPage;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const EditProductionStandardPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Get Query Params
|
||||||
|
const productionStandardId = searchParams.get('productionStandardId');
|
||||||
|
|
||||||
|
// Fetch Data
|
||||||
|
const { data: productionStandard, isLoading: isLoadingProductionStandard } =
|
||||||
|
useSWR(productionStandardId, (id: number) =>
|
||||||
|
ProductionStandardApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!productionStandardId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isLoadingProductionStandard &&
|
||||||
|
(!productionStandard || isResponseError(productionStandard))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoadingProductionStandard && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingProductionStandard &&
|
||||||
|
isResponseSuccess(productionStandard) && (
|
||||||
|
<ProductionStandardForm
|
||||||
|
formType='edit'
|
||||||
|
initialValue={productionStandard.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditProductionStandardPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const DetailProductionStandardPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// Get Query Params
|
||||||
|
const productionStandardId = searchParams.get('productionStandardId');
|
||||||
|
|
||||||
|
// Fetch Data
|
||||||
|
const { data: productionStandard, isLoading: isLoadingProductionStandard } =
|
||||||
|
useSWR(productionStandardId, (id: number) =>
|
||||||
|
ProductionStandardApi.getSingle(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!productionStandardId) {
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isLoadingProductionStandard &&
|
||||||
|
(!productionStandard || isResponseError(productionStandard))
|
||||||
|
) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoadingProductionStandard && (
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
)}
|
||||||
|
{!isLoadingProductionStandard &&
|
||||||
|
isResponseSuccess(productionStandard) && (
|
||||||
|
<ProductionStandardForm
|
||||||
|
formType='detail'
|
||||||
|
initialValue={productionStandard.data}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailProductionStandardPage;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
|
||||||
|
|
||||||
|
const ProductionStandardPage = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<ProductionStandardTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionStandardPage;
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
|
||||||
import ProjectFlockChickinDetail from '@/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail';
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
const AddChickin = () => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const projectFlockId = searchParams.get('projectFlockId');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className='w-full'>
|
|
||||||
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddChickin;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
|
|
||||||
|
|
||||||
const Chickin = () => {
|
|
||||||
return (
|
|
||||||
<section className='w-full'>
|
|
||||||
<ChickinTable />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default Chickin;
|
|
||||||
@@ -64,7 +64,7 @@ const Drawer = ({
|
|||||||
),
|
),
|
||||||
drawerSidebarContent: cn(
|
drawerSidebarContent: cn(
|
||||||
baseClassNames.drawerSidebarContent,
|
baseClassNames.drawerSidebarContent,
|
||||||
'w-full min-w-120 sm:w-fit'
|
'w-full sm:min-w-120 sm:w-fit'
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (variant === 'left') {
|
} else if (variant === 'left') {
|
||||||
@@ -76,7 +76,7 @@ const Drawer = ({
|
|||||||
),
|
),
|
||||||
drawerSidebarContent: cn(
|
drawerSidebarContent: cn(
|
||||||
baseClassNames.drawerSidebarContent,
|
baseClassNames.drawerSidebarContent,
|
||||||
'w-full min-w-120 sm:w-fit'
|
'w-full sm:min-w-120 sm:w-fit'
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import {
|
||||||
|
FINANCE_INITIAL_BALANCE_STATUS,
|
||||||
|
FINANCE_TRANSACTION_STATUS,
|
||||||
|
} from '@/config/constant';
|
||||||
|
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const informasiUmum = [
|
||||||
|
{
|
||||||
|
label: 'ID',
|
||||||
|
value: finance.payment_code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Jenis Transaksi',
|
||||||
|
value: finance.transaction_type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pihak',
|
||||||
|
value: finance.party.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tanggal',
|
||||||
|
value: formatDate(finance.payment_date, 'DD MMM yyyy'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Metode Pembayaran',
|
||||||
|
value: finance.payment_method,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Catatan',
|
||||||
|
value: finance.notes || '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const informasiTransfer = [
|
||||||
|
{
|
||||||
|
label: 'No. Referensi',
|
||||||
|
value: finance.reference_number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nomor Rekening',
|
||||||
|
value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Rekening ${formatTitleCase(finance.party.type)}`,
|
||||||
|
value: finance.party.account_number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nominal',
|
||||||
|
value: formatCurrency(finance.expense_amount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sisa',
|
||||||
|
value: formatCurrency(finance.income_amount),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
await FinanceApi.delete(finance.id as number);
|
||||||
|
router.back();
|
||||||
|
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Successfully delete Finance!');
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
|
<FormHeader title='' backUrl='/finance' />
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title='Detail Keuangan'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
variant='bordered'
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-2 gap-4 mb-6'>
|
||||||
|
<Table
|
||||||
|
data={informasiUmum}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
id: 'label',
|
||||||
|
accessorKey: 'label',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
id: 'value',
|
||||||
|
accessorKey: 'value',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
className={{
|
||||||
|
headerRowClassName: 'hidden',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
data={informasiTransfer}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
id: 'label',
|
||||||
|
accessorKey: 'label',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
id: 'value',
|
||||||
|
accessorKey: 'value',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
className={{
|
||||||
|
headerRowClassName: 'hidden',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className='flex flex-row gap-2 justify-end'>
|
||||||
|
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && (
|
||||||
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
className='min-w-24'
|
||||||
|
href={`/finance/detail/edit?financeId=${finance.id}`}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:pencil-outline' />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
|
||||||
|
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
className='min-w-24'
|
||||||
|
href={`/finance/detail/edit/initial-balance?financeId=${finance.id}`}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:pencil-outline' />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
<RequirePermission permissions='lti.finance.transaction.delete'>
|
||||||
|
<Button
|
||||||
|
color='error'
|
||||||
|
className='min-w-24'
|
||||||
|
onClick={() => deleteModal.openModal()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete-outline' />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Finance ini (${finance?.payment_code})?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceDetail;
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
|
import { CellContext, Row } from '@tanstack/react-table';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Dropdown from '@/components/dropdown/Dropdown';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import Menu from '@/components/menu/Menu';
|
||||||
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
|
import {
|
||||||
|
FINANCE_INITIAL_BALANCE_STATUS,
|
||||||
|
FINANCE_INJECTION_STATUS,
|
||||||
|
FINANCE_TRANSACTION_STATUS,
|
||||||
|
ROWS_OPTIONS,
|
||||||
|
} from '@/config/constant';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<Finance, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.finance.transaction.detail'>
|
||||||
|
<Button
|
||||||
|
href={`/finance/detail?financeId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
{FINANCE_TRANSACTION_STATUS.includes(
|
||||||
|
props.row.original.transaction_type
|
||||||
|
) && (
|
||||||
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
|
<Button
|
||||||
|
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(
|
||||||
|
props.row.original.transaction_type
|
||||||
|
) && (
|
||||||
|
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
||||||
|
<Button
|
||||||
|
href={`/finance/detail/edit/initial-balance?financeId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{FINANCE_INJECTION_STATUS.includes(
|
||||||
|
props.row.original.transaction_type
|
||||||
|
) && (
|
||||||
|
<RequirePermission permissions='lti.finance.injections.update'>
|
||||||
|
<Button
|
||||||
|
href={`/finance/detail/edit/injection?financeId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.finance.transaction.delete'>
|
||||||
|
<Button
|
||||||
|
onClick={deleteClickHandler}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='text-error hover:text-inherit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</RowOptionsMenuWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FinanceTable = () => {
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
transactionType: '',
|
||||||
|
bankId: '',
|
||||||
|
partyType: '',
|
||||||
|
sortBy: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
transactionType: 'transaction_type',
|
||||||
|
bankId: 'bank_id',
|
||||||
|
partyType: 'party_type',
|
||||||
|
sortBy: 'sort_date',
|
||||||
|
startDate: 'start_date',
|
||||||
|
endDate: 'end_date',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== State =====
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const [pendingFilters, setPendingFilters] = useState({
|
||||||
|
search: '',
|
||||||
|
transactionType: '',
|
||||||
|
bankId: '',
|
||||||
|
partyType: '',
|
||||||
|
sortBy: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
});
|
||||||
|
const [selectedTransactionType, setSelectedTransactionType] =
|
||||||
|
useState<OptionType | null>(null);
|
||||||
|
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
|
||||||
|
const [selectedPartyType, setSelectedPartyType] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
|
||||||
|
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
// ===== Fetch Data =====
|
||||||
|
const {
|
||||||
|
data: finances,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshFinances,
|
||||||
|
} = useSWR(
|
||||||
|
`${FinanceApi.basePath}/transactions${getTableFilterQueryString()}`,
|
||||||
|
FinanceApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== Options =====
|
||||||
|
const transactionTypeOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{ label: 'Transfer', value: 'TRANSFER' },
|
||||||
|
{ label: 'Cash', value: 'CASH' },
|
||||||
|
{ label: 'Card', value: 'CARD' },
|
||||||
|
{ label: 'Cheque', value: 'CHEQUE' },
|
||||||
|
{ label: 'Saldo', value: 'SALDO' },
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
const partyTypeOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{ label: 'Customer', value: 'CUSTOMER' },
|
||||||
|
{ label: 'Supplier', value: 'SUPPLIER' },
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
const sortByOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
||||||
|
{ label: 'Tanggal Dibuat', value: 'created_at' },
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>(
|
||||||
|
BankApi.basePath,
|
||||||
|
'id',
|
||||||
|
'alias',
|
||||||
|
'',
|
||||||
|
{
|
||||||
|
limit: 'limit',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== Handler =====
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
setPendingFilters((prev) => ({ ...prev, search: e.target.value }));
|
||||||
|
};
|
||||||
|
const transactionTypeChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
setSelectedTransactionType(val as OptionType);
|
||||||
|
setPendingFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transactionType: val ? ((val as OptionType).value as string) : '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedBank(val as OptionType);
|
||||||
|
setPendingFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
bankId: val ? ((val as OptionType).value as string) : '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const partyTypeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedPartyType(val as OptionType);
|
||||||
|
setPendingFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
partyType: val ? ((val as OptionType).value as string) : '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedSortBy(val as OptionType);
|
||||||
|
setPendingFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
sortBy: val ? ((val as OptionType).value as string) : '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const startDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
setPendingFilters((prev) => ({ ...prev, startDate: e.target.value }));
|
||||||
|
};
|
||||||
|
const endDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
setPendingFilters((prev) => ({ ...prev, endDate: e.target.value }));
|
||||||
|
};
|
||||||
|
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
const newVal = val as OptionType;
|
||||||
|
setPageSize(newVal.value as number);
|
||||||
|
};
|
||||||
|
const submitFilterHandler = () => {
|
||||||
|
updateFilter('search', pendingFilters.search);
|
||||||
|
updateFilter('transactionType', pendingFilters.transactionType);
|
||||||
|
updateFilter('bankId', pendingFilters.bankId);
|
||||||
|
updateFilter('partyType', pendingFilters.partyType);
|
||||||
|
updateFilter('sortBy', pendingFilters.sortBy);
|
||||||
|
updateFilter('startDate', pendingFilters.startDate);
|
||||||
|
updateFilter('endDate', pendingFilters.endDate);
|
||||||
|
};
|
||||||
|
const resetFilterHandler = () => {
|
||||||
|
setSelectedTransactionType(null);
|
||||||
|
setSelectedBank(null);
|
||||||
|
setSelectedPartyType(null);
|
||||||
|
setSelectedSortBy(null);
|
||||||
|
|
||||||
|
const emptyFilters = {
|
||||||
|
search: '',
|
||||||
|
transactionType: '',
|
||||||
|
bankId: '',
|
||||||
|
partyType: '',
|
||||||
|
sortBy: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
};
|
||||||
|
setPendingFilters(emptyFilters);
|
||||||
|
|
||||||
|
updateFilter('search', '');
|
||||||
|
updateFilter('transactionType', '');
|
||||||
|
updateFilter('bankId', '');
|
||||||
|
updateFilter('partyType', '');
|
||||||
|
updateFilter('sortBy', '');
|
||||||
|
updateFilter('startDate', '');
|
||||||
|
updateFilter('endDate', '');
|
||||||
|
};
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
await FinanceApi.delete(selectedFinance?.id as number);
|
||||||
|
refreshFinances();
|
||||||
|
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Successfully delete Finance!');
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
header: 'ID',
|
||||||
|
accessorKey: 'payment_code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'References Number',
|
||||||
|
accessorKey: 'reference_number',
|
||||||
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
|
const value = props.row.original.reference_number;
|
||||||
|
return <span>{value ?? '-'}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Jenis Transaksi',
|
||||||
|
accessorKey: 'transaction_type',
|
||||||
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
|
const value = props.row.original.transaction_type
|
||||||
|
.split('_')
|
||||||
|
.join(' ');
|
||||||
|
return <span>{formatTitleCase(value)}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Pihak',
|
||||||
|
accessorFn: (finance: Finance) => finance.party.name,
|
||||||
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
|
if (props.row.original.party.id) {
|
||||||
|
return <span>{props.row.original.party.name}</span>;
|
||||||
|
}
|
||||||
|
return <span>{'-'}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tanggal',
|
||||||
|
accessorFn: (finance: Finance) =>
|
||||||
|
formatDate(finance.payment_date, 'DD MMM YYYY'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Metode Pembayaran',
|
||||||
|
accessorKey: 'payment_method',
|
||||||
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
|
const value = props.row.original.payment_method.split('_').join(' ');
|
||||||
|
return <span>{formatTitleCase(value)}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Bank',
|
||||||
|
accessorFn: (finance: Finance) =>
|
||||||
|
`${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Pengeluaran (Rp)',
|
||||||
|
accessorFn: (finance: Finance) =>
|
||||||
|
formatCurrency(finance.expense_amount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Pemasukan (Rp)',
|
||||||
|
accessorFn: (finance: Finance) => formatCurrency(finance.income_amount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props: CellContext<Finance, 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 = () => {
|
||||||
|
setSelectedFinance(props.row.original);
|
||||||
|
deleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentPageSize > 2 && (
|
||||||
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='dropdown'
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowDropdownOptions>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPageSize <= 2 && (
|
||||||
|
<RowCollapseOptions>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='collapse'
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowCollapseOptions>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<section className='size-full p-6 flex flex-col gap-6'>
|
||||||
|
<div className='flex justify-end gap-2'>
|
||||||
|
<RequirePermission permissions='lti.finance.injections.create'>
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
className='min-w-24'
|
||||||
|
href='/finance/add/injection'
|
||||||
|
>
|
||||||
|
Injection Saldo Bank
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.finance.initial_balances.create'>
|
||||||
|
<Button
|
||||||
|
color='info'
|
||||||
|
className='text-white min-w-24'
|
||||||
|
href='/finance/add/initial-balance'
|
||||||
|
>
|
||||||
|
Saldo Awal
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.finance.payments.create'>
|
||||||
|
<Button color='primary' className='min-w-24' href='/finance/add'>
|
||||||
|
Tambah
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<div className='flex justify-end gap-2'>
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
className='min-w-24'
|
||||||
|
onClick={resetFilterHandler}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
className='min-w-24'
|
||||||
|
onClick={submitFilterHandler}
|
||||||
|
>
|
||||||
|
Cari
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='grid grid-cols-4 gap-6'>
|
||||||
|
<SelectInput
|
||||||
|
options={transactionTypeOptions}
|
||||||
|
label='Jenis Transaksi'
|
||||||
|
value={selectedTransactionType}
|
||||||
|
onChange={transactionTypeChangeHandler}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
options={
|
||||||
|
isResponseSuccess(bankRawData)
|
||||||
|
? bankOptions.map((bank) => ({
|
||||||
|
label:
|
||||||
|
bankRawData.data.find((data) => data.id === bank.value)
|
||||||
|
?.alias +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data.find((data) => data.id === bank.value)
|
||||||
|
?.account_number +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data.find((data) => data.id === bank.value)
|
||||||
|
?.owner,
|
||||||
|
value: bank.value,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
label='Bank'
|
||||||
|
value={selectedBank}
|
||||||
|
onChange={bankChangeHandler}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
options={partyTypeOptions}
|
||||||
|
label='Pihak'
|
||||||
|
value={selectedPartyType}
|
||||||
|
onChange={partyTypeChangeHandler}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
label='Cari'
|
||||||
|
placeholder='Cari'
|
||||||
|
value={pendingFilters.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
options={sortByOptions}
|
||||||
|
label='Urutkan Berdasarkan'
|
||||||
|
value={selectedSortBy}
|
||||||
|
onChange={sortByChangeHandler}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
name='startDate'
|
||||||
|
label='Periode Tanggal (Mulai)'
|
||||||
|
value={pendingFilters.startDate}
|
||||||
|
onChange={startDateChangeHandler}
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
name='endDate'
|
||||||
|
label='Periode Tanggal (Akhir)'
|
||||||
|
value={pendingFilters.endDate}
|
||||||
|
onChange={endDateChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Table<Finance>
|
||||||
|
data={isResponseSuccess(finances) ? finances.data : []}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={tableFilterState.page}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(finances) ? finances.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Finance ini (${selectedFinance?.payment_code})?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FinanceTable;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Payload format:
|
||||||
|
* {
|
||||||
|
"party_id": 1,
|
||||||
|
"party_type": "CUSTOMER",
|
||||||
|
"payment_date": "2025-11-21",
|
||||||
|
"payment_method": "Transfer",
|
||||||
|
"bank_id": 1,
|
||||||
|
"reference_number": "DO.MBU.123",
|
||||||
|
"nominal": 25000000,
|
||||||
|
"notes": "Pembayaran piutang penjualan telur"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Type for form values (includes option objects for SelectInput)
|
||||||
|
export type FinanceFormValues = {
|
||||||
|
party_type_option: OptionType | null;
|
||||||
|
party_id_option: OptionType | null;
|
||||||
|
party_account_number: string;
|
||||||
|
payment_date: string;
|
||||||
|
payment_method_option: OptionType | null;
|
||||||
|
bank_id_option: OptionType | null;
|
||||||
|
reference_number: string;
|
||||||
|
nominal: string;
|
||||||
|
notes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FinanceFormSchema = Yup.object().shape({
|
||||||
|
party_type_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Jenis transaksi wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
party_id_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Pihak wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
party_account_number: Yup.string().required('Nomor rekening wajib diisi'),
|
||||||
|
payment_date: Yup.string().required('Tanggal pembayaran wajib diisi'),
|
||||||
|
payment_method_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Metode pembayaran wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
bank_id_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Bank wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
||||||
|
nominal: Yup.string().required('Nominal wajib diisi'),
|
||||||
|
notes: Yup.string().required('Catatan wajib diisi'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateFinanceFormSchema = FinanceFormSchema;
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import {
|
||||||
|
FinanceFormSchema,
|
||||||
|
FinanceFormValues,
|
||||||
|
} from '@/components/pages/finance/add/FormFinanceAdd.schema';
|
||||||
|
import {
|
||||||
|
FINANCE_PARTY_TYPE_OPTIONS,
|
||||||
|
FINANCE_PAYMENT_METHOD_OPTIONS,
|
||||||
|
} from '@/config/constant';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import {
|
||||||
|
CreateFinancePayment,
|
||||||
|
Finance,
|
||||||
|
UpdateFinancePayment,
|
||||||
|
} from '@/types/api/finance/finance';
|
||||||
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
interface FormFinanceAddProps {
|
||||||
|
type?: 'add' | 'edit';
|
||||||
|
initialValues?: Finance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFinanceAdd = ({
|
||||||
|
type = 'add',
|
||||||
|
initialValues,
|
||||||
|
}: FormFinanceAddProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ===== Formik =====
|
||||||
|
const formikInitialValues = useMemo((): FinanceFormValues => {
|
||||||
|
return {
|
||||||
|
party_type_option:
|
||||||
|
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||||
|
(option) => option.value === initialValues?.party.type
|
||||||
|
) || null,
|
||||||
|
party_id_option: {
|
||||||
|
label: initialValues?.party.name || '',
|
||||||
|
value: initialValues?.party.id || 0,
|
||||||
|
},
|
||||||
|
payment_date: initialValues?.payment_date || '',
|
||||||
|
payment_method_option:
|
||||||
|
FINANCE_PAYMENT_METHOD_OPTIONS.find(
|
||||||
|
(option) => option.value === initialValues?.payment_method
|
||||||
|
) || null,
|
||||||
|
bank_id_option: initialValues?.bank
|
||||||
|
? {
|
||||||
|
label: initialValues.bank.name,
|
||||||
|
value: initialValues.bank.id,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
party_account_number: initialValues?.party.account_number || '',
|
||||||
|
reference_number: initialValues?.reference_number || '',
|
||||||
|
nominal: initialValues?.nominal.toString() || '',
|
||||||
|
notes: initialValues?.notes || '',
|
||||||
|
};
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
const formik = useFormik<FinanceFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema: FinanceFormSchema,
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const payload = transformFormValuesToPayload(values);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createFinance(payload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
if (initialValues?.id) {
|
||||||
|
await updateFinance(initialValues.id, payload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Options =====
|
||||||
|
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
|
||||||
|
useSelect(
|
||||||
|
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||||
|
? CustomerApi.basePath
|
||||||
|
: SupplierApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'',
|
||||||
|
{ limit: 'limit' }
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
options: bankOptions,
|
||||||
|
rawData: bankRawData,
|
||||||
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
||||||
|
|
||||||
|
// ===== Helper Functions =====
|
||||||
|
const transformFormValuesToPayload = (
|
||||||
|
values: FinanceFormValues
|
||||||
|
): CreateFinancePayment => {
|
||||||
|
return {
|
||||||
|
party_id: Number(values.party_id_option?.value) || 0,
|
||||||
|
party_type: (values.party_type_option?.value as string) || '',
|
||||||
|
payment_date: formatDate(values.payment_date, 'YYYY-MM-DD'),
|
||||||
|
payment_method: (values.payment_method_option?.value as string) || '',
|
||||||
|
bank_id: Number(values.bank_id_option?.value) || 0,
|
||||||
|
reference_number: values.reference_number,
|
||||||
|
nominal: Number(values.nominal.replace(/\D/g, '')) || 0,
|
||||||
|
notes: values.notes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Handler =====
|
||||||
|
const createFinance = useCallback(
|
||||||
|
async (payload: CreateFinancePayment) => {
|
||||||
|
const response = await FinanceApi.create(payload);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
toast.error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Data berhasil ditambahkan');
|
||||||
|
router.refresh();
|
||||||
|
router.push('/finance');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
const updateFinance = useCallback(
|
||||||
|
async (financeId: number, payload: UpdateFinancePayment) => {
|
||||||
|
const response = await FinanceApi.update(financeId, payload);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
toast.error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Data berhasil diperbarui');
|
||||||
|
router.refresh();
|
||||||
|
router.push('/finance');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-xl mx-auto'>
|
||||||
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
|
<FormHeader
|
||||||
|
title={`${type === 'add' ? 'Tambah' : 'Ubah'} Data Keuangan`}
|
||||||
|
backUrl='/finance'
|
||||||
|
/>
|
||||||
|
<form className='flex flex-col gap-4' onSubmit={formik.handleSubmit}>
|
||||||
|
<SelectInput
|
||||||
|
label='Jenis Transaksi'
|
||||||
|
placeholder='Pilih jenis transaksi'
|
||||||
|
options={FINANCE_PARTY_TYPE_OPTIONS}
|
||||||
|
value={formik.values.party_type_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('party_type_option', value);
|
||||||
|
}}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.party_type_option &&
|
||||||
|
formik.errors.party_type_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.party_type_option &&
|
||||||
|
formik.errors.party_type_option
|
||||||
|
? formik.errors.party_type_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label={
|
||||||
|
formik.values.party_type_option?.value
|
||||||
|
? formatTitleCase(
|
||||||
|
formik.values.party_type_option.value as string
|
||||||
|
)
|
||||||
|
: 'Pilih Jenis Transaksi Dahulu'
|
||||||
|
}
|
||||||
|
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
|
||||||
|
options={partyOptions}
|
||||||
|
value={formik.values.party_id_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('party_id_option', value);
|
||||||
|
}}
|
||||||
|
isLoading={isLoadingPartyOptions}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.party_id_option && formik.errors.party_id_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.party_id_option && formik.errors.party_id_option
|
||||||
|
? formik.errors.party_id_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.party_type_option?.value}
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal'
|
||||||
|
placeholder='Pilih tanggal'
|
||||||
|
name='payment_date'
|
||||||
|
value={formik.values.payment_date}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.payment_date && formik.errors.payment_date
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.payment_date && formik.errors.payment_date
|
||||||
|
? formik.errors.payment_date
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Metode Pembayaran'
|
||||||
|
placeholder='Pilih metode pembayaran'
|
||||||
|
options={FINANCE_PAYMENT_METHOD_OPTIONS}
|
||||||
|
value={formik.values.payment_method_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('payment_method_option', value);
|
||||||
|
}}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.payment_method_option &&
|
||||||
|
formik.errors.payment_method_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.payment_method_option &&
|
||||||
|
formik.errors.payment_method_option
|
||||||
|
? formik.errors.payment_method_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Bank'
|
||||||
|
placeholder='Pilih bank'
|
||||||
|
options={
|
||||||
|
isResponseSuccess(bankRawData)
|
||||||
|
? bankOptions.map((option) => ({
|
||||||
|
label:
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.alias +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.account_number +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.owner,
|
||||||
|
value: option.value,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
value={formik.values.bank_id_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('bank_id_option', value);
|
||||||
|
}}
|
||||||
|
isLoading={isLoadingBankOptions}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.bank_id_option && formik.errors.bank_id_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.bank_id_option && formik.errors.bank_id_option
|
||||||
|
? formik.errors.bank_id_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
|
||||||
|
placeholder={`Masukkan nomor rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'pihak'}`}
|
||||||
|
name='party_account_number'
|
||||||
|
value={formik.values.party_account_number}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.party_account_number &&
|
||||||
|
formik.errors.party_account_number
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.party_account_number &&
|
||||||
|
formik.errors.party_account_number
|
||||||
|
? formik.errors.party_account_number
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label='Nomor Referensi'
|
||||||
|
placeholder='Masukkan nomor referensi'
|
||||||
|
name='reference_number'
|
||||||
|
value={formik.values.reference_number}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.reference_number &&
|
||||||
|
formik.errors.reference_number
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.reference_number &&
|
||||||
|
formik.errors.reference_number
|
||||||
|
? formik.errors.reference_number
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label='Nominal'
|
||||||
|
placeholder='Masukkan nominal'
|
||||||
|
name='nominal'
|
||||||
|
value={formik.values.nominal}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(formik.touched.nominal && formik.errors.nominal)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.nominal && formik.errors.nominal
|
||||||
|
? formik.errors.nominal
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
label='Catatan'
|
||||||
|
placeholder='Masukkan catatan'
|
||||||
|
name='notes'
|
||||||
|
value={formik.values.notes}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(formik.touched.notes && formik.errors.notes)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.notes && formik.errors.notes
|
||||||
|
? formik.errors.notes
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className='flex justify-center gap-4'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
color='warning'
|
||||||
|
className='w-min-24'
|
||||||
|
onClick={() => formik.resetForm()}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='w-min-24'
|
||||||
|
disabled={formik.isSubmitting || !formik.isValid}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormFinanceAdd;
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Payload format for Initial Balance:
|
||||||
|
* {
|
||||||
|
"party_type": "CUSTOMER",
|
||||||
|
"party_id": 1,
|
||||||
|
"bank_id": 1,
|
||||||
|
"reference_number": "IB.MBU.001",
|
||||||
|
"initial_balance_type": "DEBIT",
|
||||||
|
"nominal": 5000000,
|
||||||
|
"note": "Saldo awal piutang customer"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Type for form values (includes option objects for SelectInput)
|
||||||
|
export type InitialBalanceFormValues = {
|
||||||
|
party_type_option: OptionType | null;
|
||||||
|
party_id_option: OptionType | null;
|
||||||
|
bank_id_option: OptionType | null;
|
||||||
|
reference_number: string;
|
||||||
|
initial_balance_type_option: OptionType | null;
|
||||||
|
nominal: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InitialBalanceFormSchema = Yup.object().shape({
|
||||||
|
party_type_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Jenis pihak wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
party_id_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Pihak wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
bank_id_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Bank wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
||||||
|
initial_balance_type_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Tipe saldo awal wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
nominal: Yup.string().required('Nominal wajib diisi'),
|
||||||
|
note: Yup.string().required('Catatan wajib diisi'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateInitialBalanceFormSchema = InitialBalanceFormSchema;
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import {
|
||||||
|
InitialBalanceFormSchema,
|
||||||
|
InitialBalanceFormValues,
|
||||||
|
} from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema';
|
||||||
|
import {
|
||||||
|
FINANCE_INITIAL_BALANCE_TYPE_OPTIONS,
|
||||||
|
FINANCE_PARTY_TYPE_OPTIONS,
|
||||||
|
} from '@/config/constant';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { formatTitleCase } from '@/lib/helper';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import {
|
||||||
|
CreateInitialBalance,
|
||||||
|
Finance,
|
||||||
|
UpdateInitialBalance,
|
||||||
|
} from '@/types/api/finance/finance';
|
||||||
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
interface FormFinanceAddInitialBalanceProps {
|
||||||
|
type?: 'add' | 'edit';
|
||||||
|
initialValues?: Finance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFinanceAddInitialBalance = ({
|
||||||
|
type = 'add',
|
||||||
|
initialValues,
|
||||||
|
}: FormFinanceAddInitialBalanceProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ===== Formik =====
|
||||||
|
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
||||||
|
// Type assertion to handle potential initial_balance_type field
|
||||||
|
const extendedInitialValues = initialValues as Finance & {
|
||||||
|
initial_balance_type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
party_type_option:
|
||||||
|
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||||
|
(option) => option.value === initialValues?.party.type
|
||||||
|
) || null,
|
||||||
|
party_id_option: initialValues?.party
|
||||||
|
? {
|
||||||
|
label: initialValues.party.name,
|
||||||
|
value: initialValues.party.id,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
bank_id_option: initialValues?.bank
|
||||||
|
? {
|
||||||
|
label: initialValues.bank.name,
|
||||||
|
value: initialValues.bank.id,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
reference_number: initialValues?.reference_number || '',
|
||||||
|
initial_balance_type_option:
|
||||||
|
(initialValues?.nominal ?? 0) < 0
|
||||||
|
? FINANCE_INITIAL_BALANCE_TYPE_OPTIONS.find(
|
||||||
|
(option) => option.value === 'NEGATIVE'
|
||||||
|
) || null
|
||||||
|
: FINANCE_INITIAL_BALANCE_TYPE_OPTIONS.find(
|
||||||
|
(option) => option.value === 'POSITIVE'
|
||||||
|
) || null,
|
||||||
|
nominal: initialValues?.nominal?.toString() || '',
|
||||||
|
note: initialValues?.notes || '',
|
||||||
|
};
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
const formik = useFormik<InitialBalanceFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema: InitialBalanceFormSchema,
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const payload = transformFormValuesToPayload(values);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createInitialBalance(payload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
if (initialValues?.id) {
|
||||||
|
await updateInitialBalance(initialValues.id, payload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Options =====
|
||||||
|
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
|
||||||
|
useSelect(
|
||||||
|
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||||
|
? CustomerApi.basePath
|
||||||
|
: SupplierApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'',
|
||||||
|
{ limit: 'limit' }
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
options: bankOptions,
|
||||||
|
rawData: bankRawData,
|
||||||
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
||||||
|
|
||||||
|
// ===== Helper Functions =====
|
||||||
|
const transformFormValuesToPayload = (
|
||||||
|
values: InitialBalanceFormValues
|
||||||
|
): CreateInitialBalance => {
|
||||||
|
return {
|
||||||
|
party_type: (values.party_type_option?.value as string) || '',
|
||||||
|
party_id: Number(values.party_id_option?.value) || 0,
|
||||||
|
bank_id: Number(values.bank_id_option?.value) || 0,
|
||||||
|
reference_number: values.reference_number,
|
||||||
|
initial_balance_type:
|
||||||
|
(values.initial_balance_type_option?.value as string) || '',
|
||||||
|
nominal: Number(values.nominal.replace(/\D/g, '')) || 0,
|
||||||
|
note: values.note,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Handler =====
|
||||||
|
const createInitialBalance = useCallback(
|
||||||
|
async (payload: CreateInitialBalance) => {
|
||||||
|
const response = await FinanceApi.createInitialBalances(payload);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
toast.error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Saldo awal berhasil ditambahkan');
|
||||||
|
router.refresh();
|
||||||
|
router.push('/finance');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateInitialBalance = useCallback(
|
||||||
|
async (financeId: number, payload: UpdateInitialBalance) => {
|
||||||
|
const response = await FinanceApi.updateInitialBalances(
|
||||||
|
financeId,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
toast.error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Saldo awal berhasil diperbarui');
|
||||||
|
router.refresh();
|
||||||
|
router.push('/finance');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-xl mx-auto'>
|
||||||
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
|
<FormHeader
|
||||||
|
title={`${type === 'add' ? 'Tambah' : 'Ubah'} Saldo Awal`}
|
||||||
|
backUrl='/finance'
|
||||||
|
/>
|
||||||
|
<form className='flex flex-col gap-4' onSubmit={formik.handleSubmit}>
|
||||||
|
<SelectInput
|
||||||
|
label='Jenis Pihak'
|
||||||
|
placeholder='Pilih jenis pihak'
|
||||||
|
options={FINANCE_PARTY_TYPE_OPTIONS}
|
||||||
|
value={formik.values.party_type_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('party_type_option', value);
|
||||||
|
}}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.party_type_option &&
|
||||||
|
formik.errors.party_type_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.party_type_option &&
|
||||||
|
formik.errors.party_type_option
|
||||||
|
? formik.errors.party_type_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label={
|
||||||
|
formik.values.party_type_option?.value
|
||||||
|
? formatTitleCase(
|
||||||
|
formik.values.party_type_option.value as string
|
||||||
|
)
|
||||||
|
: 'Pilih Jenis Pihak Dahulu'
|
||||||
|
}
|
||||||
|
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
|
||||||
|
options={partyOptions}
|
||||||
|
value={formik.values.party_id_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('party_id_option', value);
|
||||||
|
}}
|
||||||
|
isLoading={isLoadingPartyOptions}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.party_id_option && formik.errors.party_id_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.party_id_option && formik.errors.party_id_option
|
||||||
|
? formik.errors.party_id_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.party_type_option?.value}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Bank'
|
||||||
|
placeholder='Pilih bank'
|
||||||
|
options={
|
||||||
|
isResponseSuccess(bankRawData)
|
||||||
|
? bankOptions.map((option) => ({
|
||||||
|
label:
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.alias +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.account_number +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.owner,
|
||||||
|
value: option.value,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
value={formik.values.bank_id_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('bank_id_option', value);
|
||||||
|
}}
|
||||||
|
isLoading={isLoadingBankOptions}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.bank_id_option && formik.errors.bank_id_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.bank_id_option && formik.errors.bank_id_option
|
||||||
|
? formik.errors.bank_id_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label='Nomor Referensi'
|
||||||
|
placeholder='Masukkan nomor referensi'
|
||||||
|
name='reference_number'
|
||||||
|
value={formik.values.reference_number}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.reference_number &&
|
||||||
|
formik.errors.reference_number
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.reference_number &&
|
||||||
|
formik.errors.reference_number
|
||||||
|
? formik.errors.reference_number
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Tipe Saldo Awal'
|
||||||
|
placeholder='Pilih tipe saldo awal'
|
||||||
|
options={FINANCE_INITIAL_BALANCE_TYPE_OPTIONS}
|
||||||
|
value={formik.values.initial_balance_type_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('initial_balance_type_option', value);
|
||||||
|
}}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.initial_balance_type_option &&
|
||||||
|
formik.errors.initial_balance_type_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.initial_balance_type_option &&
|
||||||
|
formik.errors.initial_balance_type_option
|
||||||
|
? formik.errors.initial_balance_type_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label='Nominal'
|
||||||
|
placeholder='Masukkan nominal'
|
||||||
|
name='nominal'
|
||||||
|
value={formik.values.nominal}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(formik.touched.nominal && formik.errors.nominal)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.nominal && formik.errors.nominal
|
||||||
|
? formik.errors.nominal
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
allowNegative={false}
|
||||||
|
startAdornment={
|
||||||
|
formik.values.initial_balance_type_option?.value ===
|
||||||
|
'POSITIVE' ? (
|
||||||
|
<Icon icon='mdi:plus' />
|
||||||
|
) : formik.values.initial_balance_type_option?.value ===
|
||||||
|
'NEGATIVE' ? (
|
||||||
|
<Icon icon='mdi:minus' />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
label='Catatan'
|
||||||
|
placeholder='Masukkan catatan'
|
||||||
|
name='note'
|
||||||
|
value={formik.values.note}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(formik.touched.note && formik.errors.note)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.note && formik.errors.note
|
||||||
|
? formik.errors.note
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className='flex justify-center gap-4'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
color='warning'
|
||||||
|
className='w-min-24'
|
||||||
|
onClick={() => formik.resetForm()}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='w-min-24'
|
||||||
|
disabled={formik.isSubmitting || !formik.isValid}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormFinanceAddInitialBalance;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
// Type for form values (includes option objects for SelectInput)
|
||||||
|
export type InjectionFormValues = {
|
||||||
|
bank_id_option: OptionType | null;
|
||||||
|
adjustment_date: string;
|
||||||
|
nominal: string;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InjectionFormSchema = Yup.object<InjectionFormValues>({
|
||||||
|
bank_id_option: Yup.mixed()
|
||||||
|
.nullable()
|
||||||
|
.test(
|
||||||
|
'is-valid-option',
|
||||||
|
'Bank wajib diisi',
|
||||||
|
(value) => value !== null && value !== undefined
|
||||||
|
),
|
||||||
|
adjustment_date: Yup.string().required('Tanggal penyesuaian wajib diisi'),
|
||||||
|
nominal: Yup.string().required('Nominal wajib diisi'),
|
||||||
|
note: Yup.string().required('Catatan wajib diisi'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateInjectionFormSchema = InjectionFormSchema;
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
import TextArea from '@/components/input/TextArea';
|
||||||
|
import {
|
||||||
|
InjectionFormSchema,
|
||||||
|
InjectionFormValues,
|
||||||
|
} from '@/components/pages/finance/add/injection/FormFinanceInjection.schema';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
|
import { BankApi } from '@/services/api/master-data';
|
||||||
|
import {
|
||||||
|
CreateInjection,
|
||||||
|
Finance,
|
||||||
|
UpdateInjection,
|
||||||
|
} from '@/types/api/finance/finance';
|
||||||
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
interface FormFinanceInjectionProps {
|
||||||
|
type?: 'add' | 'edit';
|
||||||
|
initialValues?: Finance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFinanceInjection = ({
|
||||||
|
type = 'add',
|
||||||
|
initialValues,
|
||||||
|
}: FormFinanceInjectionProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ===== Formik =====
|
||||||
|
const formikInitialValues = useMemo((): InjectionFormValues => {
|
||||||
|
return {
|
||||||
|
bank_id_option: initialValues?.bank
|
||||||
|
? {
|
||||||
|
label: initialValues.bank.name,
|
||||||
|
value: initialValues.bank.id,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
adjustment_date: initialValues?.payment_date || '',
|
||||||
|
nominal: initialValues?.nominal?.toString() || '',
|
||||||
|
note: initialValues?.notes || '',
|
||||||
|
};
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
const formik = useFormik<InjectionFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema: InjectionFormSchema,
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const payload = transformFormValuesToPayload(values);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createInjection(payload);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'edit':
|
||||||
|
if (initialValues?.id) {
|
||||||
|
await updateInjection(initialValues.id, payload);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Options =====
|
||||||
|
const {
|
||||||
|
options: bankOptions,
|
||||||
|
rawData: bankRawData,
|
||||||
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
||||||
|
|
||||||
|
// ===== Helper Functions =====
|
||||||
|
const transformFormValuesToPayload = (
|
||||||
|
values: InjectionFormValues
|
||||||
|
): CreateInjection => {
|
||||||
|
return {
|
||||||
|
bank_id: Number(values.bank_id_option?.value) || 0,
|
||||||
|
adjustment_date: formatDate(values.adjustment_date, 'YYYY-MM-DD'),
|
||||||
|
nominal: Number(values.nominal.replace(/\D/g, '')) || 0,
|
||||||
|
notes: values.note,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Handler =====
|
||||||
|
const createInjection = useCallback(
|
||||||
|
async (payload: CreateInjection) => {
|
||||||
|
const response = await FinanceApi.createInjections(payload);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
toast.error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Injeksi dana berhasil ditambahkan');
|
||||||
|
router.refresh();
|
||||||
|
router.push('/finance');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateInjection = useCallback(
|
||||||
|
async (financeId: number, payload: UpdateInjection) => {
|
||||||
|
const response = await FinanceApi.updateInjections(financeId, payload);
|
||||||
|
|
||||||
|
if (isResponseError(response)) {
|
||||||
|
toast.error(response.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Injeksi dana berhasil diperbarui');
|
||||||
|
router.refresh();
|
||||||
|
router.push('/finance');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-xl mx-auto'>
|
||||||
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
|
<FormHeader
|
||||||
|
title={`${type === 'add' ? 'Tambah' : 'Ubah'} Injeksi Dana`}
|
||||||
|
backUrl='/finance'
|
||||||
|
/>
|
||||||
|
<form className='flex flex-col gap-4' onSubmit={formik.handleSubmit}>
|
||||||
|
<SelectInput
|
||||||
|
label='Bank'
|
||||||
|
placeholder='Pilih bank'
|
||||||
|
options={
|
||||||
|
isResponseSuccess(bankRawData)
|
||||||
|
? bankOptions.map((option) => ({
|
||||||
|
label:
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.alias +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.account_number +
|
||||||
|
' - ' +
|
||||||
|
bankRawData.data?.find(
|
||||||
|
(item) => item.id === option.value
|
||||||
|
)?.owner,
|
||||||
|
value: option.value,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
value={formik.values.bank_id_option}
|
||||||
|
onChange={(value) => {
|
||||||
|
formik.setFieldValue('bank_id_option', value);
|
||||||
|
}}
|
||||||
|
isLoading={isLoadingBankOptions}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.bank_id_option && formik.errors.bank_id_option
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.bank_id_option && formik.errors.bank_id_option
|
||||||
|
? formik.errors.bank_id_option
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Penyesuaian'
|
||||||
|
placeholder='Pilih tanggal penyesuaian'
|
||||||
|
name='adjustment_date'
|
||||||
|
value={formik.values.adjustment_date}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(
|
||||||
|
formik.touched.adjustment_date && formik.errors.adjustment_date
|
||||||
|
)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.adjustment_date && formik.errors.adjustment_date
|
||||||
|
? formik.errors.adjustment_date
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label='Nominal'
|
||||||
|
placeholder='Masukkan nominal'
|
||||||
|
name='nominal'
|
||||||
|
value={formik.values.nominal}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(formik.touched.nominal && formik.errors.nominal)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.nominal && formik.errors.nominal
|
||||||
|
? formik.errors.nominal
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
allowNegative={true}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
label='Catatan'
|
||||||
|
placeholder='Masukkan catatan'
|
||||||
|
name='note'
|
||||||
|
value={formik.values.note}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={Boolean(formik.touched.note && formik.errors.note)}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.note && formik.errors.note
|
||||||
|
? formik.errors.note
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className='flex justify-center gap-4'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
color='warning'
|
||||||
|
className='w-min-24'
|
||||||
|
onClick={() => formik.resetForm()}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='w-min-24'
|
||||||
|
disabled={formik.isSubmitting || !formik.isValid}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormFinanceInjection;
|
||||||
@@ -79,18 +79,6 @@ const InventoryAdjustmentTable = () => {
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: 'before_quantity',
|
|
||||||
// header: 'Stok Sebelum',
|
|
||||||
// accessorFn: (row) =>
|
|
||||||
// formatNumber(String(row.product_warehouse?.quantity)),
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'after_quantity',
|
|
||||||
// header: 'Stok Sesudah',
|
|
||||||
// accessorFn: (row) =>
|
|
||||||
// formatNumber(String(row.product_warehouse?.quantity)),
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: 'quantity',
|
id: 'quantity',
|
||||||
header: 'Kuantitas',
|
header: 'Kuantitas',
|
||||||
@@ -187,14 +175,6 @@ const InventoryAdjustmentTable = () => {
|
|||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
{/* <DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Stock Adjustment'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
|
||||||
/> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-end'>
|
<div className='flex flex-row justify-end'>
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ const MarketingTable = () => {
|
|||||||
value={pageSize}
|
value={pageSize}
|
||||||
onChange={pageSizeChangeHandler}
|
onChange={pageSizeChangeHandler}
|
||||||
options={ROWS_OPTIONS}
|
options={ROWS_OPTIONS}
|
||||||
|
className='flex sm:flex-row flex-col gap-3 items-end justify-end'
|
||||||
>
|
>
|
||||||
{/* select multiple product */}
|
{/* select multiple product */}
|
||||||
<SelectInput
|
<SelectInput
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ const MarketingForm = ({
|
|||||||
wrapper: 'bg-white w-full',
|
wrapper: 'bg-white w-full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='grid grid-cols-2 gap-3 mt-3'>
|
<div className='grid sm:grid-cols-2 gap-3 mt-3'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Pelanggan'
|
label='Pelanggan'
|
||||||
options={customerOptions}
|
options={customerOptions}
|
||||||
@@ -651,7 +651,7 @@ const MarketingForm = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input Notes */}
|
{/* Input Notes */}
|
||||||
<div className='grid grid-cols-2 gap-3'>
|
<div className='grid sm:grid-cols-2 gap-3'>
|
||||||
<DebouncedTextArea
|
<DebouncedTextArea
|
||||||
required
|
required
|
||||||
name='notes'
|
name='notes'
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='grid grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-2 gap-4'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={options}
|
options={options}
|
||||||
label='Produk'
|
label='Produk'
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ const SalesOrderProductForm = ({
|
|||||||
{/* <small className='block text-rose-500'>
|
{/* <small className='block text-rose-500'>
|
||||||
{JSON.stringify(formik.errors)}
|
{JSON.stringify(formik.errors)}
|
||||||
</small> */}
|
</small> */}
|
||||||
<div className='grid grid-cols-2 gap-4 z-200'>
|
<div className='grid sm:grid-cols-2 gap-4 z-200'>
|
||||||
<PatternInput
|
<PatternInput
|
||||||
name='vehicle_number'
|
name='vehicle_number'
|
||||||
label='No. Polisi'
|
label='No. Polisi'
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||||
|
import { ProductionStandard } from '@/types/api/master-data/production-standard';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<ProductionStandard, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.production_standards.detail'>
|
||||||
|
<Button
|
||||||
|
href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.production_standards.update'>
|
||||||
|
<Button
|
||||||
|
href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.production_standards.delete'>
|
||||||
|
<Button
|
||||||
|
onClick={deleteClickHandler}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='text-error hover:text-inherit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</RowOptionsMenuWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProductionStandardTable = () => {
|
||||||
|
const deleteModal = useModal();
|
||||||
|
|
||||||
|
const [selectedProductionStandard, setSelectedProductionStandard] = useState<
|
||||||
|
ProductionStandard | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: productionStandards,
|
||||||
|
isLoading: productionStandardsLoading,
|
||||||
|
mutate: refreshProductionStandards,
|
||||||
|
} = useSWR(
|
||||||
|
`${ProductionStandardApi.basePath}`,
|
||||||
|
ProductionStandardApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
await ProductionStandardApi.delete(
|
||||||
|
selectedProductionStandard?.id as number
|
||||||
|
);
|
||||||
|
refreshProductionStandards();
|
||||||
|
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Successfully delete Production Standard!');
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex flex-col gap-6 p-6'>
|
||||||
|
<div className='flex flex-row gap-6 justify-start'>
|
||||||
|
<RequirePermission permissions='lti.master.production_standards.create'>
|
||||||
|
<Button
|
||||||
|
href='/master-data/production-standard/add'
|
||||||
|
variant='outline'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:plus' /> Tambah
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
<RequirePermission permissions='lti.master.production_standards.list'>
|
||||||
|
<Table<ProductionStandard>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(productionStandards)
|
||||||
|
? productionStandards.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'No',
|
||||||
|
accessorFn: (row, index) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Nama',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Kategori',
|
||||||
|
accessorFn: (row) => row.project_category,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props) => {
|
||||||
|
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 = () => {
|
||||||
|
setSelectedProductionStandard(props.row.original);
|
||||||
|
deleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentPageSize > 2 && (
|
||||||
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='dropdown'
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowDropdownOptions>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPageSize <= 2 && (
|
||||||
|
<RowCollapseOptions>
|
||||||
|
<RowOptionsMenu
|
||||||
|
type='collapse'
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
/>
|
||||||
|
</RowCollapseOptions>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
className={{
|
||||||
|
headerColumnClassName: cn(
|
||||||
|
TABLE_DEFAULT_STYLING.headerColumnClassName,
|
||||||
|
'last:flex last:flex-row last:justify-end'
|
||||||
|
),
|
||||||
|
bodyColumnClassName: cn(
|
||||||
|
TABLE_DEFAULT_STYLING.bodyColumnClassName,
|
||||||
|
'last:flex last:flex-row last:justify-end'
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
<RequirePermission permissions='lti.master.production_standards.delete'>
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Production Standard ini (${selectedProductionStandard?.name})?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RequirePermission>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionStandardTable;
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
// Schema for LAYING category (production_standard_details is required)
|
||||||
|
const LayingRepeaterFormSchema = Yup.object({
|
||||||
|
week: Yup.number().required('Minggu wajib diisi!'),
|
||||||
|
production_standard_uniformity_details: Yup.object({
|
||||||
|
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
||||||
|
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
||||||
|
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
||||||
|
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
||||||
|
}),
|
||||||
|
production_standard_details: Yup.object({
|
||||||
|
target_hen_day_production: Yup.number().required(
|
||||||
|
'Produksi telur per hari wajib diisi!'
|
||||||
|
),
|
||||||
|
target_hen_house_production: Yup.number().required(
|
||||||
|
'Produksi telur per kandang wajib diisi!'
|
||||||
|
),
|
||||||
|
target_egg_weight: Yup.number().required('Berat telur wajib diisi!'),
|
||||||
|
target_egg_mass: Yup.number().required('Massa telur wajib diisi!'),
|
||||||
|
}).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for GROWING category (production_standard_details is optional)
|
||||||
|
const GrowingRepeaterFormSchema = Yup.object({
|
||||||
|
week: Yup.number().required('Minggu wajib diisi!'),
|
||||||
|
production_standard_uniformity_details: Yup.object({
|
||||||
|
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
||||||
|
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
||||||
|
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
||||||
|
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
||||||
|
}),
|
||||||
|
production_standard_details: Yup.object({
|
||||||
|
target_hen_day_production: Yup.number().optional(),
|
||||||
|
target_hen_house_production: Yup.number().optional(),
|
||||||
|
target_egg_weight: Yup.number().optional(),
|
||||||
|
target_egg_mass: Yup.number().optional(),
|
||||||
|
}).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Explicit types for better type inference
|
||||||
|
export type LayingRepeaterFormValues = Yup.InferType<
|
||||||
|
typeof LayingRepeaterFormSchema
|
||||||
|
>;
|
||||||
|
export type GrowingRepeaterFormValues = Yup.InferType<
|
||||||
|
typeof GrowingRepeaterFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
// Union type for repeater form values
|
||||||
|
export type ProductionStandardRepeaterFormSchemaValues =
|
||||||
|
| LayingRepeaterFormValues
|
||||||
|
| GrowingRepeaterFormValues;
|
||||||
|
|
||||||
|
// Dynamic schema factory for repeater form based on project category
|
||||||
|
export const createProductionStandardRepeaterFormSchema = (
|
||||||
|
category: string
|
||||||
|
) => {
|
||||||
|
// For LAYING category, production_standard_details is required
|
||||||
|
if (category === 'LAYING') {
|
||||||
|
return LayingRepeaterFormSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For GROWING category, production_standard_details is optional
|
||||||
|
return GrowingRepeaterFormSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamic schema factory for main form based on project category
|
||||||
|
export const createProductionStandardFormSchema = (category: string) => {
|
||||||
|
return Yup.object({
|
||||||
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
|
project_category: Yup.string().required('Kategori proyek wajib diisi!'),
|
||||||
|
details: Yup.array().of(
|
||||||
|
createProductionStandardRepeaterFormSchema(category)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static schemas for backward compatibility (default to LAYING)
|
||||||
|
export const ProductionStandardFormSchema =
|
||||||
|
createProductionStandardFormSchema('LAYING');
|
||||||
|
|
||||||
|
export const UpdateProductionStandardFormSchema = ProductionStandardFormSchema;
|
||||||
|
|
||||||
|
export type ProductionStandardFormValues = Yup.InferType<
|
||||||
|
typeof ProductionStandardFormSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ProductionStandardRepeaterFormSchema = LayingRepeaterFormSchema;
|
||||||
|
|
||||||
|
export const UpdateProductionStandardRepeaterFormSchema =
|
||||||
|
ProductionStandardRepeaterFormSchema;
|
||||||
+1247
File diff suppressed because it is too large
Load Diff
@@ -1,324 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
|
||||||
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
|
||||||
import { ChickinApi } from '@/services/api/production/chickin';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { Chickin } from '@/types/api/production/chickin';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { CellContext, SortingState } from '@tanstack/react-table';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const ChickinTable = () => {
|
|
||||||
const {
|
|
||||||
state: tableFilterState,
|
|
||||||
updateFilter,
|
|
||||||
setPage,
|
|
||||||
setPageSize,
|
|
||||||
toQueryString: getTableFilterQueryString,
|
|
||||||
} = useTableFilter({
|
|
||||||
initial: {
|
|
||||||
search: '',
|
|
||||||
},
|
|
||||||
paramMap: {
|
|
||||||
page: 'page',
|
|
||||||
pageSize: 'limit',
|
|
||||||
search: 'search',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
|
||||||
const [selectedChickin, setSelectedChickin] = useState<Chickin | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
|
|
||||||
const deleteModal = useModal();
|
|
||||||
const chickinModal = useModal();
|
|
||||||
|
|
||||||
// Data Fetching
|
|
||||||
const {
|
|
||||||
data: chickins,
|
|
||||||
isLoading,
|
|
||||||
mutate: refreshChickins,
|
|
||||||
} = useSWR(
|
|
||||||
`${ChickinApi.basePath}${getTableFilterQueryString()}`,
|
|
||||||
ChickinApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateFilter('search', event.target.value);
|
|
||||||
setPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
const newVal = val as OptionType;
|
|
||||||
setPageSize(newVal.value as number);
|
|
||||||
setPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
|
||||||
setIsDeleteLoading(true);
|
|
||||||
try {
|
|
||||||
await ChickinApi.delete(selectedChickin?.id as number);
|
|
||||||
refreshChickins();
|
|
||||||
deleteModal.closeModal();
|
|
||||||
} finally {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
|
||||||
<Button
|
|
||||||
href='/production/project-flock/chickin/add?projectFlockId=1'
|
|
||||||
variant='outline'
|
|
||||||
color='primary'
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon icon='uil:plus' width={24} height={24} />
|
|
||||||
Tambah
|
|
||||||
</Button>
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Chickin'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TableRowSizeSelector
|
|
||||||
value={tableFilterState.pageSize}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Table<Chickin>
|
|
||||||
data={isResponseSuccess(chickins) ? chickins?.data : []}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: '#',
|
|
||||||
cell: (props) =>
|
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.project_flock_kandang?.kandang.name,
|
|
||||||
header: 'Kandang',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.quantity,
|
|
||||||
header: 'Jumlah Chickin',
|
|
||||||
cell: (props) => {
|
|
||||||
if (props.row.original.quantity) {
|
|
||||||
return formatNumber(props.row.original.quantity);
|
|
||||||
} else {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.chick_in_date,
|
|
||||||
header: 'Tanggal Chickin',
|
|
||||||
cell: (props) => {
|
|
||||||
if (props.row.original.chick_in_date) {
|
|
||||||
return new Date(
|
|
||||||
props.row.original.chick_in_date
|
|
||||||
).toLocaleDateString('id-ID');
|
|
||||||
} else {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.note,
|
|
||||||
header: 'Catatan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aksi',
|
|
||||||
cell: (props) => {
|
|
||||||
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 = () => {
|
|
||||||
setSelectedChickin(props.row.original);
|
|
||||||
deleteModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const editClickHandler = () => {
|
|
||||||
setSelectedChickin(props.row.original);
|
|
||||||
chickinModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{currentPageSize > 2 && (
|
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
|
||||||
<RowOptionsMenu
|
|
||||||
type='dropdown'
|
|
||||||
props={props}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
editClickHandler={editClickHandler}
|
|
||||||
/>
|
|
||||||
</RowDropdownOptions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<RowOptionsMenu
|
|
||||||
type='collapse'
|
|
||||||
props={props}
|
|
||||||
deleteClickHandler={deleteClickHandler}
|
|
||||||
editClickHandler={editClickHandler}
|
|
||||||
/>
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={isResponseSuccess(chickins) ? chickins?.meta?.page : 0}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(chickins) ? chickins?.meta?.total_results : 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
isLoading={isLoading}
|
|
||||||
sorting={sorting}
|
|
||||||
setSorting={setSorting}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'mb-20':
|
|
||||||
isResponseSuccess(chickins) && chickins?.data?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ConfirmationModal
|
|
||||||
ref={deleteModal.ref}
|
|
||||||
type='error'
|
|
||||||
text={`Apakah anda yakin ingin menghapus data Chickin ini?`}
|
|
||||||
secondaryButton={{
|
|
||||||
text: 'Tidak',
|
|
||||||
}}
|
|
||||||
primaryButton={{
|
|
||||||
text: 'Ya',
|
|
||||||
onClick: confirmationModalDeleteClickHandler,
|
|
||||||
isLoading: isDeleteLoading,
|
|
||||||
color: 'error',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Modal ref={chickinModal.ref}>
|
|
||||||
<div className='flex flex-row justify-between items-center'>
|
|
||||||
<h1 className='text-xl font-semibold text-center mb-6'>
|
|
||||||
Chickin Kandang -{' '}
|
|
||||||
{selectedChickin?.project_flock_kandang &&
|
|
||||||
selectedChickin?.project_flock_kandang.kandang?.name}
|
|
||||||
</h1>
|
|
||||||
<Button
|
|
||||||
color='error'
|
|
||||||
variant='link'
|
|
||||||
onClick={chickinModal.closeModal}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className='text-black'
|
|
||||||
icon='uil:times'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/* <ChickinForm
|
|
||||||
initialValues={selectedChickin}
|
|
||||||
formType='edit'
|
|
||||||
afterSubmit={() => {
|
|
||||||
refreshChickins();
|
|
||||||
chickinModal.closeModal();
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
|
||||||
type = 'dropdown',
|
|
||||||
props,
|
|
||||||
editClickHandler,
|
|
||||||
deleteClickHandler,
|
|
||||||
}: {
|
|
||||||
type: 'dropdown' | 'collapse';
|
|
||||||
props: CellContext<Chickin, unknown>;
|
|
||||||
editClickHandler: () => void;
|
|
||||||
deleteClickHandler: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<RowOptionsMenuWrapper type={type}>
|
|
||||||
<Button
|
|
||||||
href={`/production/project-flock/chickin/detail?chickinId=${props.row.original.id}`}
|
|
||||||
variant='ghost'
|
|
||||||
color='primary'
|
|
||||||
className='justify-start text-sm'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
|
||||||
Detail
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='ghost'
|
|
||||||
color='warning'
|
|
||||||
className='justify-start text-sm'
|
|
||||||
onClick={editClickHandler}
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={deleteClickHandler}
|
|
||||||
variant='ghost'
|
|
||||||
color='error'
|
|
||||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:delete-outline-rounded'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
/>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</RowOptionsMenuWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChickinTable;
|
|
||||||
@@ -17,6 +17,7 @@ import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
|
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
const ChickinFormKandang = ({
|
const ChickinFormKandang = ({
|
||||||
formType = 'add',
|
formType = 'add',
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -144,6 +145,7 @@ const ChickinFormKandang = ({
|
|||||||
<h2 className='text-xl font-semibold'>Informasi Chick In</h2>
|
<h2 className='text-xl font-semibold'>Informasi Chick In</h2>
|
||||||
{/* Badge Row */}
|
{/* Badge Row */}
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
|
<RequirePermission permissions='lti.production.chickins.create'>
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={'success'}
|
color={'success'}
|
||||||
@@ -151,10 +153,16 @@ const ChickinFormKandang = ({
|
|||||||
badge: 'rounded-lg px-2',
|
badge: 'rounded-lg px-2',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:circle' width={12} height={12} color={'success'} />{' '}
|
<Icon
|
||||||
|
icon='mdi:circle'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
color={'success'}
|
||||||
|
/>{' '}
|
||||||
Perlu Chick In ({initialValues.available_qtys?.length ?? 0})
|
Perlu Chick In ({initialValues.available_qtys?.length ?? 0})
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||||
|
</RequirePermission>
|
||||||
<Badge
|
<Badge
|
||||||
color='neutral'
|
color='neutral'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
@@ -176,11 +184,13 @@ const ChickinFormKandang = ({
|
|||||||
afterSubmit={afterSubmitFormChickin}
|
afterSubmit={afterSubmitFormChickin}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<RequirePermission permissions='lti.production.chickins.create'>
|
||||||
<ChickinFormView
|
<ChickinFormView
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
formType={formType}
|
formType={formType}
|
||||||
afterSubmit={afterSubmitFormChickin}
|
afterSubmit={afterSubmitFormChickin}
|
||||||
/>
|
/>
|
||||||
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Alert from '@/components/Alert';
|
import Alert from '@/components/Alert';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import PillBadge from '@/components/PillBadge';
|
import PillBadge from '@/components/PillBadge';
|
||||||
@@ -146,6 +147,7 @@ const ChickinLogsView = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{initialValues?.approval?.step_number <= 2 && (
|
{initialValues?.approval?.step_number <= 2 && (
|
||||||
|
<RequirePermission permissions='lti.production.chickins.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='success'
|
color='success'
|
||||||
onClick={handleClickApprove}
|
onClick={handleClickApprove}
|
||||||
@@ -154,6 +156,7 @@ const ChickinLogsView = ({
|
|||||||
<Icon width={24} height={24} icon='material-symbols:check' />
|
<Icon width={24} height={24} icon='material-symbols:check' />
|
||||||
Approve Semua Chick In
|
Approve Semua Chick In
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{chickinErrorMessage && (
|
{chickinErrorMessage && (
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='min-h-screen w-full p-0 sm:p-4'>
|
<div className='min-h-screen w-full p-4'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col justify-between items-end gap-2'>
|
<div className='w-full flex flex-col justify-between items-end gap-2'>
|
||||||
<div className='flex flex-col sm:flex-row gap-3 w-full'>
|
<div className='flex flex-col sm:flex-row gap-3 w-full'>
|
||||||
@@ -307,32 +307,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
{/* <Button
|
|
||||||
variant='outline'
|
|
||||||
color='success'
|
|
||||||
onClick={() => {
|
|
||||||
setApprovalAction('APPROVED');
|
|
||||||
confirmModal.openModal();
|
|
||||||
}}
|
|
||||||
disabled={selectedRowIds.length === 0}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='error'
|
|
||||||
onClick={() => {
|
|
||||||
setApprovalAction('REJECTED');
|
|
||||||
confirmModal.openModal();
|
|
||||||
}}
|
|
||||||
disabled={selectedRowIds.length === 0}
|
|
||||||
className='w-full sm:w-fit'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:times' width={24} height={24} />
|
|
||||||
Reject
|
|
||||||
</Button> */}
|
|
||||||
<div className='ms-auto w-full sm:w-auto'>
|
<div className='ms-auto w-full sm:w-auto'>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
@@ -551,49 +525,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
formatDate(props.row.original.created_at, 'MMM DD, YYYY'),
|
formatDate(props.row.original.created_at, 'MMM DD, YYYY'),
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// header: 'Aksi',
|
|
||||||
// cell: (props) => {
|
|
||||||
// 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 = () => {
|
|
||||||
// setSelectedProjectFlock(props.row.original);
|
|
||||||
// deleteModal.openModal();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// {currentPageSize > 2 && (
|
|
||||||
// <RowDropdownOptions isLast2Rows={isLast2Rows}>
|
|
||||||
// <RowOptionsMenu
|
|
||||||
// type='dropdown'
|
|
||||||
// props={props}
|
|
||||||
// deleteClickHandler={deleteClickHandler}
|
|
||||||
// />
|
|
||||||
// </RowDropdownOptions>
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// {currentPageSize <= 2 && (
|
|
||||||
// <RowCollapseOptions>
|
|
||||||
// <RowOptionsMenu
|
|
||||||
// type='collapse'
|
|
||||||
// props={props}
|
|
||||||
// deleteClickHandler={deleteClickHandler}
|
|
||||||
// />
|
|
||||||
// </RowCollapseOptions>
|
|
||||||
// )}
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
]}
|
]}
|
||||||
pageSize={tableFilterState.pageSize}
|
pageSize={tableFilterState.pageSize}
|
||||||
page={
|
page={
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Badge from '@/components/Badge';
|
|
||||||
import Button from '@/components/Button';
|
|
||||||
import Card from '@/components/Card';
|
|
||||||
import SelectInput, {
|
|
||||||
OptionType,
|
|
||||||
useSelect,
|
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import PillBadge from '@/components/PillBadge';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
|
||||||
import { cn, formatDate, formatTitleCase } from '@/lib/helper';
|
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
|
||||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
|
||||||
|
|
||||||
const ProjectFlockChickinDetail = ({
|
|
||||||
projectFlockId,
|
|
||||||
}: {
|
|
||||||
projectFlockId: number | undefined;
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Tables Props
|
|
||||||
const { state: tableFilterState } = useTableFilter({
|
|
||||||
initial: { search: '' },
|
|
||||||
paramMap: { page: 'page', pageSize: 'limit' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// States
|
|
||||||
const [searchProjectFlock, setSearchProjectFlock] = useState('');
|
|
||||||
const [selectedProjectFlock, setSelectedProjectFlock] =
|
|
||||||
useState<OptionType | null>(null);
|
|
||||||
const [projectFlock, setProjectFlock] = useState<ProjectFlock>();
|
|
||||||
|
|
||||||
// Fetch Data
|
|
||||||
const { data: listProjectFlockKandang } = useSWR(
|
|
||||||
`${ProjectFlockKandangApi.basePath}?${new URLSearchParams({
|
|
||||||
search: searchProjectFlock,
|
|
||||||
project_flock_id:
|
|
||||||
projectFlock?.id?.toString() ?? projectFlockId?.toString() ?? '',
|
|
||||||
}).toString()}`,
|
|
||||||
ProjectFlockKandangApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
options: options,
|
|
||||||
isLoadingOptions: isLoadingListProjectFlock,
|
|
||||||
rawData: listProjectFlock,
|
|
||||||
} = useSelect<ProjectFlock>(
|
|
||||||
ProjectFlockApi.basePath,
|
|
||||||
'id',
|
|
||||||
'flock_name',
|
|
||||||
'',
|
|
||||||
{
|
|
||||||
search: searchProjectFlock,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle Function
|
|
||||||
const handleChickinClick = async (
|
|
||||||
projectFlockKandang: ProjectFlockKandang
|
|
||||||
) => {
|
|
||||||
router.push(
|
|
||||||
`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${projectFlockKandang.id}&projectFlockId=${projectFlockId ?? selectedProjectFlock?.value}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeProjectFlock = (val: OptionType | null) => {
|
|
||||||
setSelectedProjectFlock(val);
|
|
||||||
if (isResponseSuccess(listProjectFlock) && val) {
|
|
||||||
const selected = listProjectFlock.data.find(
|
|
||||||
(pf) => pf.id === Number(val.value)
|
|
||||||
);
|
|
||||||
setProjectFlock(selected);
|
|
||||||
} else {
|
|
||||||
setProjectFlock(undefined);
|
|
||||||
}
|
|
||||||
if (projectFlockId) {
|
|
||||||
router.push('/production/project-flock/chickin/add');
|
|
||||||
}
|
|
||||||
if (!val && projectFlockId) {
|
|
||||||
router.push('/production/project-flock/chickin/add');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectFlockId && isResponseSuccess(listProjectFlock)) {
|
|
||||||
setProjectFlock(
|
|
||||||
listProjectFlock.data.find((pf) => pf.id === Number(projectFlockId))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [projectFlockId, listProjectFlock]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Header */}
|
|
||||||
<div className='flex flex-row justify-between items-center px-4 py-4'>
|
|
||||||
<div className='flex flex-row items-center h-full gap-2'>
|
|
||||||
<Link
|
|
||||||
href={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
|
|
||||||
className='hover:text-gray-400'
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:arrow-left' width={24} height={24} />
|
|
||||||
</Link>
|
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
|
||||||
<div className='text-sm text-neutral'>
|
|
||||||
Chick In {projectFlock?.flock_name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <FormHeader
|
|
||||||
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
|
|
||||||
backUrl={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
|
|
||||||
/> */}
|
|
||||||
{/* <div className='flex flex-col gap-4 w-full my-4'>
|
|
||||||
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
|
|
||||||
<SelectInput
|
|
||||||
required
|
|
||||||
label='Ganti Project Flock'
|
|
||||||
placeholder='Pilih Project Flock'
|
|
||||||
options={options}
|
|
||||||
onInputChange={(val) => {
|
|
||||||
setSearchProjectFlock(val);
|
|
||||||
}}
|
|
||||||
isLoading={isLoadingListProjectFlock}
|
|
||||||
value={
|
|
||||||
projectFlock
|
|
||||||
? {
|
|
||||||
label: `${projectFlock?.flock_name}`,
|
|
||||||
value: projectFlock?.id,
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onChange={(val) => {
|
|
||||||
handleChangeProjectFlock(val as OptionType | null);
|
|
||||||
}}
|
|
||||||
isSearchable
|
|
||||||
isClearable
|
|
||||||
startAdornment={
|
|
||||||
projectFlock && (
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
color='success'
|
|
||||||
size='sm'
|
|
||||||
className={{
|
|
||||||
badge: 'whitespace-nowrap font-semibold',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Periode {projectFlock?.period}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
{/* Informasi Umum */}
|
|
||||||
{projectFlock && (
|
|
||||||
<div className='border-t-1 border-gray-300'>
|
|
||||||
<div className='p-4 flex flex-col gap-4'>
|
|
||||||
<h2 className='text-2xl font-semibold'>Informasi Umum</h2>
|
|
||||||
{/* Badge Row */}
|
|
||||||
<div className='flex flex-row gap-2'>
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
color={
|
|
||||||
projectFlock.approval.step_number == 1
|
|
||||||
? 'neutral'
|
|
||||||
: projectFlock.approval.step_number == 2
|
|
||||||
? 'success'
|
|
||||||
: projectFlock.approval.step_number >= 3
|
|
||||||
? 'error'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
badge: 'rounded-lg px-2',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='mdi:circle'
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
color={
|
|
||||||
projectFlock.approval.step_number == 1
|
|
||||||
? 'neutral'
|
|
||||||
: projectFlock.approval.step_number == 2
|
|
||||||
? 'success'
|
|
||||||
: projectFlock.approval.step_number >= 3
|
|
||||||
? 'error'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>{' '}
|
|
||||||
{projectFlock.approval.step_name}
|
|
||||||
</Badge>
|
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
|
||||||
<Badge
|
|
||||||
color='neutral'
|
|
||||||
variant='soft'
|
|
||||||
className={{ badge: 'rounded-lg px-2' }}
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:bookmark' width={12} height={12} />
|
|
||||||
{` ${formatTitleCase(projectFlock.category)}`}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{/* Information Grid */}
|
|
||||||
<div className='grid grid-cols-3 gap-4'>
|
|
||||||
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
|
||||||
<Icon width={14} height={14} icon='mdi:account' /> Submitted
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
color='neutral'
|
|
||||||
className={{
|
|
||||||
badge: 'rounded-lg px-2',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:account-circle' width={14} height={14} />{' '}
|
|
||||||
{projectFlock.created_user.name}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
|
||||||
<Icon width={14} height={14} icon={'mdi:clock'} /> History
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>
|
|
||||||
<Button variant='outline' className='py-1 text-sm'>
|
|
||||||
See History{' '}
|
|
||||||
<Icon
|
|
||||||
icon='mdi:arrow-top-right-thin'
|
|
||||||
width={11}
|
|
||||||
height={11}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* BARIS 1 */}
|
|
||||||
<div
|
|
||||||
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
|
|
||||||
relative
|
|
||||||
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
|
|
||||||
>
|
|
||||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>{projectFlock.area.name}</div>
|
|
||||||
|
|
||||||
{/* BARIS 2 */}
|
|
||||||
<div
|
|
||||||
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
|
|
||||||
relative
|
|
||||||
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
|
|
||||||
>
|
|
||||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>{projectFlock.location.name}</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
|
|
||||||
relative
|
|
||||||
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
|
|
||||||
>
|
|
||||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>{projectFlock.fcr.name}</div>
|
|
||||||
|
|
||||||
{/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
|
|
||||||
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
|
|
||||||
<Icon width={14} height={14} icon='mdi:circle-slice-8' />{' '}
|
|
||||||
Kategori
|
|
||||||
</div>
|
|
||||||
<div className='col-span-2'>
|
|
||||||
{formatTitleCase(projectFlock.category)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* <Card
|
|
||||||
title='Informasi Flock'
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full bg-white mb-3',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<ProjectFlock>
|
|
||||||
emptyContent={
|
|
||||||
<div className='w-full p-5 text-center'>
|
|
||||||
<span className='text-lg opacity-50'>
|
|
||||||
Pilih project flock terlebih dahulu...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
data={projectFlock ? [projectFlock] : []}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: 'ID',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Area',
|
|
||||||
accessorKey: 'area.name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Lokasi',
|
|
||||||
accessorKey: 'location.name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Nama Flock',
|
|
||||||
accessorKey: 'flock_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Kategori',
|
|
||||||
accessorKey: 'category',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.approval?.step_name ? (
|
|
||||||
<PillBadge
|
|
||||||
color={(() => {
|
|
||||||
switch (
|
|
||||||
props.row.original.approval?.step_name.toUpperCase()
|
|
||||||
) {
|
|
||||||
case 'AKTIF':
|
|
||||||
return 'red';
|
|
||||||
case 'PENGAJUAN':
|
|
||||||
return 'green';
|
|
||||||
default:
|
|
||||||
return 'gray';
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
content={props.row.original.approval?.step_name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/\b\w/g, (char) => char.toUpperCase())}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'FCR Layer',
|
|
||||||
accessorKey: 'fcr.name',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
page={undefined}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'mb-20': projectFlock && projectFlock.kandangs?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card> */}
|
|
||||||
{/* Card Kandangs */}
|
|
||||||
<div className='border-t-1 border-gray-300'>
|
|
||||||
<div className='p-4 flex flex-col gap-4'>
|
|
||||||
<h2 className='text-2xl font-semibold'>Daftar Kandang</h2>
|
|
||||||
{isResponseSuccess(listProjectFlock) ? (
|
|
||||||
<>
|
|
||||||
{/* Badge Row */}
|
|
||||||
<div className='flex flex-row gap-2'>
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
color={'success'}
|
|
||||||
className={{
|
|
||||||
badge: 'rounded-lg px-2',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='mdi:circle'
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
color={'success'}
|
|
||||||
/>{' '}
|
|
||||||
Disetujui (
|
|
||||||
{isResponseSuccess(listProjectFlockKandang) &&
|
|
||||||
listProjectFlockKandang.data.filter(
|
|
||||||
(k) => k.approval?.step_number == 1
|
|
||||||
).length}
|
|
||||||
)
|
|
||||||
</Badge>
|
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
color={'neutral'}
|
|
||||||
className={{
|
|
||||||
badge: 'rounded-lg px-2',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='mdi:circle'
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
color={'neutral'}
|
|
||||||
/>{' '}
|
|
||||||
Pengajuan (
|
|
||||||
{isResponseSuccess(listProjectFlockKandang) &&
|
|
||||||
listProjectFlockKandang.data.filter(
|
|
||||||
(k) => k.approval?.step_number == 2
|
|
||||||
).length}
|
|
||||||
)
|
|
||||||
</Badge>
|
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
|
||||||
<Badge
|
|
||||||
color='error'
|
|
||||||
variant='soft'
|
|
||||||
className={{ badge: 'rounded-lg px-2' }}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={`mdi:circle`}
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
color='error'
|
|
||||||
/>
|
|
||||||
Belum Chickin (
|
|
||||||
{isResponseSuccess(listProjectFlockKandang) &&
|
|
||||||
listProjectFlockKandang.data.filter(
|
|
||||||
(k) => k.approval == null
|
|
||||||
).length}
|
|
||||||
)
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
{/* Card Kandang */}
|
|
||||||
<Card
|
|
||||||
variant='bordered'
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full',
|
|
||||||
body: 'p-3',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex flex-col gap-6'>
|
|
||||||
{isResponseSuccess(listProjectFlockKandang) &&
|
|
||||||
listProjectFlockKandang.data.map((kandang) => (
|
|
||||||
<div
|
|
||||||
key={kandang.id}
|
|
||||||
className='flex flex-row justify-between items-center'
|
|
||||||
>
|
|
||||||
<div className='flex flex-row gap-2 items-center cursor-pointer text-gray-400'>
|
|
||||||
<Badge
|
|
||||||
variant='soft'
|
|
||||||
color={
|
|
||||||
kandang.approval
|
|
||||||
? kandang.approval.step_number == 1
|
|
||||||
? 'success'
|
|
||||||
: 'neutral'
|
|
||||||
: 'error'
|
|
||||||
}
|
|
||||||
className={{
|
|
||||||
badge: 'rounded-lg px-2',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='mdi:circle'
|
|
||||||
width={12}
|
|
||||||
height={12}
|
|
||||||
color={
|
|
||||||
kandang.approval
|
|
||||||
? kandang.approval.step_number == 1
|
|
||||||
? 'success'
|
|
||||||
: 'neutral'
|
|
||||||
: 'gray'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Badge>
|
|
||||||
<span className='font-semibold'>
|
|
||||||
{kandang.kandang.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<RequirePermission permissions='lti.production.chickins.create'>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='py-1 text-sm'
|
|
||||||
onClick={() => {
|
|
||||||
handleChickinClick(kandang);
|
|
||||||
}}
|
|
||||||
disabled={projectFlock?.approval?.step_number === 1}
|
|
||||||
>
|
|
||||||
Chick In{' '}
|
|
||||||
<Icon
|
|
||||||
icon='mdi:arrow-top-right-thin'
|
|
||||||
width={11}
|
|
||||||
height={11}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className='w-full p-5 text-center'>
|
|
||||||
<span className='text-lg opacity-50'>
|
|
||||||
Pilih project flock terlebih dahulu...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <Card
|
|
||||||
title='Daftar Kandang'
|
|
||||||
className={{
|
|
||||||
wrapper: 'w-full bg-white',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Table<ProjectFlockKandang>
|
|
||||||
emptyContent={
|
|
||||||
<div className='w-full p-5 text-center'>
|
|
||||||
<span className='text-lg opacity-50'>
|
|
||||||
Pilih project flock terlebih dahulu...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
data={
|
|
||||||
projectFlock && isResponseSuccess(listProjectFlockKandang)
|
|
||||||
? listProjectFlockKandang.data
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: '#',
|
|
||||||
cell: (props) =>
|
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row?.project_flock?.area?.name,
|
|
||||||
header: 'Area',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row?.project_flock?.location?.name,
|
|
||||||
header: 'Lokasi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'kandang.name',
|
|
||||||
header: 'Kandang',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'kandang.capacity',
|
|
||||||
header: 'Kapasitas',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: () => projectFlock?.period,
|
|
||||||
header: 'Periode',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'approval.step_name',
|
|
||||||
header: 'Status',
|
|
||||||
cell: (props) => {
|
|
||||||
return props.row.original.approval?.step_name ? (
|
|
||||||
<PillBadge
|
|
||||||
color={(() => {
|
|
||||||
switch (
|
|
||||||
props.row.original.approval?.step_name.toUpperCase()
|
|
||||||
) {
|
|
||||||
case 'DISETUJUI':
|
|
||||||
return 'green';
|
|
||||||
case 'PENGAJUAN':
|
|
||||||
return 'yellow';
|
|
||||||
default:
|
|
||||||
return 'gray';
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
content={props.row.original.approval?.step_name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/\b\w/g, (char) => char.toUpperCase())}
|
|
||||||
/>
|
|
||||||
) : projectFlock?.approval?.step_number === 1 ? (
|
|
||||||
<PillBadge color='red' content={'Tidak Dapat Chick In'} />
|
|
||||||
) : (
|
|
||||||
<PillBadge color='gray' content={'Belum Chick In'} />
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aksi',
|
|
||||||
cell: (props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
color='success'
|
|
||||||
variant='outline'
|
|
||||||
onClick={() => {
|
|
||||||
handleChickinClick(props.row.original);
|
|
||||||
}}
|
|
||||||
className='p-1'
|
|
||||||
disabled={projectFlock?.approval?.step_number === 1}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='mdi:home-import-outline'
|
|
||||||
width={18}
|
|
||||||
height={18}
|
|
||||||
/>
|
|
||||||
Chickin
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
page={undefined}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'mb-20': projectFlock && projectFlock.kandangs?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card> */}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectFlockChickinDetail;
|
|
||||||
@@ -22,6 +22,7 @@ import toast from 'react-hot-toast';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
import { ApprovalApi } from '@/services/api/approval';
|
import { ApprovalApi } from '@/services/api/approval';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const ProjectFlockClosingForm = ({
|
const ProjectFlockClosingForm = ({
|
||||||
projectFlock,
|
projectFlock,
|
||||||
@@ -285,6 +286,7 @@ const ProjectFlockClosingForm = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='p-4 mt-6'>
|
<div className='p-4 mt-6'>
|
||||||
|
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
|
||||||
<Button
|
<Button
|
||||||
className='w-full'
|
className='w-full'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -295,6 +297,7 @@ const ProjectFlockClosingForm = ({
|
|||||||
<Icon icon='mdi:checkbox-marked-circle-outline' />{' '}
|
<Icon icon='mdi:checkbox-marked-circle-outline' />{' '}
|
||||||
{isCanClose ? 'Close' : 'Unclose'}
|
{isCanClose ? 'Close' : 'Unclose'}
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ const ProjectFlockDetail = ({
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</Card>
|
</Card>
|
||||||
<div className='grid grid-cols-4 gap-3'>
|
<div className='grid grid-cols-4 gap-3'>
|
||||||
<RequirePermission permissions='lti.production.chickins.create'>
|
<RequirePermission permissions='lti.production.chickins.detail'>
|
||||||
<Link
|
<Link
|
||||||
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
|
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
|
||||||
className='m-0 p-0'
|
className='m-0 p-0'
|
||||||
@@ -441,7 +441,7 @@ const ProjectFlockDetail = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
|
<RequirePermission permissions='lti.production.project_flock_kandangs.closing.detail'>
|
||||||
<Link
|
<Link
|
||||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
||||||
className='m-0 p-0'
|
className='m-0 p-0'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import SelectInput from '@/components/input/SelectInput';
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export interface OptionType {
|
export interface OptionType {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10,6 +11,7 @@ interface TableRowSizeSelectorProps {
|
|||||||
onChange: (val: OptionType | OptionType[] | null) => void;
|
onChange: (val: OptionType | OptionType[] | null) => void;
|
||||||
options: OptionType[];
|
options: OptionType[];
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableRowSizeSelector = ({
|
export const TableRowSizeSelector = ({
|
||||||
@@ -17,9 +19,10 @@ export const TableRowSizeSelector = ({
|
|||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
}: TableRowSizeSelectorProps) => {
|
}: TableRowSizeSelectorProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-row gap-3 items-end justify-end'>
|
<div className={cn('flex flex-row gap-3 items-end justify-end', className)}>
|
||||||
{children}
|
{children}
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Baris'
|
label='Baris'
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/marketing',
|
link: '/marketing',
|
||||||
icon: 'heroicons-outline:currency-dollar',
|
icon: 'heroicons-outline:currency-dollar',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Keuangan',
|
||||||
|
link: '/finance',
|
||||||
|
icon: 'heroicons-outline:banknotes',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Biaya',
|
text: 'Biaya',
|
||||||
link: '/expense',
|
link: '/expense',
|
||||||
@@ -185,6 +190,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
link: '/master-data/flock',
|
link: '/master-data/flock',
|
||||||
permission: ['lti.master.flocks.list'],
|
permission: ['lti.master.flocks.list'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Standar Produksi',
|
||||||
|
link: '/master-data/production-standard',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
@@ -278,6 +287,30 @@ export const RECORDING_FLAG_OPTIONS = [
|
|||||||
{ label: 'Ayam Mati', value: 'Ayam Mati' },
|
{ label: 'Ayam Mati', value: 'Ayam Mati' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const FINANCE_PARTY_TYPE_OPTIONS = [
|
||||||
|
{ label: 'Customer', value: 'CUSTOMER' },
|
||||||
|
{ label: 'Supplier', value: 'SUPPLIER' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FINANCE_PAYMENT_METHOD_OPTIONS = [
|
||||||
|
{ label: 'Transfer', value: 'TRANSFER' },
|
||||||
|
{ label: 'Cash', value: 'CASH' },
|
||||||
|
{ label: 'Card', value: 'CARD' },
|
||||||
|
{ label: 'Cheque', value: 'CHEQUE' },
|
||||||
|
{ label: 'Saldo', value: 'SALDO' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FINANCE_INITIAL_BALANCE_TYPE_OPTIONS = [
|
||||||
|
{ label: 'Saldo Awal Positif', value: 'POSITIVE' },
|
||||||
|
{ label: 'Saldo Awal Negatif', value: 'NEGATIVE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FINANCE_TRANSACTION_STATUS = ['PENJUALAN', 'BIAYA'];
|
||||||
|
|
||||||
|
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
|
||||||
|
|
||||||
|
export const FINANCE_INJECTION_STATUS = ['INJECTION'];
|
||||||
|
|
||||||
export const APPROVAL_WORKFLOWS = [
|
export const APPROVAL_WORKFLOWS = [
|
||||||
{
|
{
|
||||||
key: 'PROJECT_FLOCKS',
|
key: 'PROJECT_FLOCKS',
|
||||||
|
|||||||
@@ -7,16 +7,23 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
// Production
|
// Production
|
||||||
// Production - Project Flock
|
// Production - Project Flock
|
||||||
'/production/project-flock/': ['lti.production.project_flocks.list'],
|
'/production/project-flock/': ['lti.production.project_flocks.list'],
|
||||||
'/production/project-flock/add/': ['lti.production.project_flocks.create'],
|
'/production/project-flock/add/': [
|
||||||
|
'lti.production.project_flocks.create',
|
||||||
|
'lti.production.project_flocks.delete',
|
||||||
|
],
|
||||||
'/production/project-flock/detail/': ['lti.production.project_flocks.detail'],
|
'/production/project-flock/detail/': ['lti.production.project_flocks.detail'],
|
||||||
'/production/project-flock/detail/edit/': [
|
'/production/project-flock/detail/edit/': [
|
||||||
'lti.production.project_flocks.update',
|
'lti.production.project_flocks.update',
|
||||||
|
'lti.production.project_flocks.delete',
|
||||||
],
|
],
|
||||||
'/production/project-flock/chickin/add/kandang/': [
|
'/production/project-flock/chickin/add/kandang/': [
|
||||||
'lti.production.chickins.create',
|
'lti.production.chickins.create',
|
||||||
|
'lti.production.chickins.detail',
|
||||||
|
'lti.production.chickins.approve',
|
||||||
],
|
],
|
||||||
'/production/project-flock/closing/': [
|
'/production/project-flock/closing/': [
|
||||||
'lti.production.project_flock_kandangs.closing',
|
'lti.production.project_flock_kandangs.closing',
|
||||||
|
'lti.production.project_flock_kandangs.closing.detail',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Production - Recording
|
// Production - Recording
|
||||||
@@ -61,6 +68,28 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/expense/realization/': ['lti.expense.create.realization'],
|
'/expense/realization/': ['lti.expense.create.realization'],
|
||||||
'/expense/realization/edit/': ['lti.expense.update.realization'],
|
'/expense/realization/edit/': ['lti.expense.update.realization'],
|
||||||
|
|
||||||
|
// Finance
|
||||||
|
// // ===== FINANCE =====
|
||||||
|
// "lti.finance.transaction.list",
|
||||||
|
// "lti.finance.transaction.detail",
|
||||||
|
// "lti.finance.transaction.delete",
|
||||||
|
// "lti.finance.payments.create",
|
||||||
|
// "lti.finance.payments.update",
|
||||||
|
// "lti.finance.initial_balances.create",
|
||||||
|
// "lti.finance.initial_balances.update",
|
||||||
|
// "lti.finance.injections.create",
|
||||||
|
// "lti.finance.injections.update",
|
||||||
|
'/finance/': ['lti.finance.transaction.list'],
|
||||||
|
'/finance/detail/': ['lti.finance.transaction.detail'],
|
||||||
|
'/finance/add/': ['lti.finance.payments.create'],
|
||||||
|
'/finance/detail/edit/': ['lti.finance.payments.update'],
|
||||||
|
'/finance/add/initial-balance/': ['lti.finance.initial_balances.create'],
|
||||||
|
'/finance/detail/edit/initial-balance/': [
|
||||||
|
'lti.finance.initial_balances.update',
|
||||||
|
],
|
||||||
|
'/finance/add/injection/': ['lti.finance.injections.create'],
|
||||||
|
'/finance/detail/edit/injection/': ['lti.finance.injections.update'],
|
||||||
|
|
||||||
// Closing
|
// Closing
|
||||||
'/closing/': ['lti.closing.list'],
|
'/closing/': ['lti.closing.list'],
|
||||||
'/closing/detail/': ['lti.closing.detail'],
|
'/closing/detail/': ['lti.closing.detail'],
|
||||||
@@ -152,4 +181,15 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/master-data/flock/add/': ['lti.master.flocks.create'],
|
'/master-data/flock/add/': ['lti.master.flocks.create'],
|
||||||
'/master-data/flock/detail/': ['lti.master.flocks.detail'],
|
'/master-data/flock/detail/': ['lti.master.flocks.detail'],
|
||||||
'/master-data/flock/detail/edit/': ['lti.master.flocks.update'],
|
'/master-data/flock/detail/edit/': ['lti.master.flocks.update'],
|
||||||
|
|
||||||
|
'/master-data/production-standard/': ['lti.master.production_standards.list'],
|
||||||
|
'/master-data/production-standard/add/': [
|
||||||
|
'lti.master.production_standards.create',
|
||||||
|
],
|
||||||
|
'/master-data/production-standard/detail/': [
|
||||||
|
'lti.master.production_standards.detail',
|
||||||
|
],
|
||||||
|
'/master-data/production-standard/detail/edit/': [
|
||||||
|
'lti.master.production_standards.update',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,139 +0,0 @@
|
|||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
|
||||||
import { DailyMarketingReport } from '@/types/api/report/marketing';
|
|
||||||
|
|
||||||
// TODO: delete this later
|
|
||||||
export const DAILY_MARKETING_DUMMY_DATA: BaseApiResponse<DailyMarketingReport> =
|
|
||||||
{
|
|
||||||
code: 200,
|
|
||||||
status: 'success',
|
|
||||||
message: 'Get daily marketing report successfully',
|
|
||||||
meta: {
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
total_pages: 1,
|
|
||||||
total_results: 2,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
rows: [
|
|
||||||
{
|
|
||||||
// metadata
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 101,
|
|
||||||
email: 'admin@example.com',
|
|
||||||
name: 'Admin User',
|
|
||||||
},
|
|
||||||
created_at: '2025-12-01T08:00:00Z',
|
|
||||||
updated_at: '2025-12-01T08:00:00Z',
|
|
||||||
|
|
||||||
// row data
|
|
||||||
no: 1,
|
|
||||||
so_date: '2025-12-01',
|
|
||||||
do_date: '2025-12-08',
|
|
||||||
aging_days: 7,
|
|
||||||
|
|
||||||
warehouse: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Warehouse Kandang A',
|
|
||||||
type: 'KANDANG',
|
|
||||||
area: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Area Barat',
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Farm Bandung',
|
|
||||||
address: 'Jl. Raya Farm No. 1',
|
|
||||||
area: null,
|
|
||||||
},
|
|
||||||
kandang: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Kandang A1',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
capacity: 5000,
|
|
||||||
location: null,
|
|
||||||
pic: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
customer: {
|
|
||||||
id: 1,
|
|
||||||
name: 'PT Maju Jaya',
|
|
||||||
pic_id: 10,
|
|
||||||
pic: {
|
|
||||||
id: 10,
|
|
||||||
id_user: 210,
|
|
||||||
email: 'pic@majujaya.com',
|
|
||||||
name: 'Budi Santoso',
|
|
||||||
},
|
|
||||||
type: 'BROILER',
|
|
||||||
address: 'Jl. Industri No. 10',
|
|
||||||
phone: '08123456789',
|
|
||||||
email: 'contact@majujaya.com',
|
|
||||||
account_number: '1234567890',
|
|
||||||
},
|
|
||||||
|
|
||||||
sales: 'Andi Wijaya',
|
|
||||||
|
|
||||||
product: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Live Chicken',
|
|
||||||
brand: 'LTI Farm',
|
|
||||||
sku: 'LC-001',
|
|
||||||
product_price: 18_000,
|
|
||||||
selling_price: 20_000,
|
|
||||||
tax: 0,
|
|
||||||
expiry_period: 0,
|
|
||||||
uom: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Kg',
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 101,
|
|
||||||
email: 'admin@example.com',
|
|
||||||
name: 'Admin User',
|
|
||||||
},
|
|
||||||
created_at: '2025-01-01T00:00:00Z',
|
|
||||||
updated_at: '2025-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
product_category: {
|
|
||||||
id: 1,
|
|
||||||
code: 'BROILER',
|
|
||||||
name: 'Broiler Chicken',
|
|
||||||
created_user: {
|
|
||||||
id: 1,
|
|
||||||
id_user: 101,
|
|
||||||
email: 'admin@example.com',
|
|
||||||
name: 'Admin User',
|
|
||||||
},
|
|
||||||
created_at: '2025-01-01T00:00:00Z',
|
|
||||||
updated_at: '2025-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
suppliers: [],
|
|
||||||
flags: ['LIVE'],
|
|
||||||
},
|
|
||||||
|
|
||||||
do_number: 'DO-2025-0001',
|
|
||||||
vehicle_number: 'B 1234 CD',
|
|
||||||
marketing_type: 'REGULAR',
|
|
||||||
|
|
||||||
qty: 1000,
|
|
||||||
average_weight_kg: 1.8,
|
|
||||||
total_weight_kg: 1800,
|
|
||||||
|
|
||||||
sales_price_per_kg: 20_000,
|
|
||||||
hpp_price_per_kg: 18_000,
|
|
||||||
|
|
||||||
sales_amount: 36_000_000,
|
|
||||||
hpp_amount: 32_400_000,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
summary: {
|
|
||||||
total_qty: 1000,
|
|
||||||
total_weight_kg: 1800,
|
|
||||||
total_sales_amount: 36_000_000,
|
|
||||||
total_hpp_amount: 32_400_000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -15,20 +15,6 @@ import {
|
|||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
import { ClosingSales } from '@/types/api/closing';
|
import { ClosingSales } from '@/types/api/closing';
|
||||||
|
|
||||||
// TODO: delete these dummy data later
|
|
||||||
import {
|
|
||||||
dummyGetAllFetcher,
|
|
||||||
dummyGetSingle,
|
|
||||||
dummyGetAllIncomingSapronakFetcher,
|
|
||||||
dummyGetAllOutgoingSapronakFetcher,
|
|
||||||
dummyGetGeneralInfo,
|
|
||||||
dummyGetPerhitunganSapronak,
|
|
||||||
dummyGetOverhead,
|
|
||||||
dummyClosingProductionData,
|
|
||||||
} from '@/dummy/closing.dummy';
|
|
||||||
import { sleep } from '@/lib/helper';
|
|
||||||
|
|
||||||
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||||
constructor(basePath: string) {
|
constructor(basePath: string) {
|
||||||
super(basePath);
|
super(basePath);
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
|
import {
|
||||||
|
CreateFinancePayment,
|
||||||
|
CreateInitialBalance,
|
||||||
|
CreateInjection,
|
||||||
|
Finance,
|
||||||
|
UpdateFinancePayment,
|
||||||
|
UpdateInitialBalance,
|
||||||
|
UpdateInjection,
|
||||||
|
} from '@/types/api/finance/finance';
|
||||||
|
|
||||||
|
export class FinanceApiService extends BaseApiService<
|
||||||
|
Finance,
|
||||||
|
unknown,
|
||||||
|
unknown
|
||||||
|
> {
|
||||||
|
constructor(basePath: string) {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSingle(id: number): Promise<BaseApiResponse<Finance>> {
|
||||||
|
return await httpClientFetcher<BaseApiResponse<Finance>>(
|
||||||
|
`${this.basePath}/transactions/${id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(payload: CreateFinancePayment) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const createRes = await httpClient<BaseApiResponse<Finance>>(
|
||||||
|
`${this.basePath}/payments`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return createRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Finance>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createInitialBalances(payload: CreateInitialBalance) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const createRes = await httpClient<BaseApiResponse<Finance>>(
|
||||||
|
`${this.basePath}/initial-balances`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return createRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Finance>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createInjections(payload: CreateInjection) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const createRes = await httpClient<BaseApiResponse<Finance>>(
|
||||||
|
`${this.basePath}/injections`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return createRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Finance>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, payload: UpdateFinancePayment) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const updatePath = `${this.basePath}/payments/${id}`;
|
||||||
|
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const updateRes = await httpClient<BaseApiResponse<Finance>>(updatePath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
return updateRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Finance>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateInitialBalances(id: number, payload: UpdateInitialBalance) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const updatePath = `${this.basePath}/initial-balances/${id}`;
|
||||||
|
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const updateRes = await httpClient<BaseApiResponse<Finance>>(updatePath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
return updateRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Finance>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateInjections(id: number, payload: UpdateInjection) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const updatePath = `${this.basePath}/injections/${id}`;
|
||||||
|
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const updateRes = await httpClient<BaseApiResponse<Finance>>(updatePath, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
return updateRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Finance>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number) {
|
||||||
|
try {
|
||||||
|
const deletePath = `${this.basePath}/transactions/${id}`;
|
||||||
|
const deleteRes = await httpClient<BaseApiResponse>(deletePath, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
return deleteRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FinanceApi = new FinanceApiService('/finance');
|
||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
Flock,
|
Flock,
|
||||||
UpdateFlockPayload,
|
UpdateFlockPayload,
|
||||||
} from '@/types/api/master-data/flock';
|
} from '@/types/api/master-data/flock';
|
||||||
|
import { ProductionStandard } from '@/types/api/master-data/production-standard';
|
||||||
|
|
||||||
export const UomApi = new BaseApiService<
|
export const UomApi = new BaseApiService<
|
||||||
Uom,
|
Uom,
|
||||||
@@ -141,3 +142,9 @@ export const FlockApi = new BaseApiService<
|
|||||||
CreateFlockPayload,
|
CreateFlockPayload,
|
||||||
UpdateFlockPayload
|
UpdateFlockPayload
|
||||||
>('/master-data/flocks');
|
>('/master-data/flocks');
|
||||||
|
|
||||||
|
export const ProductionStandardApi = new BaseApiService<
|
||||||
|
ProductionStandard,
|
||||||
|
unknown,
|
||||||
|
unknown
|
||||||
|
>('/master-data/production-standards');
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { FormStore } from '@/types/stores';
|
||||||
|
import { createProductionStandardFormSlice } from '@/stores/form/slices/production-standard-form.slice';
|
||||||
|
|
||||||
|
export const useFormStore = create<FormStore>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(...args) => ({
|
||||||
|
...createProductionStandardFormSlice(...args),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'production-standard-form-cache',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: 'FormStore',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { StateCreator } from 'zustand';
|
||||||
|
import { FormStore } from '@/types/stores';
|
||||||
|
|
||||||
|
export const createProductionStandardFormSlice: StateCreator<
|
||||||
|
FormStore,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
FormStore
|
||||||
|
> = (set): FormStore => ({
|
||||||
|
formData: null,
|
||||||
|
editMode: false,
|
||||||
|
editIndex: null,
|
||||||
|
|
||||||
|
setFormData: (data) =>
|
||||||
|
set(() => ({
|
||||||
|
formData: data,
|
||||||
|
})),
|
||||||
|
|
||||||
|
setEditMode: (mode, index) =>
|
||||||
|
set(() => ({
|
||||||
|
editMode: mode,
|
||||||
|
editIndex: index,
|
||||||
|
})),
|
||||||
|
|
||||||
|
addDetail: (detail) =>
|
||||||
|
set((state) => ({
|
||||||
|
formData: state.formData
|
||||||
|
? {
|
||||||
|
...state.formData,
|
||||||
|
details: [...state.formData.details, detail],
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
})),
|
||||||
|
|
||||||
|
updateDetail: (index, detail) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.formData) return state;
|
||||||
|
const newDetails = [...state.formData.details];
|
||||||
|
newDetails[index] = detail;
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
...state.formData,
|
||||||
|
details: newDetails,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteDetail: (index) =>
|
||||||
|
set((state) => {
|
||||||
|
if (!state.formData) return state;
|
||||||
|
const newDetails = [...state.formData.details];
|
||||||
|
newDetails.splice(index, 1);
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
...state.formData,
|
||||||
|
details: newDetails,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearCache: () =>
|
||||||
|
set(() => ({
|
||||||
|
formData: null,
|
||||||
|
editMode: false,
|
||||||
|
editIndex: null,
|
||||||
|
})),
|
||||||
|
});
|
||||||
Vendored
+60
@@ -0,0 +1,60 @@
|
|||||||
|
export type Finance = {
|
||||||
|
id: number;
|
||||||
|
payment_code: string;
|
||||||
|
reference_number: string;
|
||||||
|
transaction_type: string;
|
||||||
|
party: FinanceParty;
|
||||||
|
payment_date: string;
|
||||||
|
created_at: string;
|
||||||
|
payment_method: string;
|
||||||
|
bank: FinanceBank;
|
||||||
|
expense_amount: number;
|
||||||
|
income_amount: number;
|
||||||
|
nominal: number;
|
||||||
|
notes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FinanceParty = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
account_number: string;
|
||||||
|
};
|
||||||
|
export type FinanceBank = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
alias: string;
|
||||||
|
owner: string;
|
||||||
|
account_number: string;
|
||||||
|
};
|
||||||
|
export type CreateFinancePayment = {
|
||||||
|
party_id: number;
|
||||||
|
party_type: string;
|
||||||
|
payment_date: string;
|
||||||
|
payment_method: string;
|
||||||
|
bank_id: number;
|
||||||
|
reference_number: string;
|
||||||
|
nominal: number;
|
||||||
|
notes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateFinancePayment = CreateFinancePayment;
|
||||||
|
export type CreateInitialBalance = {
|
||||||
|
party_type: string;
|
||||||
|
party_id: number;
|
||||||
|
bank_id: number;
|
||||||
|
reference_number: string;
|
||||||
|
initial_balance_type: string;
|
||||||
|
nominal: number;
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateInitialBalance = CreateInitialBalance;
|
||||||
|
export type CreateInjection = {
|
||||||
|
bank_id: number;
|
||||||
|
adjustment_date: string;
|
||||||
|
nominal: number;
|
||||||
|
notes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateInjection = CreateInjection;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { CreatedUser } from '@/types/api/api-general';
|
||||||
|
|
||||||
|
export interface ProductionStandard {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
project_category: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_user: CreatedUser;
|
||||||
|
details: StandardDetails[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StandardDetails {
|
||||||
|
week: number;
|
||||||
|
growth_standard_detail: StandardGrowthDetails;
|
||||||
|
egg_production_standard_detail: ProductionStandardDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductionStandardDetails {
|
||||||
|
target_hen_day_production: number;
|
||||||
|
target_hen_house_production: number;
|
||||||
|
target_egg_weight: number;
|
||||||
|
target_egg_mass: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StandardGrowthDetails {
|
||||||
|
target_mean_bw: number;
|
||||||
|
max_depletion: number;
|
||||||
|
min_uniformity: number;
|
||||||
|
feed_intake: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProductionStandardPayload {
|
||||||
|
name: string;
|
||||||
|
project_category: string;
|
||||||
|
details: {
|
||||||
|
week: number;
|
||||||
|
growth_standard_detail: {
|
||||||
|
target_mean_bw: number;
|
||||||
|
max_depletion: number;
|
||||||
|
min_uniformity: number;
|
||||||
|
feed_intake: number;
|
||||||
|
};
|
||||||
|
egg_production_standard_detail: {
|
||||||
|
target_hen_day_production: number;
|
||||||
|
target_hen_house_production: number;
|
||||||
|
target_egg_weight: number;
|
||||||
|
target_egg_mass: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProductionStandardPayload {
|
||||||
|
name: string;
|
||||||
|
project_category: string;
|
||||||
|
details: {
|
||||||
|
week: number;
|
||||||
|
growth_standard_detail: {
|
||||||
|
target_mean_bw: number;
|
||||||
|
max_depletion: number;
|
||||||
|
min_uniformity: number;
|
||||||
|
feed_intake: number;
|
||||||
|
};
|
||||||
|
egg_production_standard_detail: {
|
||||||
|
target_hen_day_production: number;
|
||||||
|
target_hen_house_production: number;
|
||||||
|
target_egg_weight: number;
|
||||||
|
target_egg_mass: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
Vendored
+27
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ProductionStandardRepeaterFormSchemaValues } from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema';
|
||||||
|
|
||||||
type MainUiSlice = {
|
type MainUiSlice = {
|
||||||
mainDrawerOpen: boolean;
|
mainDrawerOpen: boolean;
|
||||||
setMainDrawerOpen: (open: boolean) => void;
|
setMainDrawerOpen: (open: boolean) => void;
|
||||||
@@ -13,3 +15,28 @@ type DrawerUISlice = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UIStore = MainUiSlice & DrawerUISlice;
|
export type UIStore = MainUiSlice & DrawerUISlice;
|
||||||
|
|
||||||
|
type ProductionStandardFormSlice = {
|
||||||
|
formData: {
|
||||||
|
name: string;
|
||||||
|
project_category: string;
|
||||||
|
details: ProductionStandardRepeaterFormSchemaValues[];
|
||||||
|
} | null;
|
||||||
|
editMode: boolean;
|
||||||
|
editIndex: number | null;
|
||||||
|
setFormData: (data: {
|
||||||
|
name: string;
|
||||||
|
project_category: string;
|
||||||
|
details: ProductionStandardRepeaterFormSchemaValues[];
|
||||||
|
}) => void;
|
||||||
|
setEditMode: (mode: boolean, index: number | null) => void;
|
||||||
|
addDetail: (detail: ProductionStandardRepeaterFormSchemaValues) => void;
|
||||||
|
updateDetail: (
|
||||||
|
index: number,
|
||||||
|
detail: ProductionStandardRepeaterFormSchemaValues
|
||||||
|
) => void;
|
||||||
|
deleteDetail: (index: number) => void;
|
||||||
|
clearCache: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormStore = ProductionStandardFormSlice;
|
||||||
|
|||||||
Reference in New Issue
Block a user