mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +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 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(
|
||||
baseClassNames.drawerSidebarContent,
|
||||
'w-full min-w-120 sm:w-fit'
|
||||
'w-full sm:min-w-120 sm:w-fit'
|
||||
),
|
||||
};
|
||||
} else if (variant === 'left') {
|
||||
@@ -76,7 +76,7 @@ const Drawer = ({
|
||||
),
|
||||
drawerSidebarContent: cn(
|
||||
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',
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// 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',
|
||||
header: 'Kuantitas',
|
||||
@@ -187,14 +175,6 @@ const InventoryAdjustmentTable = () => {
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{/* <DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Stock Adjustment'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-end'>
|
||||
|
||||
@@ -330,6 +330,7 @@ const MarketingTable = () => {
|
||||
value={pageSize}
|
||||
onChange={pageSizeChangeHandler}
|
||||
options={ROWS_OPTIONS}
|
||||
className='flex sm:flex-row flex-col gap-3 items-end justify-end'
|
||||
>
|
||||
{/* select multiple product */}
|
||||
<SelectInput
|
||||
|
||||
@@ -576,7 +576,7 @@ const MarketingForm = ({
|
||||
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
|
||||
label='Pelanggan'
|
||||
options={customerOptions}
|
||||
@@ -651,7 +651,7 @@ const MarketingForm = ({
|
||||
)}
|
||||
|
||||
{/* Input Notes */}
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div className='grid sm:grid-cols-2 gap-3'>
|
||||
<DebouncedTextArea
|
||||
required
|
||||
name='notes'
|
||||
|
||||
@@ -193,7 +193,7 @@ const DeliveryOrderProductForm = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='grid sm:grid-cols-2 gap-4'>
|
||||
<SelectInput
|
||||
options={options}
|
||||
label='Produk'
|
||||
|
||||
@@ -183,7 +183,7 @@ const SalesOrderProductForm = ({
|
||||
{/* <small className='block text-rose-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small> */}
|
||||
<div className='grid grid-cols-2 gap-4 z-200'>
|
||||
<div className='grid sm:grid-cols-2 gap-4 z-200'>
|
||||
<PatternInput
|
||||
name='vehicle_number'
|
||||
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 Badge from '@/components/Badge';
|
||||
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
const ChickinFormKandang = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
@@ -144,6 +145,7 @@ const ChickinFormKandang = ({
|
||||
<h2 className='text-xl font-semibold'>Informasi Chick In</h2>
|
||||
{/* Badge Row */}
|
||||
<div className='flex flex-row gap-2'>
|
||||
<RequirePermission permissions='lti.production.chickins.create'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color={'success'}
|
||||
@@ -151,10 +153,16 @@ const ChickinFormKandang = ({
|
||||
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})
|
||||
</Badge>
|
||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||
</RequirePermission>
|
||||
<Badge
|
||||
color='neutral'
|
||||
variant='soft'
|
||||
@@ -176,11 +184,13 @@ const ChickinFormKandang = ({
|
||||
afterSubmit={afterSubmitFormChickin}
|
||||
/>
|
||||
)}
|
||||
<RequirePermission permissions='lti.production.chickins.create'>
|
||||
<ChickinFormView
|
||||
initialValues={initialValues}
|
||||
formType={formType}
|
||||
afterSubmit={afterSubmitFormChickin}
|
||||
/>
|
||||
</RequirePermission>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Alert from '@/components/Alert';
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import PillBadge from '@/components/PillBadge';
|
||||
@@ -146,6 +147,7 @@ const ChickinLogsView = ({
|
||||
)}
|
||||
|
||||
{initialValues?.approval?.step_number <= 2 && (
|
||||
<RequirePermission permissions='lti.production.chickins.approve'>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={handleClickApprove}
|
||||
@@ -154,6 +156,7 @@ const ChickinLogsView = ({
|
||||
<Icon width={24} height={24} icon='material-symbols:check' />
|
||||
Approve Semua Chick In
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
|
||||
{chickinErrorMessage && (
|
||||
|
||||
@@ -293,7 +293,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
|
||||
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='w-full flex flex-col justify-between items-end gap-2'>
|
||||
<div className='flex flex-col sm:flex-row gap-3 w-full'>
|
||||
@@ -307,32 +307,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
Tambah
|
||||
</Button>
|
||||
</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'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
@@ -551,49 +525,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
cell: (props) =>
|
||||
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}
|
||||
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 { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { ApprovalApi } from '@/services/api/approval';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
const ProjectFlockClosingForm = ({
|
||||
projectFlock,
|
||||
@@ -285,6 +286,7 @@ const ProjectFlockClosingForm = ({
|
||||
</div>
|
||||
|
||||
<div className='p-4 mt-6'>
|
||||
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
|
||||
<Button
|
||||
className='w-full'
|
||||
color='error'
|
||||
@@ -295,6 +297,7 @@ const ProjectFlockClosingForm = ({
|
||||
<Icon icon='mdi:checkbox-marked-circle-outline' />{' '}
|
||||
{isCanClose ? 'Close' : 'Unclose'}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -423,7 +423,7 @@ const ProjectFlockDetail = ({
|
||||
</RadioGroup>
|
||||
</Card>
|
||||
<div className='grid grid-cols-4 gap-3'>
|
||||
<RequirePermission permissions='lti.production.chickins.create'>
|
||||
<RequirePermission permissions='lti.production.chickins.detail'>
|
||||
<Link
|
||||
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
|
||||
className='m-0 p-0'
|
||||
@@ -441,7 +441,7 @@ const ProjectFlockDetail = ({
|
||||
</Button>
|
||||
</Link>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
|
||||
<RequirePermission permissions='lti.production.project_flock_kandangs.closing.detail'>
|
||||
<Link
|
||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
||||
className='m-0 p-0'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SelectInput from '@/components/input/SelectInput';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export interface OptionType {
|
||||
label: string;
|
||||
@@ -10,6 +11,7 @@ interface TableRowSizeSelectorProps {
|
||||
onChange: (val: OptionType | OptionType[] | null) => void;
|
||||
options: OptionType[];
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TableRowSizeSelector = ({
|
||||
@@ -17,9 +19,10 @@ export const TableRowSizeSelector = ({
|
||||
onChange,
|
||||
options,
|
||||
children,
|
||||
className,
|
||||
}: TableRowSizeSelectorProps) => {
|
||||
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}
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
|
||||
@@ -42,6 +42,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
link: '/marketing',
|
||||
icon: 'heroicons-outline:currency-dollar',
|
||||
},
|
||||
{
|
||||
text: 'Keuangan',
|
||||
link: '/finance',
|
||||
icon: 'heroicons-outline:banknotes',
|
||||
},
|
||||
{
|
||||
text: 'Biaya',
|
||||
link: '/expense',
|
||||
@@ -185,6 +190,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
link: '/master-data/flock',
|
||||
permission: ['lti.master.flocks.list'],
|
||||
},
|
||||
{
|
||||
text: 'Standar Produksi',
|
||||
link: '/master-data/production-standard',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
@@ -278,6 +287,30 @@ export const RECORDING_FLAG_OPTIONS = [
|
||||
{ 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 = [
|
||||
{
|
||||
key: 'PROJECT_FLOCKS',
|
||||
|
||||
@@ -7,16 +7,23 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
// Production
|
||||
// Production - Project Flock
|
||||
'/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/edit/': [
|
||||
'lti.production.project_flocks.update',
|
||||
'lti.production.project_flocks.delete',
|
||||
],
|
||||
'/production/project-flock/chickin/add/kandang/': [
|
||||
'lti.production.chickins.create',
|
||||
'lti.production.chickins.detail',
|
||||
'lti.production.chickins.approve',
|
||||
],
|
||||
'/production/project-flock/closing/': [
|
||||
'lti.production.project_flock_kandangs.closing',
|
||||
'lti.production.project_flock_kandangs.closing.detail',
|
||||
],
|
||||
|
||||
// Production - Recording
|
||||
@@ -61,6 +68,28 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/expense/realization/': ['lti.expense.create.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/': ['lti.closing.list'],
|
||||
'/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/detail/': ['lti.master.flocks.detail'],
|
||||
'/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 { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||
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> {
|
||||
constructor(basePath: string) {
|
||||
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,
|
||||
UpdateFlockPayload,
|
||||
} from '@/types/api/master-data/flock';
|
||||
import { ProductionStandard } from '@/types/api/master-data/production-standard';
|
||||
|
||||
export const UomApi = new BaseApiService<
|
||||
Uom,
|
||||
@@ -141,3 +142,9 @@ export const FlockApi = new BaseApiService<
|
||||
CreateFlockPayload,
|
||||
UpdateFlockPayload
|
||||
>('/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 = {
|
||||
mainDrawerOpen: boolean;
|
||||
setMainDrawerOpen: (open: boolean) => void;
|
||||
@@ -13,3 +15,28 @@ type 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