Compare commits

...

24 Commits

Author SHA1 Message Date
Rivaldi A N S 4beb3d4f91 Merge branch 'dev/randy' into 'fix/FE/master-data-and-production'
[FIX/FE][US#33-74] Fix issue in master data and adjusmen in project flock

See merge request mbugroup/lti-web-client!120
2025-12-30 02:47:23 +00:00
randy-ar 2712821f4e fix(FE-337): fix finance input and transaction type 2025-12-30 09:25:54 +07:00
randy-ar 7e73c99074 Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into dev/randy 2025-12-29 16:35:11 +07:00
randy-ar 0f86ba0f2d fix(FE): adjust options in master data product 2025-12-29 15:52:43 +07:00
randy-ar 10a37fde75 fix(FE): fix delete and update button row in master data production standards 2025-12-29 15:01:42 +07:00
randy-ar cd3a5ad441 feat(FE): adding production standard select options for project flock 2025-12-29 13:50:23 +07:00
randy-ar cd42bd6bc0 fix(FE): change hatchery to optional in master data supplier 2025-12-29 13:26:39 +07:00
randy-ar ea7f8a68f4 fix(FE): change select option warehouse marketing to correct data warehouses 2025-12-29 13:19:16 +07:00
Rivaldi A N S e0371b0884 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
2025-12-28 07:15:27 +00:00
Rivaldi A N S 432d837aaf Merge branch 'dev/randy' into 'feat/FE/US-388/master-data-uniformity-standard'
[FEAT/FE][US#388] Master Data Standar Produksi

See merge request mbugroup/lti-web-client!116
2025-12-28 05:23:38 +00:00
randy-ar 77b05c6440 chore(FE): delete dummy data 2025-12-28 04:39:40 +07:00
randy-ar 731bec5a94 feat(FE-337): slicing ui form finance and API integration 2025-12-28 04:28:02 +07:00
randy-ar 6ea25aa3b1 feat(FE-331): implement permission guard in project flock, chickin and closing kandang 2025-12-28 01:00:20 +07:00
randy-ar d4f4505405 feat(FE-331): implement permission guard in project flock, chickin and closing kandang 2025-12-28 00:56:39 +07:00
randy-ar bd653851e2 feat(FE-331): implement permission guard in master data productions standards 2025-12-28 00:23:50 +07:00
randy-ar b9b349aa7a fix(FE): resolve git pull merge development 2025-12-27 16:49:16 +07:00
Rivaldi A N S 6ec6323bbc Merge branch 'feat/FE/US-304/permission-guard' into 'development'
[FEAT/FE][US#304] Permission Guard

See merge request mbugroup/lti-web-client!115
2025-12-27 09:41:13 +00:00
randy-ar c44e63bd2b fix(FE): fix page responsive in project flock dan marketing modules 2025-12-27 16:36:07 +07:00
ValdiANS 500c30c2bc feat(FE-331): implement permission guard in transfer to laying 2025-12-27 16:05:50 +07:00
randy-ar d49bca1d40 feat(FE): api integration production standards 2025-12-27 13:46:19 +07:00
randy-ar 663c1dea14 feat(FE): add master data production standard, slicing form and index table 2025-12-27 03:23:03 +07:00
randy-ar 4ddd1dc8e3 feat(FE-337): slicing ui form add finance 2025-12-24 18:45:33 +07:00
randy-ar 8c95dc8327 feat(FE-350): add filtering table 2025-12-24 16:44:53 +07:00
randy-ar 36ff6d04ee feat(FE-337): init slicing UI and define data types 2025-12-23 17:38:16 +07:00
63 changed files with 4730 additions and 2640 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
npm run format
npm run lint
npm run build
npx tsc --noEmit
+5
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
const FinanceAddInjectionPage = () => {
return <FormFinanceInjection type='add' />;
};
export default FinanceAddInjectionPage;
+7
View File
@@ -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;
+52
View File
@@ -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;
+41
View File
@@ -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;
+14
View File
@@ -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;
+2 -2
View File
@@ -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,410 @@
'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;
}
interface PartyCommonProps {
id: number;
name: string;
account_number: string;
}
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: initialValues?.party
? {
label: initialValues?.party.name || '',
value: initialValues?.party.id || 0,
}
: null,
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,
rawData: partyRawData,
} = useSelect<PartyCommonProps>(
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);
if (isResponseSuccess(partyRawData) && value) {
formik.setFieldValue(
'party_account_number',
partyRawData.data?.find(
(item) => item.id === (value as OptionType)?.value
)?.account_number || ''
);
}
}}
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
readOnly
/>
<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;
@@ -0,0 +1,49 @@
import * as Yup from 'yup';
import { OptionType } from '@/components/input/SelectInput';
// 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'
@@ -174,26 +174,13 @@ const DeliveryOrderProductForm = ({
}}
onReset={handleResetForm}
>
{/* <small className='block text-blue-500'>
{JSON.stringify(exisitingValues)}
</small>
<small className='block text-emerald-500'>
{JSON.stringify(formik.values)}
</small> */}
{/* <small className='block text-red-500'>
{JSON.stringify(formik.errors)}
</small>
<div className='hidden'>
{JSON.stringify(formik.values.marketing_product)}
</div> */}
{formikErrorMessage && (
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
<Alert color='error'>{formikErrorMessage}</Alert>
</div>
)}
<div className='grid grid-cols-2 gap-4'>
<div className='grid sm:grid-cols-2 gap-4'>
<SelectInput
options={options}
label='Produk'
@@ -11,7 +11,7 @@ import SelectInput, {
useSelect,
} from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang';
import { KandangApi } from '@/services/api/master-data';
import { KandangApi, WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput';
@@ -61,7 +61,7 @@ const SalesOrderProductForm = ({
const {
options: kandangSourceOptions,
isLoadingOptions: isLoadingKandangSourceOptions,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
const {
options: warehouseSourceOptions,
@@ -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'
@@ -79,14 +79,14 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
uomId: initialValues?.uom_id ?? 0,
uom: initialValues?.uom
? {
value: initialValues?.uom.id,
label: initialValues?.uom.name,
value: initialValues?.uom?.id,
label: initialValues?.uom?.name,
}
: null,
supplierIds:
initialValues?.suppliers.map((supplier) => supplier.id) ?? [],
initialValues?.suppliers?.map((supplier) => supplier.id) ?? [],
suppliers:
initialValues?.suppliers.map((supplier) => ({
initialValues?.suppliers?.map((supplier) => ({
value: supplier.id,
label: supplier.name,
})) ?? [],
@@ -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;
@@ -0,0 +1,93 @@
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()
.min(1, 'Kategori proyek wajib diisi!')
.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;
@@ -18,7 +18,7 @@ export const SupplierFormSchema = Yup.object({
value: Yup.string().required(),
label: Yup.string().required(),
}).required('Tipe wajib diisi!'),
hatchery: Yup.string().required('Hatchery wajib diisi!'),
hatchery: Yup.string().optional(),
phone: Yup.string()
.matches(/^[0-9]+$/, 'Nomor telepon hanya boleh berisi angka!')
.min(10, 'Nomor telepon minimal 10 digit!')
@@ -142,7 +142,7 @@ const SupplierForm = ({
pic: values.pic,
type: values.type.value,
category: values.category.value,
hatchery: values.hatchery,
hatchery: values.hatchery ?? '',
phone: values.phone,
email: values.email,
address: values.address,
@@ -171,12 +171,12 @@ const SupplierForm = ({
useEffect(() => {
formikSetValues(formikInitialValues);
if (formType != 'add') {
const hatcheryArrays = formikInitialValues.hatchery.split(',');
const hatcheryCreatedOptions = hatcheryArrays.map((item) => ({
const hatcheryArrays = formikInitialValues.hatchery?.split(',');
const hatcheryCreatedOptions = hatcheryArrays?.map((item) => ({
value: item,
label: item,
}));
setHatcheryOptionValues(hatcheryCreatedOptions);
setHatcheryOptionValues(hatcheryCreatedOptions ?? []);
}
}, [formikSetValues, formikInitialValues, setHatcheryOptionValues]);
useEffect(() => {
@@ -302,7 +302,6 @@ const SupplierForm = ({
<SelectInput
isMulti
createables
required
placeholder='Pilih Hatchery'
label='Hatchery'
value={hatcheryOptionsValues}
@@ -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,17 +145,24 @@ const ChickinFormKandang = ({
<h2 className='text-xl font-semibold'>Informasi Chick In</h2>
{/* 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'} />{' '}
Perlu Chick In ({initialValues.available_qtys?.length ?? 0})
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<RequirePermission permissions='lti.production.chickins.create'>
<Badge
variant='soft'
color={'success'}
className={{
badge: 'rounded-lg px-2',
}}
>
<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}
/>
)}
<ChickinFormView
initialValues={initialValues}
formType={formType}
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,14 +147,16 @@ const ChickinLogsView = ({
)}
{initialValues?.approval?.step_number <= 2 && (
<Button
color='success'
onClick={handleClickApprove}
className='w-full'
>
<Icon width={24} height={24} icon='material-symbols:check' />
Approve Semua Chick In
</Button>
<RequirePermission permissions='lti.production.chickins.approve'>
<Button
color='success'
onClick={handleClickApprove}
className='w-full'
>
<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={
@@ -687,7 +618,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock_name})?`}
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedRowIds?.length} data)?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -702,7 +633,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds.length} data)?`}
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds?.length} data)?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -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,16 +286,18 @@ const ProjectFlockClosingForm = ({
</div>
<div className='p-4 mt-6'>
<Button
className='w-full'
color='error'
isLoading={isLoading}
disabled={!isCanCloseValid}
onClick={() => closeModal.openModal()}
>
<Icon icon='mdi:checkbox-marked-circle-outline' />{' '}
{isCanClose ? 'Close' : 'Unclose'}
</Button>
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
<Button
className='w-full'
color='error'
isLoading={isLoading}
disabled={!isCanCloseValid}
onClick={() => closeModal.openModal()}
>
<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'
@@ -21,6 +21,11 @@ type ProjectFlockFormSchemaType = {
label: string;
} | null;
fcr_id: number;
production_standard: {
value: number | string;
label: string;
} | null;
production_standard_id: number;
location: {
value: number | string;
label: string;
@@ -100,6 +105,15 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
.min(1, 'FCR wajib diisi!')
.required('FCR wajib diisi!'),
// Production Standard
production_standard: Yup.object({
value: Yup.number().required('ID Standar Produksi wajib diisi!'),
label: Yup.string().required('Nama Standar Produksi wajib diisi!'),
}).nullable(),
production_standard_id: Yup.number()
.min(1, 'Standar Produksi wajib diisi!')
.required('Standar Produksi wajib diisi!'),
// Location
location: Yup.object({
value: Yup.number().required('ID Lokasi wajib diisi!'),
@@ -13,6 +13,7 @@ import {
KandangApi,
LocationApi,
NonstockApi,
ProductionStandardApi,
} from '@/services/api/master-data';
import { Icon } from '@iconify/react';
import { FormikErrors, useFormik } from 'formik';
@@ -136,6 +137,11 @@ const ProjectFlockForm = ({
'name'
);
const {
options: optionsProductionStandards,
isLoadingOptions: isLoadingProductionStandards,
} = useSelect(ProductionStandardApi.basePath, 'id', 'name');
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: '',
location_id: selectedLocation == '' ? '0' : selectedLocation,
@@ -341,6 +347,12 @@ const ProjectFlockForm = ({
label: initialValues.fcr.name,
}
: null,
production_standard: initialValues?.production_standard
? {
value: initialValues.production_standard?.id,
label: initialValues.production_standard.name,
}
: null,
location: initialValues?.location
? {
value: initialValues.location?.id,
@@ -356,6 +368,7 @@ const ProjectFlockForm = ({
'GROWING' | 'LAYING' | undefined
>,
fcr_id: initialValues?.fcr?.id ?? 0,
production_standard_id: initialValues?.production_standard?.id ?? 0,
location_id: initialValues?.location?.id ?? 0,
kandang_ids: initialValues?.kandangs?.map(
(k: Kandang) => k.id
@@ -400,6 +413,7 @@ const ProjectFlockForm = ({
area_id: values.area_id as number,
category: values.category as string,
fcr_id: values.fcr_id as number,
production_standard_id: values.production_standard_id as number,
location_id: values.location_id as number,
kandang_ids: values.kandang_ids as number[],
project_budgets: values.project_budgets.flatMap((budget) => {
@@ -858,6 +872,23 @@ const ProjectFlockForm = ({
isClearable
isDisabled={formType != 'add'}
/>
<SelectInput
required
label='Standar Produksi'
value={formik.values.production_standard as OptionType}
onChange={(val) => {
optionChangeHandler(val, 'production_standard');
}}
options={optionsProductionStandards}
isLoading={isLoadingProductionStandards}
isError={
formik.touched.production_standard &&
Boolean(formik.errors.production_standard)
}
errorMessage={formik.errors.production_standard as string}
isClearable
isDisabled={formType != 'add'}
/>
<SelectInput
required
label='Kategori'
@@ -26,6 +26,7 @@ import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import RequirePermission from '@/components/helper/RequirePermission';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
@@ -56,72 +57,81 @@ const RowOptionsMenu = ({
const showDeleteButton = showEditButton;
// TODO: apply RBAC
const showApproveButton = showEditButton;
const showRejectButton = showEditButton;
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{showEditButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.detail'>
<Button
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
color='warning'
color='primary'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
{showEditButton && (
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
<Button
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${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>
)}
{/* TODO: apply RBAC */}
{showApproveButton && (
<Button
variant='ghost'
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='ghost'
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
</RequirePermission>
)}
{showRejectButton && (
<Button
variant='ghost'
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='ghost'
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</RequirePermission>
)}
{showDeleteButton && (
<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>
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
<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>
</RequirePermission>
)}
</RowOptionsMenuWrapper>
);
@@ -502,47 +512,53 @@ const TransferToLayingsTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
<Button
href='/production/transfer-to-laying/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.create'>
<Button
href='/production/transfer-to-laying/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
{selectedRowIds.length > 0 && (
<>
<Button
variant='outline'
color='success'
onClick={bulkApproveClickHandler}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='success'
onClick={bulkApproveClickHandler}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
</RequirePermission>
<Button
variant='outline'
color='error'
onClick={bulkRejectClickHandler}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='error'
onClick={bulkRejectClickHandler}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</RequirePermission>
</>
)}
</div>
@@ -8,6 +8,7 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import RequirePermission from '@/components/helper/RequirePermission';
import SelectInput, {
OptionType,
useSelect,
@@ -500,34 +501,37 @@ const TransferToLayingForm = ({
<>
{isShowApproveRejectButton && (
<div className='w-full flex flex-row justify-end gap-2'>
{/* TODO: apply RBAC */}
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
</RequirePermission>
<Button
variant='outline'
color='error'
onClick={rejectClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
<Button
variant='outline'
color='error'
onClick={rejectClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</RequirePermission>
</div>
)}
</>
@@ -788,37 +792,41 @@ const TransferToLayingForm = ({
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
{isShowDeleteButton && (
<Button
type='button'
color='error'
onClick={deleteTransferToLayingClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
<Button
type='button'
color='error'
onClick={deleteTransferToLayingClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
)}
{type !== 'edit' && isShowEditButton && (
<Button
type='button'
color='warning'
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
<Button
type='button'
color='warning'
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -833,15 +841,23 @@ const TransferToLayingForm = ({
Reset
</Button>
<Button
type='submit'
color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
className='px-4'
<RequirePermission
permissions={
type === 'add'
? 'lti.production.transfer_to_laying.create'
: 'lti.production.transfer_to_laying.update'
}
>
Submit
</Button>
<Button
type='submit'
color='primary'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
className='px-4'
>
Submit
</Button>
</RequirePermission>
</div>
)}
</div>
@@ -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'
+41 -5
View File
@@ -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;
@@ -255,17 +264,20 @@ export const FLOCK_CATEGORY_OPTIONS = [
value: 'LAYING',
},
];
export const PRODUCT_FLAG_OPTIONS = [
{ label: 'DOC', value: 'DOC' },
{ label: 'EKSPEDISI', value: 'EKSPEDISI' },
{ label: 'FINISHER', value: 'FINISHER' },
{ label: 'ACTIVE', value: 'IS_ACTIVE' },
{ label: 'KIMIA', value: 'KIMIA' },
{ label: 'LAYER', value: 'LAYER' },
{ label: 'OBAT', value: 'OBAT' },
{ label: 'OVK', value: 'OVK' },
{ label: 'PAKAN', value: 'PAKAN' },
{ label: 'PRE-STARTER', value: 'PRE-STARTER' },
{ label: 'PULLET', value: 'PULLET' },
{ label: 'STARTER', value: 'STARTER' },
{ label: 'FINISHER', value: 'FINISHER' },
{ label: 'OVK', value: 'OVK' },
{ label: 'OBAT', value: 'OBAT' },
{ label: 'VITAMIN', value: 'VITAMIN' },
{ label: 'KIMIA', value: 'KIMIA' },
];
export const SUPPLIER_FLAG_OPTIONS = [
@@ -278,6 +290,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', 'PEMBELIAN'];
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
export const FINANCE_INJECTION_STATUS = ['INJECTION'];
export const APPROVAL_WORKFLOWS = [
{
key: 'PROJECT_FLOCKS',
+41 -1
View File
@@ -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
-139
View File
@@ -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,
},
},
};
-14
View File
@@ -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);
+193
View File
@@ -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');
+7
View File
@@ -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');
+23
View File
@@ -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,
})),
});
+60
View File
@@ -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;
+71
View File
@@ -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;
};
}[];
}
+3
View File
@@ -16,6 +16,8 @@ export type BaseProjectFlock = {
category: string;
fcr: Fcr;
fcr_id: number;
production_standard: ProductionStandard;
production_standard_id: number;
location: Location;
location_id: number;
period: number;
@@ -48,6 +50,7 @@ export type CreateProjectFlockPayload = {
area_id: number;
category: string;
fcr_id: number;
production_standard_id: number;
location_id: number;
kandang_ids: number[];
project_budgets?: ProjectFlockBudget[];
+27
View File
@@ -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;