mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE): US#278 slicing UI from and client side validation
This commit is contained in:
@@ -4,7 +4,7 @@ import { usePathname, useRouter } from 'next/navigation';
|
||||
import Drawer from '@/components/Drawer';
|
||||
import React, { ReactNode } from 'react';
|
||||
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { useProjectFlockUiStore } from '@/stores/ui/slices/production/project-flock.slice';
|
||||
|
||||
export default function ProjectFlockLayout({
|
||||
children,
|
||||
@@ -13,7 +13,7 @@ export default function ProjectFlockLayout({
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const toggleValidate = useUiStore((s) => s.toggleValidate);
|
||||
const toggleValidate = useProjectFlockUiStore((s) => s.toggleValidate);
|
||||
|
||||
const isAdd = pathname.endsWith('/add');
|
||||
const isEdit = pathname.includes('/detail/edit');
|
||||
@@ -22,10 +22,10 @@ export default function ProjectFlockLayout({
|
||||
|
||||
const isOpen = isAdd || isEdit || isDetail || isChickin;
|
||||
|
||||
// const childRef = useRef<ProjectFlockFormRef>(null);
|
||||
|
||||
const handleBackdropClick = () => {
|
||||
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
||||
const unsub = useProjectFlockUiStore
|
||||
.getState()
|
||||
.subscribeIsValid((isValid) => {
|
||||
if (isValid) {
|
||||
unsub(); // berhenti listen
|
||||
router.push('/production/project-flock');
|
||||
@@ -39,7 +39,9 @@ export default function ProjectFlockLayout({
|
||||
<>
|
||||
{/* List page always rendered */}
|
||||
<div>
|
||||
<ProjectFlockTable />
|
||||
<ProjectFlockTable
|
||||
refresh={() => !isOpen && router.push('/production/project-flock')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Render Drawer only on /add */}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { CellContext, SortingState } from '@tanstack/react-table';
|
||||
import { ChangeEventHandler, useRef, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
|
||||
@@ -94,7 +94,7 @@ const RowOptionsMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectFlockTable = () => {
|
||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -154,7 +154,8 @@ const ProjectFlockTable = () => {
|
||||
mutate: refreshProjectFlocks,
|
||||
} = useSWR(
|
||||
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
|
||||
ProjectFlockApi.getAllFetcher
|
||||
ProjectFlockApi.getAllFetcher,
|
||||
{ revalidateOnMount: true }
|
||||
);
|
||||
|
||||
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
||||
@@ -255,6 +256,10 @@ const ProjectFlockTable = () => {
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshProjectFlocks();
|
||||
}, [refresh]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
|
||||
@@ -1,6 +1,71 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const ProjectFlockFormSchema = Yup.object({
|
||||
type ProjectFlockFormSchemaType = {
|
||||
flock: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
flock_name: string;
|
||||
area: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
area_id: number;
|
||||
category_option: {
|
||||
value: string;
|
||||
label: string;
|
||||
} | null;
|
||||
category: string;
|
||||
fcr: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
fcr_id: number;
|
||||
location: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
location_id: number;
|
||||
kandang_ids: number[];
|
||||
project_budgets: ProjectFlockBudgetsSchemaType[];
|
||||
};
|
||||
|
||||
export type ProjectFlockBudgetsSchemaType = {
|
||||
nonstock: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
nonstock_id: number | string;
|
||||
qty: number | string;
|
||||
price: number | string;
|
||||
total_price: number | string;
|
||||
};
|
||||
|
||||
export const ProjectFlockBudgetsSchema: Yup.ObjectSchema<ProjectFlockBudgetsSchemaType> =
|
||||
Yup.object({
|
||||
nonstock: Yup.object({
|
||||
value: Yup.number().required('ID Nonstock wajib diisi!'),
|
||||
label: Yup.string().required('Nama Nonstock wajib diisi!'),
|
||||
}).required('Nonstock wajib diisi!'),
|
||||
nonstock_id: Yup.number()
|
||||
.min(1, 'Nonstock wajib diisi!')
|
||||
.required('Nonstock wajib diisi!'),
|
||||
qty: Yup.number()
|
||||
.typeError('Jumlah harus berupa angka!')
|
||||
.min(1, 'Jumlah minimal 1!')
|
||||
.required('Jumlah wajib diisi!'),
|
||||
price: Yup.number()
|
||||
.typeError('Harga harus berupa angka!')
|
||||
.min(1, 'Harga minimal 1!')
|
||||
.required('Harga wajib diisi!'),
|
||||
total_price: Yup.number()
|
||||
.typeError('Harga harus berupa angka!')
|
||||
.min(1, 'Harga minimal 1!')
|
||||
.required('Harga wajib diisi!'),
|
||||
});
|
||||
|
||||
export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType> =
|
||||
Yup.object({
|
||||
// Flock
|
||||
flock: Yup.object({
|
||||
value: Yup.number().required('ID Flock wajib diisi!'),
|
||||
@@ -31,7 +96,9 @@ export const ProjectFlockFormSchema = Yup.object({
|
||||
value: Yup.number().required('ID FCR wajib diisi!'),
|
||||
label: Yup.string().required('Nama FCR wajib diisi!'),
|
||||
}).nullable(),
|
||||
fcr_id: Yup.number().min(1, 'FCR wajib diisi!').required('FCR wajib diisi!'),
|
||||
fcr_id: Yup.number()
|
||||
.min(1, 'FCR wajib diisi!')
|
||||
.required('FCR wajib diisi!'),
|
||||
|
||||
// Location
|
||||
location: Yup.object({
|
||||
@@ -43,10 +110,15 @@ export const ProjectFlockFormSchema = Yup.object({
|
||||
.required('Lokasi wajib diisi!'),
|
||||
|
||||
kandang_ids: Yup.array()
|
||||
.of(Yup.number().typeError('Kandang tidak valid!'))
|
||||
.of(Yup.number().required('Kandang tidak valid!'))
|
||||
.min(1, 'Minimal harus ada 1 kandang!')
|
||||
.required('Kandang wajib diisi!'),
|
||||
});
|
||||
|
||||
project_budgets: Yup.array()
|
||||
.of(ProjectFlockBudgetsSchema)
|
||||
.min(1, 'Minimal harus ada 1 data budget!')
|
||||
.required('Data budget wajib diisi!'),
|
||||
});
|
||||
|
||||
export type ProjectFlockFormValues = Yup.InferType<
|
||||
typeof ProjectFlockFormSchema
|
||||
|
||||
@@ -12,13 +12,15 @@ import {
|
||||
FlockApi,
|
||||
KandangApi,
|
||||
LocationApi,
|
||||
NonstockApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useFormik } from 'formik';
|
||||
import { FormikErrors, useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import useSWR, { KeyedMutator } from 'swr';
|
||||
import {
|
||||
ProjectFlockBudgetsSchemaType,
|
||||
ProjectFlockFormSchema,
|
||||
ProjectFlockFormValues,
|
||||
UpdateProjectFlockFormSchema,
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
import {
|
||||
CreateProjectFlockPayload,
|
||||
ProjectFlock,
|
||||
ProjectFlockBudget,
|
||||
} from '@/types/api/production/project-flock';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
@@ -41,8 +44,9 @@ import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import Card from '@/components/Card';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
|
||||
import { useProjectFlockUiStore } from '@/stores/ui/slices/production/project-flock.slice';
|
||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
|
||||
interface ProjectFlockFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -79,8 +83,8 @@ const ProjectFlockForm = ({
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
) ?? ''
|
||||
);
|
||||
const subscribeValidate = useUiStore((s) => s.subscribeValidate);
|
||||
const setIsValid = useUiStore((s) => s.setIsValid);
|
||||
const subscribeValidate = useProjectFlockUiStore((s) => s.subscribeValidate);
|
||||
const setIsValid = useProjectFlockUiStore((s) => s.setIsValid);
|
||||
|
||||
const deleteModal = useModal();
|
||||
const confirmModal = useModal();
|
||||
@@ -158,6 +162,12 @@ const ProjectFlockForm = ({
|
||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||
);
|
||||
|
||||
const {
|
||||
options: optionsNonstock,
|
||||
rawData: nonstocks,
|
||||
isLoadingOptions: isLoadingNonstocks,
|
||||
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
approvals,
|
||||
isLoading: approvalsLoading,
|
||||
@@ -359,10 +369,18 @@ const ProjectFlockForm = ({
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
|
||||
| number
|
||||
| undefined
|
||||
)[],
|
||||
kandang_ids: initialValues?.kandangs?.map(
|
||||
(k: Kandang) => k.id
|
||||
) as number[],
|
||||
project_budgets: [
|
||||
{
|
||||
nonstock: null,
|
||||
nonstock_id: '',
|
||||
qty: '',
|
||||
price: '',
|
||||
total_price: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [initialValues, optionsFlock]);
|
||||
|
||||
@@ -441,6 +459,15 @@ const ProjectFlockForm = ({
|
||||
| number
|
||||
| undefined
|
||||
)[],
|
||||
project_budgets: [
|
||||
{
|
||||
nonstock: null,
|
||||
nonstock_id: '',
|
||||
qty: '',
|
||||
price: '',
|
||||
total_price: '',
|
||||
},
|
||||
],
|
||||
} as ProjectFlockFormValues,
|
||||
enableReinitialize: true,
|
||||
validationSchema:
|
||||
@@ -457,6 +484,13 @@ const ProjectFlockForm = ({
|
||||
fcr_id: values.fcr_id as number,
|
||||
location_id: values.location_id as number,
|
||||
kandang_ids: values.kandang_ids as number[],
|
||||
project_budgets: values.project_budgets.flatMap((budget) => {
|
||||
return {
|
||||
nonstock_id: budget.nonstock_id,
|
||||
qty: budget.qty,
|
||||
price: budget.price,
|
||||
} as ProjectFlockBudget;
|
||||
}),
|
||||
};
|
||||
|
||||
switch (formType) {
|
||||
@@ -471,8 +505,8 @@ const ProjectFlockForm = ({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
// Effect Initial
|
||||
useEffect(() => {
|
||||
if (formType == 'detail') {
|
||||
@@ -522,6 +556,33 @@ const ProjectFlockForm = ({
|
||||
});
|
||||
}, [rowSelection, formikSetValues]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = subscribeValidate(() => {
|
||||
formik.validateForm().then((errors) => {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
// Membentuk touched object yang strongly-typed
|
||||
const touched: Record<string, boolean | Record<string, boolean>[]> =
|
||||
{};
|
||||
Object.keys(formik.values).forEach((key) => {
|
||||
if (
|
||||
key === 'project_budgets' &&
|
||||
Array.isArray(formik.values.project_budgets)
|
||||
) {
|
||||
touched[key] = formik.values.project_budgets.map(() => ({})); // Mark each item as touched if it's an array
|
||||
} else {
|
||||
touched[key] = true;
|
||||
}
|
||||
});
|
||||
|
||||
formik.setTouched(touched, true);
|
||||
}
|
||||
setIsValid(Object.keys(errors).length === 0);
|
||||
});
|
||||
});
|
||||
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
// Actions handler
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
@@ -539,6 +600,42 @@ const ProjectFlockForm = ({
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const onAddBudgetRowHandler = () => {
|
||||
const newProjectBudgets = [
|
||||
...(formik.values.project_budgets ?? []),
|
||||
{
|
||||
nonstock: null,
|
||||
nonstock_id: '',
|
||||
qty: '',
|
||||
price: '',
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('project_budgets', newProjectBudgets);
|
||||
};
|
||||
|
||||
const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
|
||||
console.log(`nonstock_id: ${nonstock_id}, index: ${index}`);
|
||||
if (!nonstock_id) {
|
||||
const updatedBudgets = formik.values.project_budgets
|
||||
.map((budget, i) => {
|
||||
if (i == index) {
|
||||
console.log(`buget: ${null}, index: ${index}, i: ${i}`);
|
||||
return null;
|
||||
} else {
|
||||
console.log(`buget: ${budget}, index: ${index}, i: ${i}`);
|
||||
return budget;
|
||||
}
|
||||
})
|
||||
.filter((budget) => budget != null);
|
||||
formik.setFieldValue('project_budgets', updatedBudgets);
|
||||
} else {
|
||||
const updatedBudgets = (formik.values.project_budgets ?? []).filter(
|
||||
(budget) => budget.nonstock_id !== nonstock_id
|
||||
);
|
||||
formik.setFieldValue('project_budgets', updatedBudgets);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmApprovalHandler = async (
|
||||
notes: string,
|
||||
approvalAction: 'REJECTED' | 'APPROVED'
|
||||
@@ -562,6 +659,67 @@ const ProjectFlockForm = ({
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const handleBudgetChange = (
|
||||
index: number,
|
||||
fieldName: 'qty' | 'price' | 'total_price',
|
||||
value: string
|
||||
) => {
|
||||
const updatedBudgets = [...formik.values.project_budgets];
|
||||
const currentBudget = updatedBudgets[index];
|
||||
|
||||
const isNewValueEmpty = value === '';
|
||||
|
||||
let numericValue: number;
|
||||
|
||||
if (isNewValueEmpty) {
|
||||
(currentBudget[fieldName] as string) = '';
|
||||
numericValue = 0;
|
||||
|
||||
formik.setFieldValue('project_budgets', updatedBudgets);
|
||||
return;
|
||||
} else {
|
||||
numericValue = Math.max(0, parseFloat(value) || 0);
|
||||
|
||||
(currentBudget[fieldName] as number) = numericValue;
|
||||
}
|
||||
|
||||
const getSafeNumber = (val: string | number) =>
|
||||
Math.max(0, parseFloat(String(val)) || 0);
|
||||
|
||||
const currentQty = getSafeNumber(currentBudget.qty);
|
||||
const currentPrice = getSafeNumber(currentBudget.price);
|
||||
const currentTotal = getSafeNumber(currentBudget.total_price);
|
||||
|
||||
let newQty = currentQty;
|
||||
let newPrice = currentPrice;
|
||||
let newTotal = currentTotal;
|
||||
|
||||
if (fieldName === 'price') {
|
||||
// Jika Harga Satuan diubah, hitung Total Harga
|
||||
newTotal = newQty * numericValue;
|
||||
newPrice = numericValue;
|
||||
} else if (fieldName === 'qty') {
|
||||
// Jika Kuantitas diubah, hitung Total Harga
|
||||
newTotal = numericValue * newPrice;
|
||||
newQty = numericValue;
|
||||
} else if (fieldName === 'total_price') {
|
||||
// Jika Total Harga diubah, hitung Harga Satuan
|
||||
newTotal = numericValue;
|
||||
if (newQty > 0) {
|
||||
newPrice = newTotal / newQty;
|
||||
} else {
|
||||
// Jika Qty 0, Harga Satuan tetap 0
|
||||
newPrice = 0;
|
||||
}
|
||||
}
|
||||
|
||||
currentBudget.qty = newQty;
|
||||
currentBudget.price = newPrice;
|
||||
currentBudget.total_price = newTotal;
|
||||
|
||||
formik.setFieldValue('project_budgets', updatedBudgets);
|
||||
};
|
||||
|
||||
const selectedPeriod = isResponseSuccess(periodFlocks)
|
||||
? periodFlocks.data.find((kandang) =>
|
||||
formik.values.kandang_ids?.includes(kandang.id)
|
||||
@@ -569,39 +727,11 @@ const ProjectFlockForm = ({
|
||||
: undefined;
|
||||
const inputPeriod =
|
||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||
|
||||
// expose method validate() ke parent
|
||||
// TODO: Buat Store untuk kirim props formik.isValid ke parent (layout.tsx)
|
||||
// useImperativeHandle(ref, () => ({
|
||||
// validate() {
|
||||
// formik.validateForm();
|
||||
// const isValid = formik.isValid;
|
||||
// return isValid;
|
||||
// },
|
||||
// }));
|
||||
useEffect(() => {
|
||||
const unsub = subscribeValidate(() => {
|
||||
formik.validateForm().then((errors) => {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
// Membentuk touched object yang strongly-typed
|
||||
const touched = Object.keys(formik.values).reduce<
|
||||
Record<keyof typeof formik.values, boolean>
|
||||
>(
|
||||
(acc, key) => {
|
||||
acc[key as keyof typeof formik.values] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<keyof typeof formik.values, boolean>
|
||||
const filteredNonStockOptions = optionsNonstock.filter((nonstock) => {
|
||||
return !(formik.values.project_budgets ?? []).some(
|
||||
(budget) => budget.nonstock_id === nonstock.value
|
||||
);
|
||||
|
||||
formik.setTouched(touched, true);
|
||||
}
|
||||
setIsValid(Object.keys(errors).length === 0);
|
||||
});
|
||||
});
|
||||
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -699,6 +829,7 @@ const ProjectFlockForm = ({
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
>
|
||||
{/* Card Informasi Umum */}
|
||||
<Card
|
||||
title='Informasi Umum'
|
||||
variant='bordered'
|
||||
@@ -807,12 +938,14 @@ const ProjectFlockForm = ({
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card Pilih Kandang */}
|
||||
<Card
|
||||
collapsible
|
||||
title='Pilih Kandang'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
wrapper: 'w-full mb-4',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto duration-300 ease-in-out'>
|
||||
@@ -833,6 +966,214 @@ const ProjectFlockForm = ({
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card Estimasi Budget */}
|
||||
<Card
|
||||
collapsible
|
||||
title='Estimasi Aggaran per Kandang'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<table className='w-full mt-4'>
|
||||
<thead>
|
||||
<tr className='border-b border-gray-300'>
|
||||
<th className='text-start px-2 py-3'>Produk</th>
|
||||
<th className='text-start px-2 py-3'>Kuantitas</th>
|
||||
<th className='text-start px-2 py-3'>Harga Satuan</th>
|
||||
<th className='text-start px-2 py-3'>Harga Total</th>
|
||||
<th className='text-start px-2 py-3'>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.project_budgets &&
|
||||
formik.values.project_budgets.length > 0 ? (
|
||||
formik.values.project_budgets.map((budget, index) => (
|
||||
<tr key={index} className='align-top'>
|
||||
<td className='px-2 py-3'>
|
||||
<SelectInput
|
||||
isClearable
|
||||
options={filteredNonStockOptions ?? []}
|
||||
isLoading={isLoadingNonstocks}
|
||||
placeholder='Pilih barang non stock'
|
||||
value={formik.values.project_budgets[index].nonstock}
|
||||
onChange={(val) => {
|
||||
const updatedBudgets = [
|
||||
...formik.values.project_budgets,
|
||||
];
|
||||
updatedBudgets[index].nonstock = val as OptionType;
|
||||
updatedBudgets[index].nonstock_id =
|
||||
(val as OptionType)
|
||||
? (val as OptionType).value
|
||||
: 0;
|
||||
formik.setFieldValue(
|
||||
'project_budgets',
|
||||
updatedBudgets
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`project_budgets[${index}].nonstock_id`,
|
||||
true
|
||||
);
|
||||
}}
|
||||
errorMessage={
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.nonstock_id as string
|
||||
}
|
||||
isError={
|
||||
formik.touched.project_budgets?.[index]
|
||||
?.nonstock_id &&
|
||||
Boolean(
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.nonstock_id as string
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className='px-2 py-3'>
|
||||
<NumberInput
|
||||
name={`project_budgets[${index}].qty`}
|
||||
placeholder='Masukkan jumlah'
|
||||
value={formik.values.project_budgets[index].qty}
|
||||
onChange={(e) =>
|
||||
handleBudgetChange(index, 'qty', e.target.value)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
allowNegative={false}
|
||||
endAdornment={
|
||||
isResponseSuccess(nonstocks)
|
||||
? (nonstocks.data.find(
|
||||
(ns) => ns.id === budget.nonstock_id
|
||||
)?.uom?.name ?? '')
|
||||
: ''
|
||||
}
|
||||
errorMessage={
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.qty as string
|
||||
}
|
||||
isError={
|
||||
formik.touched.project_budgets?.[index]?.qty &&
|
||||
Boolean(
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.qty as string
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className='px-2 py-3'>
|
||||
<NumberInput
|
||||
name={`project_budgets[${index}].price`}
|
||||
value={formik.values.project_budgets[index].price}
|
||||
onChange={(e) =>
|
||||
handleBudgetChange(index, 'price', e.target.value)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
placeholder='Masukkan harga satuan'
|
||||
allowNegative={false}
|
||||
startAdornment='Rp'
|
||||
errorMessage={
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.price as string
|
||||
}
|
||||
isError={
|
||||
formik.touched.project_budgets?.[index]?.price &&
|
||||
Boolean(
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.price as string
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className='px-2 py-3'>
|
||||
<NumberInput
|
||||
name={`project_budgets[${index}].total_price`}
|
||||
value={
|
||||
formik.values.project_budgets[index].total_price
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleBudgetChange(
|
||||
index,
|
||||
'total_price',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
placeholder='Masukkan harga satuan'
|
||||
allowNegative={false}
|
||||
startAdornment='Rp'
|
||||
errorMessage={
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.total_price as string
|
||||
}
|
||||
isError={
|
||||
formik.touched.project_budgets?.[index]
|
||||
?.total_price &&
|
||||
Boolean(
|
||||
(
|
||||
formik.errors.project_budgets?.[
|
||||
index
|
||||
] as FormikErrors<ProjectFlockBudgetsSchemaType>
|
||||
)?.total_price as string
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className='px-2 py-4'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() =>
|
||||
onDeleteBudgetRowHandler(
|
||||
budget.nonstock_id as number,
|
||||
index
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className='text-center py-4 text-gray-500'>
|
||||
Tidak ada data estimasi anggaran.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
onClick={onAddBudgetRowHandler}
|
||||
disabled={filteredNonStockOptions.length == 0}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Item
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<div className='flex flex-row justify-center gap-2 flex-wrap my-6'>
|
||||
{formType !== 'detail' && (
|
||||
<div className='flex flex-row justify-end gap-2'>
|
||||
|
||||
+25
-4
@@ -1,11 +1,21 @@
|
||||
import { StateCreator } from 'zustand';
|
||||
import { DrawerUiSlice } from '@/types/stores';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
export const createFormDrawerUiSlice: StateCreator<
|
||||
DrawerUiSlice,
|
||||
export type ProjectFloockUISlice = {
|
||||
triggerValidate: boolean;
|
||||
toggleValidate: () => void;
|
||||
subscribeValidate: (callback: () => void) => void;
|
||||
isValid: boolean;
|
||||
setIsValid: (v: boolean) => void;
|
||||
subscribeIsValid: (callback: (isValid: boolean) => void) => () => void;
|
||||
};
|
||||
|
||||
export const createProjectFlockUiSlice: StateCreator<
|
||||
ProjectFloockUISlice,
|
||||
[],
|
||||
[],
|
||||
DrawerUiSlice
|
||||
ProjectFloockUISlice
|
||||
> = (set, get, api) => ({
|
||||
// event flag untuk memicu formik validate
|
||||
triggerValidate: false,
|
||||
@@ -38,3 +48,14 @@ export const createFormDrawerUiSlice: StateCreator<
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const useProjectFlockUiStore = create<ProjectFloockUISlice>()(
|
||||
devtools(
|
||||
(...args) => ({
|
||||
...createProjectFlockUiSlice(...args),
|
||||
}),
|
||||
{
|
||||
name: 'ProjectFlockUiStore',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -5,13 +5,11 @@ import { devtools } from 'zustand/middleware';
|
||||
|
||||
import { UIStore } from '@/types/stores';
|
||||
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
||||
import { createFormDrawerUiSlice } from '@/stores/ui/slices/drawer.slice';
|
||||
|
||||
export const useUiStore = create<UIStore>()(
|
||||
devtools(
|
||||
(...args) => ({
|
||||
...createMainUiSlice(...args),
|
||||
...createFormDrawerUiSlice(...args),
|
||||
}),
|
||||
{
|
||||
name: 'UIStore',
|
||||
|
||||
+11
@@ -4,6 +4,7 @@ import { Flock } from '@/types/api/master-data/flock';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
|
||||
export type BaseProjectFlock = {
|
||||
id: number;
|
||||
@@ -30,6 +31,15 @@ export type PeriodFlock = {
|
||||
next_period: number;
|
||||
};
|
||||
|
||||
export type ProjectFlockBudget = {
|
||||
id?: number;
|
||||
project_flock_id?: number;
|
||||
nonstock_id: number;
|
||||
nonstock?: Nonstock;
|
||||
qty: number;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type ProjectFlock = BaseMetadata & BaseProjectFlock;
|
||||
|
||||
export type CreateProjectFlockPayload = {
|
||||
@@ -39,6 +49,7 @@ export type CreateProjectFlockPayload = {
|
||||
fcr_id: number;
|
||||
location_id: number;
|
||||
kandang_ids: number[];
|
||||
project_budgets?: ProjectFlockBudget[];
|
||||
};
|
||||
|
||||
export type UpdateProjectFlockPayload = CreateProjectFlockPayload;
|
||||
|
||||
Vendored
+1
-10
@@ -3,13 +3,4 @@ type MainUiSlice = {
|
||||
setMainDrawerOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
type DrawerUiSlice = {
|
||||
triggerValidate: boolean;
|
||||
toggleValidate: () => void;
|
||||
subscribeValidate: (callback: () => void) => void;
|
||||
isValid: boolean;
|
||||
setIsValid: (v: boolean) => void;
|
||||
subscribeIsValid: (callback: (isValid: boolean) => void) => () => void;
|
||||
};
|
||||
|
||||
export type UIStore = MainUiSlice & DrawerUiSlice;
|
||||
export type UIStore = MainUiSlice;
|
||||
|
||||
Reference in New Issue
Block a user