Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/refactor-submission-form

This commit is contained in:
rstubryan
2026-01-07 14:01:45 +07:00
11 changed files with 119 additions and 178 deletions
+3 -1
View File
@@ -67,7 +67,9 @@ const MainDrawer = ({
const pathname = usePathname(); const pathname = usePathname();
const { permissionCheck } = useAuth(); const { permissionCheck } = useAuth();
const isPermitted = ROUTE_PERMISSIONS[pathname]?.some((permission) => const formattedPathname = pathname.endsWith('/') ? pathname : `${pathname}/`;
const isPermitted = ROUTE_PERMISSIONS[formattedPathname]?.some((permission) =>
permissionCheck(permission) permissionCheck(permission)
); );
@@ -45,7 +45,12 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
{ {
id: 'perhitunganSapronak', id: 'perhitunganSapronak',
label: 'Perhitungan Sapronak', label: 'Perhitungan Sapronak',
content: <ClosingSapronakCalculationTabContent projectFlockId={id} />, content: (
<ClosingSapronakCalculationTabContent
closingGeneralInformation={initialValue}
projectFlockId={id}
/>
),
}, },
{ {
id: 'penjualan', id: 'penjualan',
@@ -1,21 +1,25 @@
'use client'; 'use client';
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable';
import { ClosingGeneralInformation } from '@/types/api/closing';
interface ClosingSapronakCalculationTabContentProps { interface ClosingSapronakCalculationTabContentProps {
projectFlockId?: number; projectFlockId?: number;
closingGeneralInformation?: ClosingGeneralInformation;
} }
const ClosingSapronakCalculationTabContent = ({ const ClosingSapronakCalculationTabContent = ({
projectFlockId, projectFlockId,
closingGeneralInformation,
}: ClosingSapronakCalculationTabContentProps) => { }: ClosingSapronakCalculationTabContentProps) => {
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{projectFlockId && ( {projectFlockId && (
<> <>
<ClosingSapronakCalculationTable projectFlockId={projectFlockId} /> <ClosingSapronakCalculationTable
closingGeneralInformation={closingGeneralInformation}
projectFlockId={projectFlockId}
/>
</> </>
)} )}
</div> </div>
@@ -13,15 +13,16 @@ import { useMemo } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { ClosingApi } from '@/services/api/closing'; import { ClosingApi } from '@/services/api/closing';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { ClosingGeneralInformation } from '@/types/api/closing';
interface ClosingSapronakCalculationTableProps { interface ClosingSapronakCalculationTableProps {
type?: 'detail';
projectFlockId: number; projectFlockId: number;
closingGeneralInformation?: ClosingGeneralInformation;
} }
const ClosingSapronakCalculationTable = ({ const ClosingSapronakCalculationTable = ({
type,
projectFlockId, projectFlockId,
closingGeneralInformation,
}: ClosingSapronakCalculationTableProps) => { }: ClosingSapronakCalculationTableProps) => {
const { data: sapronakCalculation, isLoading } = useSWR( const { data: sapronakCalculation, isLoading } = useSWR(
`/closing/sapronak-calculation/${projectFlockId}`, `/closing/sapronak-calculation/${projectFlockId}`,
@@ -182,8 +183,13 @@ const ClosingSapronakCalculationTable = ({
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{/* Table DOC jika kategori Project Flock Growing */}
<Card <Card
title='DOC' title={
closingGeneralInformation?.project_category === 'GROWING'
? 'DOC'
: 'Pullet'
}
collapsible collapsible
defaultCollapsed={false} defaultCollapsed={false}
className={{ className={{
@@ -194,10 +200,16 @@ const ClosingSapronakCalculationTable = ({
<Table<RowSapronakCalculation> <Table<RowSapronakCalculation>
data={ data={
isResponseSuccess(sapronakCalculation) isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc?.rows ?? []) ? ((closingGeneralInformation?.project_category === 'GROWING'
? sapronakCalculation.data?.doc?.rows
: sapronakCalculation.data?.pullet?.rows) ?? [])
: [] : []
} }
columns={docColumns} columns={
closingGeneralInformation?.project_category === 'GROWING'
? docColumns
: pulletColumns
}
className={{ className={{
containerClassName: 'my-4', containerClassName: 'my-4',
}} }}
@@ -250,29 +262,6 @@ const ClosingSapronakCalculationTable = ({
renderFooter={isResponseSuccess(sapronakCalculation)} renderFooter={isResponseSuccess(sapronakCalculation)}
/> />
</Card> </Card>
<Card
title='Pullet'
variant='bordered'
collapsible
defaultCollapsed={true}
className={{
wrapper: 'w-full',
}}
>
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pullet?.rows ?? [])
: []
}
columns={pulletColumns}
className={{
containerClassName: 'my-4',
}}
renderFooter={isResponseSuccess(sapronakCalculation)}
/>
</Card>
</div> </div>
); );
}; };
@@ -682,7 +682,7 @@ const MarketingTable = () => {
<Modal <Modal
ref={productsModal.ref} ref={productsModal.ref}
className={{ className={{
modalBox: 'max-w-2/5 z-100', modalBox: 'xs:max-w-2/5 z-100',
}} }}
closeOnBackdrop closeOnBackdrop
> >
@@ -724,6 +724,7 @@ const MarketingTable = () => {
}, },
]} ]}
className={{ className={{
containerClassName: 'p-6',
tableWrapperClassName: 'overflow-x-auto min-h-full!', tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200', headerRowClassName: 'border-b border-b-gray-200',
@@ -124,7 +124,10 @@ const MarketingDetail = ({
return ( return (
<> <>
<div className='flex flex-col w-full gap-4'> <div className='flex flex-col w-full gap-4'>
<FormHeader title='Detail Sales Order' backUrl='/marketing' /> <FormHeader
title={`Detail ${Number(initialValues?.latest_approval?.step_number) > 2 ? 'Delivery Order' : 'Sales Order'}`}
backUrl='/marketing'
/>
{!isLoadingApproval && approvals && ( {!isLoadingApproval && approvals && (
<ApprovalSteps approvals={approvals} /> <ApprovalSteps approvals={approvals} />
)} )}
@@ -202,8 +205,23 @@ const MarketingDetail = ({
No. Sales Order No. Sales Order
</td> </td>
<td>:</td> <td>:</td>
<td width='50%'>{initialValues?.so_number}</td> <td width='50%' className='font-mono'>
{initialValues?.so_number}
</td>
</tr> </tr>
{Number(initialValues?.latest_approval?.step_number) > 2 && (
<tr>
<td width='45%' className='font-semibold'>
No. Delivery Order
</td>
<td>:</td>
<td width='50%' className='font-mono'>
{initialValues?.delivery_order
?.map((item) => item.do_number)
.join(', ')}
</td>
</tr>
)}
<tr> <tr>
<td className='font-semibold'>Nama Pelanggan</td> <td className='font-semibold'>Nama Pelanggan</td>
<td>:</td> <td>:</td>
@@ -230,12 +248,27 @@ const MarketingDetail = ({
<td>{initialValues?.notes ?? '-'}</td> <td>{initialValues?.notes ?? '-'}</td>
</tr> </tr>
<tr> <tr>
<td className='font-semibold'>Dokumen</td> <td className='font-semibold'>Dokumen Penjualan</td>
<td>:</td> <td>:</td>
<td> <td>
<SalesOrderExport data={initialValues} /> <SalesOrderExport data={initialValues} />
</td> </td>
</tr> </tr>
{Number(initialValues?.latest_approval?.step_number) > 2 && (
<tr>
<td className='font-semibold'>Dokumen Pengiriman</td>
<td>:</td>
<td className='flex flex-wrap gap-2'>
{initialValues?.delivery_order?.map((item, index) => (
<DeliveryOrderExport
key={index}
data={initialValues}
deliveryOrder={item}
/>
))}
</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -15,6 +15,7 @@ import { BaseSalesOrder } from '@/types/api/marketing/marketing';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm'; import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { isResponseSuccess } from '@/lib/api-helper';
const DeliveryOrderProductForm = ({ const DeliveryOrderProductForm = ({
formState, formState,
@@ -208,7 +209,7 @@ const DeliveryOrderProductForm = ({
...formik.values, ...formik.values,
marketing_product_id: undefined, marketing_product_id: undefined,
marketing_product: null, marketing_product: null,
qty: formik.values.qty || '', qty: '',
unit_price: '', unit_price: '',
total_price: '', total_price: '',
avg_weight: '', avg_weight: '',
@@ -222,7 +223,7 @@ const DeliveryOrderProductForm = ({
...formik.values, ...formik.values,
marketing_product_id: selected.value as number, marketing_product_id: selected.value as number,
marketing_product: SalesProductToFieldValues(so), marketing_product: SalesProductToFieldValues(so),
qty: formik.values.qty || so.qty, qty: so.qty,
unit_price: so.unit_price, unit_price: so.unit_price,
total_price: so.total_price, total_price: so.total_price,
avg_weight: so.avg_weight, avg_weight: so.avg_weight,
@@ -298,8 +299,18 @@ const DeliveryOrderProductForm = ({
isError={Boolean(formik.errors.qty)} isError={Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty} errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas' placeholder='Masukan Kuantitas'
bottomLabel={
formik.values.marketing_product_id
? 'Stok dijual: ' +
salesOrders?.find(
(item) => item.id === formik.values.marketing_product_id
)?.qty
: ''
}
/> />
</div>
<div className='divider my-6'></div>
<div className='grid sm:grid-cols-2 gap-4'>
<NumberInput <NumberInput
required required
label='Avg. Bobot (Kg)' label='Avg. Bobot (Kg)'
@@ -17,7 +17,11 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { formatVechicleNumber } from '@/lib/helper'; import {
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import PatternInput from '@/components/input/PatternInput'; import PatternInput from '@/components/input/PatternInput';
import Alert from '@/components/Alert'; import Alert from '@/components/Alert';
@@ -34,6 +38,7 @@ const SalesOrderProductForm = ({
const [formErrorMessage, setFormErrorMessage] = useState(''); const [formErrorMessage, setFormErrorMessage] = useState('');
const [currentInput, setCurrentInput] = useState<string>(''); const [currentInput, setCurrentInput] = useState<string>('');
// ============ Formik ============
const formik = useFormik<SalesOrderProductFormValues>({ const formik = useFormik<SalesOrderProductFormValues>({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
@@ -58,6 +63,7 @@ const SalesOrderProductForm = ({
isInitialValid: false, isInitialValid: false,
}); });
// ===== Options =====
const { const {
options: kandangSourceOptions, options: kandangSourceOptions,
isLoadingOptions: isLoadingKandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions,
@@ -86,12 +92,13 @@ const SalesOrderProductForm = ({
); );
}, [warehouseSourceOptions, exisitingValues]); }, [warehouseSourceOptions, exisitingValues]);
// ===== Handler =====
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('kandang', val as OptionType); formik.setFieldValue('kandang', val as OptionType);
formik.setFieldValue('kandang_id', (val as OptionType)?.value); formik.setFieldValue('kandang_id', (val as OptionType)?.value);
formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse_id', null);
formik.setFieldValue('product_warehouse', null); formik.setFieldValue('product_warehouse', null);
formik.setFieldValue('qty', null); formik.setFieldValue('qty', '');
}; };
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -106,7 +113,7 @@ const SalesOrderProductForm = ({
formik.setFieldValue('qty', productWarehouse?.quantity); formik.setFieldValue('qty', productWarehouse?.quantity);
handleBlurField('qty'); handleBlurField('qty');
} else { } else {
formik.setFieldValue('qty', null); formik.setFieldValue('qty', '');
} }
}; };
@@ -248,7 +255,24 @@ const SalesOrderProductForm = ({
isError={formik.touched.qty && Boolean(formik.errors.qty)} isError={formik.touched.qty && Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty} errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas' placeholder='Masukan Kuantitas'
bottomLabel={
isResponseSuccess(warehouseSourceRawData) &&
formik.values.product_warehouse_id
? `Stok tersedia: ${formatNumber(
warehouseSourceRawData?.data?.find(
(item) => item.id === formik.values.product_warehouse_id
)?.quantity ?? 0
)} ${
warehouseSourceRawData?.data?.find(
(item) => item.id === formik.values.product_warehouse_id
)?.product?.uom?.name ?? ''
}`
: ''
}
/> />
</div>
<div className='divider my-6'></div>
<div className='grid sm:grid-cols-2 gap-4 z-200'>
<NumberInput <NumberInput
required required
label='Avg. Bobot (Kg)' label='Avg. Bobot (Kg)'
@@ -308,7 +308,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<Button <Button
color='primary' color='primary'
className='w-full sm:w-fit' className='w-full sm:w-fit'
href='/production/project-flock/add' onClick={() => {
setRowSelection({});
router.push('/production/project-flock/add');
}}
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='ic:round-plus' width={24} height={24} />
Tambah Tambah
@@ -38,11 +38,6 @@ import { BaseApiResponse } from '@/types/api/api-general';
import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant'; import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import Card from '@/components/Card'; import Card from '@/components/Card';
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable'; import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
@@ -90,18 +85,8 @@ const ProjectFlockForm = ({
const setIsValid = useUiStore((s) => s.setIsValid); const setIsValid = useUiStore((s) => s.setIsValid);
const deleteModal = useModal(); const deleteModal = useModal();
const confirmModal = useModal();
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isApprovedDisabled, setIsApprovedDisabled] = useState(
initialValues?.approval?.step_name == 'Pengajuan' ? false : true
);
const [isRejectedDisabled, setIsRejectedDisabled] =
useState(!isApprovedDisabled);
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
!isApprovedDisabled ? 'APPROVED' : 'REJECTED'
);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>( const [rowSelection, setRowSelection] = useState<Record<string, boolean>>(
() => () =>
@@ -163,17 +148,6 @@ const ProjectFlockForm = ({
isLoadingOptions: isLoadingNonstocks, isLoadingOptions: isLoadingNonstocks,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name'); } = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
const {
approvals,
isLoading: approvalsLoading,
refresh: refreshApprovals,
} = useApprovalSteps({
latestApproval: initialValues?.approval,
approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
moduleName: 'PROJECT_FLOCKS',
moduleId: initialValues?.id.toString() ?? '',
});
useEffect(() => { useEffect(() => {
if (isResponseSuccess(kandang)) { if (isResponseSuccess(kandang)) {
if (selectedLocation) { if (selectedLocation) {
@@ -522,19 +496,6 @@ const ProjectFlockForm = ({
return unsub; return unsub;
}, []); }, []);
useEffect(() => {
if (initialValues?.approval?.step_name) {
const pengajuanRejected =
initialValues.approval.step_number == 1 &&
initialValues.approval.action == 'REJECTED';
const approvedDisabled =
initialValues.approval.step_number !== 1 || pengajuanRejected;
setIsApprovedDisabled(approvedDisabled);
setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
}
}, [initialValues]);
// Actions handler // Actions handler
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
@@ -588,29 +549,6 @@ const ProjectFlockForm = ({
} }
}; };
const confirmApprovalHandler = async (
notes: string,
approvalAction: 'REJECTED' | 'APPROVED'
) => {
if (initialValues?.id === undefined) return;
setIsApproveLoading(true);
const approvalRes =
approvalAction == 'APPROVED'
? await ProjectFlockApi.approve(initialValues?.id, notes)
: await ProjectFlockApi.reject(initialValues?.id, notes);
if (isResponseSuccess(approvalRes)) {
refreshProjectFlocks?.();
toast.success(approvalRes.message as string);
}
if (isResponseError(approvalRes)) {
toast.error(approvalRes?.message as string);
}
refreshApprovals();
confirmModal.closeModal();
setIsApproveLoading(false);
};
const handleBudgetChange = ( const handleBudgetChange = (
index: number, index: number,
fieldName: 'qty' | 'price' | 'total_price', fieldName: 'qty' | 'price' | 'total_price',
@@ -744,47 +682,7 @@ const ProjectFlockForm = ({
</div> </div>
</div> </div>
)} )}
{approvals && !approvalsLoading && formType == 'detail' && (
<ApprovalSteps approvals={approvals} />
)}
{formType == 'detail' && (
<div className='w-full flex flex-col sm:flex-row gap-2 py-4'>
<RequirePermission permissions='lti.production.project_flocks.approve'>
<Button
variant='outline'
color='success'
onClick={() => {
if (initialValues?.id) {
setApprovalAction('APPROVED');
confirmModal.openModal();
}
}}
disabled={!initialValues?.id || isApprovedDisabled}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
</RequirePermission>
<RequirePermission permissions='lti.production.project_flocks.approve'>
<Button
variant='outline'
color='error'
onClick={() => {
if (initialValues?.id) {
setApprovalAction('REJECTED');
confirmModal.openModal();
}
}}
disabled={!initialValues?.id || isRejectedDisabled}
className='w-full sm:w-fit'
>
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
</RequirePermission>
</div>
)}
<form <form
className='w-auto h-auto' className='w-auto h-auto'
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
@@ -1154,14 +1052,6 @@ const ProjectFlockForm = ({
</div> </div>
<div className='flex flex-row justify-center gap-2 flex-wrap my-6 px-4'> <div className='flex flex-row justify-center gap-2 flex-wrap my-6 px-4'>
{/* <div className='w-120'>
<div className='text-primary text-sm'>
{JSON.stringify(formik.values)}
</div>
<div className='text-error text-sm'>
{JSON.stringify(formik.errors)}
</div>
</div> */}
{formType !== 'detail' && ( {formType !== 'detail' && (
<RequirePermission <RequirePermission
permissions={ permissions={
@@ -1200,27 +1090,6 @@ const ProjectFlockForm = ({
onClick: confirmationModalDeleteClickHandler, onClick: confirmationModalDeleteClickHandler,
}} }}
/> />
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${
approvalAction == 'APPROVED' ? 'approve' : 'reject'
} Project Flock berikut? (${initialValues?.flock_name} - ${
initialValues?.area?.name
})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: approvalAction == 'APPROVED' ? 'success' : 'error',
isLoading: isApproveLoading,
onClick: (notes) => {
confirmApprovalHandler(notes, approvalAction);
},
}}
/>
</> </>
); );
}; };
+2 -2
View File
@@ -13,7 +13,7 @@ export class MarketingReportApiService extends BaseApiService<
unknown, unknown,
unknown unknown
> { > {
constructor(basePath: string = '/reports/marketings/daily-marketing') { constructor(basePath: string = '/reports/marketing') {
super(basePath); super(basePath);
} }
@@ -71,5 +71,5 @@ export class MarketingReportApiService extends BaseApiService<
} }
export const MarketingReportApi = new MarketingReportApiService( export const MarketingReportApi = new MarketingReportApiService(
'/reports/marketings/daily-marketing' '/reports/marketing'
); );