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 Drawer from '@/components/Drawer';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
|
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({
|
export default function ProjectFlockLayout({
|
||||||
children,
|
children,
|
||||||
@@ -13,7 +13,7 @@ export default function ProjectFlockLayout({
|
|||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toggleValidate = useUiStore((s) => s.toggleValidate);
|
const toggleValidate = useProjectFlockUiStore((s) => s.toggleValidate);
|
||||||
|
|
||||||
const isAdd = pathname.endsWith('/add');
|
const isAdd = pathname.endsWith('/add');
|
||||||
const isEdit = pathname.includes('/detail/edit');
|
const isEdit = pathname.includes('/detail/edit');
|
||||||
@@ -22,15 +22,15 @@ export default function ProjectFlockLayout({
|
|||||||
|
|
||||||
const isOpen = isAdd || isEdit || isDetail || isChickin;
|
const isOpen = isAdd || isEdit || isDetail || isChickin;
|
||||||
|
|
||||||
// const childRef = useRef<ProjectFlockFormRef>(null);
|
|
||||||
|
|
||||||
const handleBackdropClick = () => {
|
const handleBackdropClick = () => {
|
||||||
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
const unsub = useProjectFlockUiStore
|
||||||
if (isValid) {
|
.getState()
|
||||||
unsub(); // berhenti listen
|
.subscribeIsValid((isValid) => {
|
||||||
router.push('/production/project-flock');
|
if (isValid) {
|
||||||
}
|
unsub(); // berhenti listen
|
||||||
});
|
router.push('/production/project-flock');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
toggleValidate();
|
toggleValidate();
|
||||||
};
|
};
|
||||||
@@ -39,7 +39,9 @@ export default function ProjectFlockLayout({
|
|||||||
<>
|
<>
|
||||||
{/* List page always rendered */}
|
{/* List page always rendered */}
|
||||||
<div>
|
<div>
|
||||||
<ProjectFlockTable />
|
<ProjectFlockTable
|
||||||
|
refresh={() => !isOpen && router.push('/production/project-flock')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render Drawer only on /add */}
|
{/* 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 { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { CellContext, SortingState } from '@tanstack/react-table';
|
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 toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ const RowOptionsMenu = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectFlockTable = () => {
|
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -154,7 +154,8 @@ const ProjectFlockTable = () => {
|
|||||||
mutate: refreshProjectFlocks,
|
mutate: refreshProjectFlocks,
|
||||||
} = useSWR(
|
} = useSWR(
|
||||||
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
|
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
|
||||||
ProjectFlockApi.getAllFetcher
|
ProjectFlockApi.getAllFetcher,
|
||||||
|
{ revalidateOnMount: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
||||||
@@ -255,6 +256,10 @@ const ProjectFlockTable = () => {
|
|||||||
setIsApproveLoading(false);
|
setIsApproveLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshProjectFlocks();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='w-full p-0 sm:p-4'>
|
<div className='w-full p-0 sm:p-4'>
|
||||||
|
|||||||
@@ -1,52 +1,124 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const ProjectFlockFormSchema = Yup.object({
|
type ProjectFlockFormSchemaType = {
|
||||||
// Flock
|
flock: {
|
||||||
flock: Yup.object({
|
value: number | string;
|
||||||
value: Yup.number().required('ID Flock wajib diisi!'),
|
label: string;
|
||||||
label: Yup.string().required('Nama Flock wajib diisi!'),
|
} | null;
|
||||||
}).nullable(),
|
flock_name: string;
|
||||||
flock_name: Yup.string().required('Nama Flock wajib diisi!'),
|
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[];
|
||||||
|
};
|
||||||
|
|
||||||
// Area
|
export type ProjectFlockBudgetsSchemaType = {
|
||||||
area: Yup.object({
|
nonstock: {
|
||||||
value: Yup.number().required('ID Area wajib diisi!'),
|
value: number | string;
|
||||||
label: Yup.string().required('Nama Area wajib diisi!'),
|
label: string;
|
||||||
}).nullable(),
|
} | null;
|
||||||
area_id: Yup.number()
|
nonstock_id: number | string;
|
||||||
.min(1, 'Area wajib diisi!')
|
qty: number | string;
|
||||||
.required('Area wajib diisi!'),
|
price: number | string;
|
||||||
|
total_price: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
// Kategori
|
export const ProjectFlockBudgetsSchema: Yup.ObjectSchema<ProjectFlockBudgetsSchemaType> =
|
||||||
category_option: Yup.object({
|
Yup.object({
|
||||||
value: Yup.string().required('Nilai Kategori wajib diisi!'),
|
nonstock: Yup.object({
|
||||||
label: Yup.string().required('Label Kategori wajib diisi!'),
|
value: Yup.number().required('ID Nonstock wajib diisi!'),
|
||||||
}).nullable(),
|
label: Yup.string().required('Nama Nonstock wajib diisi!'),
|
||||||
category: Yup.string()
|
}).required('Nonstock wajib diisi!'),
|
||||||
.oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
|
nonstock_id: Yup.number()
|
||||||
.required('Kategori wajib diisi!'),
|
.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!'),
|
||||||
|
});
|
||||||
|
|
||||||
// FCR
|
export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType> =
|
||||||
fcr: Yup.object({
|
Yup.object({
|
||||||
value: Yup.number().required('ID FCR wajib diisi!'),
|
// Flock
|
||||||
label: Yup.string().required('Nama FCR wajib diisi!'),
|
flock: Yup.object({
|
||||||
}).nullable(),
|
value: Yup.number().required('ID Flock wajib diisi!'),
|
||||||
fcr_id: Yup.number().min(1, 'FCR wajib diisi!').required('FCR wajib diisi!'),
|
label: Yup.string().required('Nama Flock wajib diisi!'),
|
||||||
|
}).nullable(),
|
||||||
|
flock_name: Yup.string().required('Nama Flock wajib diisi!'),
|
||||||
|
|
||||||
// Location
|
// Area
|
||||||
location: Yup.object({
|
area: Yup.object({
|
||||||
value: Yup.number().required('ID Lokasi wajib diisi!'),
|
value: Yup.number().required('ID Area wajib diisi!'),
|
||||||
label: Yup.string().required('Nama Lokasi wajib diisi!'),
|
label: Yup.string().required('Nama Area wajib diisi!'),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
location_id: Yup.number()
|
area_id: Yup.number()
|
||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Area wajib diisi!')
|
||||||
.required('Lokasi wajib diisi!'),
|
.required('Area wajib diisi!'),
|
||||||
|
|
||||||
kandang_ids: Yup.array()
|
// Kategori
|
||||||
.of(Yup.number().typeError('Kandang tidak valid!'))
|
category_option: Yup.object({
|
||||||
.min(1, 'Minimal harus ada 1 kandang!')
|
value: Yup.string().required('Nilai Kategori wajib diisi!'),
|
||||||
.required('Kandang wajib diisi!'),
|
label: Yup.string().required('Label Kategori wajib diisi!'),
|
||||||
});
|
}).nullable(),
|
||||||
|
category: Yup.string()
|
||||||
|
.oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
|
||||||
|
.required('Kategori wajib diisi!'),
|
||||||
|
|
||||||
|
// FCR
|
||||||
|
fcr: 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!'),
|
||||||
|
|
||||||
|
// Location
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().required('ID Lokasi wajib diisi!'),
|
||||||
|
label: Yup.string().required('Nama Lokasi wajib diisi!'),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
|
kandang_ids: Yup.array()
|
||||||
|
.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<
|
export type ProjectFlockFormValues = Yup.InferType<
|
||||||
typeof ProjectFlockFormSchema
|
typeof ProjectFlockFormSchema
|
||||||
|
|||||||
@@ -12,13 +12,15 @@ import {
|
|||||||
FlockApi,
|
FlockApi,
|
||||||
KandangApi,
|
KandangApi,
|
||||||
LocationApi,
|
LocationApi,
|
||||||
|
NonstockApi,
|
||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { FormikErrors, useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import useSWR, { KeyedMutator } from 'swr';
|
import useSWR, { KeyedMutator } from 'swr';
|
||||||
import {
|
import {
|
||||||
|
ProjectFlockBudgetsSchemaType,
|
||||||
ProjectFlockFormSchema,
|
ProjectFlockFormSchema,
|
||||||
ProjectFlockFormValues,
|
ProjectFlockFormValues,
|
||||||
UpdateProjectFlockFormSchema,
|
UpdateProjectFlockFormSchema,
|
||||||
@@ -26,6 +28,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
CreateProjectFlockPayload,
|
CreateProjectFlockPayload,
|
||||||
ProjectFlock,
|
ProjectFlock,
|
||||||
|
ProjectFlockBudget,
|
||||||
} from '@/types/api/production/project-flock';
|
} from '@/types/api/production/project-flock';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
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 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 { useUiStore } from '@/stores/ui/ui.store';
|
|
||||||
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
|
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 {
|
interface ProjectFlockFormProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -79,8 +83,8 @@ const ProjectFlockForm = ({
|
|||||||
initialValues?.flock_name?.lastIndexOf(' ')
|
initialValues?.flock_name?.lastIndexOf(' ')
|
||||||
) ?? ''
|
) ?? ''
|
||||||
);
|
);
|
||||||
const subscribeValidate = useUiStore((s) => s.subscribeValidate);
|
const subscribeValidate = useProjectFlockUiStore((s) => s.subscribeValidate);
|
||||||
const setIsValid = useUiStore((s) => s.setIsValid);
|
const setIsValid = useProjectFlockUiStore((s) => s.setIsValid);
|
||||||
|
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const confirmModal = useModal();
|
const confirmModal = useModal();
|
||||||
@@ -158,6 +162,12 @@ const ProjectFlockForm = ({
|
|||||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: optionsNonstock,
|
||||||
|
rawData: nonstocks,
|
||||||
|
isLoadingOptions: isLoadingNonstocks,
|
||||||
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
approvals,
|
approvals,
|
||||||
isLoading: approvalsLoading,
|
isLoading: approvalsLoading,
|
||||||
@@ -359,10 +369,18 @@ const ProjectFlockForm = ({
|
|||||||
>,
|
>,
|
||||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||||
location_id: initialValues?.location?.id ?? 0,
|
location_id: initialValues?.location?.id ?? 0,
|
||||||
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
|
kandang_ids: initialValues?.kandangs?.map(
|
||||||
| number
|
(k: Kandang) => k.id
|
||||||
| undefined
|
) as number[],
|
||||||
)[],
|
project_budgets: [
|
||||||
|
{
|
||||||
|
nonstock: null,
|
||||||
|
nonstock_id: '',
|
||||||
|
qty: '',
|
||||||
|
price: '',
|
||||||
|
total_price: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}, [initialValues, optionsFlock]);
|
}, [initialValues, optionsFlock]);
|
||||||
|
|
||||||
@@ -441,6 +459,15 @@ const ProjectFlockForm = ({
|
|||||||
| number
|
| number
|
||||||
| undefined
|
| undefined
|
||||||
)[],
|
)[],
|
||||||
|
project_budgets: [
|
||||||
|
{
|
||||||
|
nonstock: null,
|
||||||
|
nonstock_id: '',
|
||||||
|
qty: '',
|
||||||
|
price: '',
|
||||||
|
total_price: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
} as ProjectFlockFormValues,
|
} as ProjectFlockFormValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema:
|
validationSchema:
|
||||||
@@ -457,6 +484,13 @@ const ProjectFlockForm = ({
|
|||||||
fcr_id: values.fcr_id as number,
|
fcr_id: values.fcr_id as number,
|
||||||
location_id: values.location_id as number,
|
location_id: values.location_id as number,
|
||||||
kandang_ids: values.kandang_ids 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) {
|
switch (formType) {
|
||||||
@@ -471,8 +505,8 @@ const ProjectFlockForm = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setValues: formikSetValues } = formik;
|
const { setValues: formikSetValues } = formik;
|
||||||
|
|
||||||
// Effect Initial
|
// Effect Initial
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formType == 'detail') {
|
if (formType == 'detail') {
|
||||||
@@ -522,6 +556,33 @@ const ProjectFlockForm = ({
|
|||||||
});
|
});
|
||||||
}, [rowSelection, formikSetValues]);
|
}, [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
|
// Actions handler
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
@@ -539,6 +600,42 @@ const ProjectFlockForm = ({
|
|||||||
setIsDeleteLoading(false);
|
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 (
|
const confirmApprovalHandler = async (
|
||||||
notes: string,
|
notes: string,
|
||||||
approvalAction: 'REJECTED' | 'APPROVED'
|
approvalAction: 'REJECTED' | 'APPROVED'
|
||||||
@@ -562,6 +659,67 @@ const ProjectFlockForm = ({
|
|||||||
setIsApproveLoading(false);
|
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)
|
const selectedPeriod = isResponseSuccess(periodFlocks)
|
||||||
? periodFlocks.data.find((kandang) =>
|
? periodFlocks.data.find((kandang) =>
|
||||||
formik.values.kandang_ids?.includes(kandang.id)
|
formik.values.kandang_ids?.includes(kandang.id)
|
||||||
@@ -569,39 +727,11 @@ const ProjectFlockForm = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
const inputPeriod =
|
const inputPeriod =
|
||||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||||
|
const filteredNonStockOptions = optionsNonstock.filter((nonstock) => {
|
||||||
// expose method validate() ke parent
|
return !(formik.values.project_budgets ?? []).some(
|
||||||
// TODO: Buat Store untuk kirim props formik.isValid ke parent (layout.tsx)
|
(budget) => budget.nonstock_id === nonstock.value
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
|
|
||||||
formik.setTouched(touched, true);
|
|
||||||
}
|
|
||||||
setIsValid(Object.keys(errors).length === 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return unsub;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -699,6 +829,7 @@ const ProjectFlockForm = ({
|
|||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formik.handleReset}
|
||||||
>
|
>
|
||||||
|
{/* Card Informasi Umum */}
|
||||||
<Card
|
<Card
|
||||||
title='Informasi Umum'
|
title='Informasi Umum'
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
@@ -807,12 +938,14 @@ const ProjectFlockForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Card Pilih Kandang */}
|
||||||
<Card
|
<Card
|
||||||
collapsible
|
collapsible
|
||||||
title='Pilih Kandang'
|
title='Pilih Kandang'
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full',
|
wrapper: 'w-full mb-4',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='overflow-x-auto duration-300 ease-in-out'>
|
<div className='overflow-x-auto duration-300 ease-in-out'>
|
||||||
@@ -833,6 +966,214 @@ const ProjectFlockForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</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'>
|
<div className='flex flex-row justify-center gap-2 flex-wrap my-6'>
|
||||||
{formType !== 'detail' && (
|
{formType !== 'detail' && (
|
||||||
<div className='flex flex-row justify-end gap-2'>
|
<div className='flex flex-row justify-end gap-2'>
|
||||||
|
|||||||
+25
-4
@@ -1,11 +1,21 @@
|
|||||||
import { StateCreator } from 'zustand';
|
import { StateCreator } from 'zustand';
|
||||||
import { DrawerUiSlice } from '@/types/stores';
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
|
||||||
export const createFormDrawerUiSlice: StateCreator<
|
export type ProjectFloockUISlice = {
|
||||||
DrawerUiSlice,
|
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) => ({
|
> = (set, get, api) => ({
|
||||||
// event flag untuk memicu formik validate
|
// event flag untuk memicu formik validate
|
||||||
triggerValidate: false,
|
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 { UIStore } from '@/types/stores';
|
||||||
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
||||||
import { createFormDrawerUiSlice } from '@/stores/ui/slices/drawer.slice';
|
|
||||||
|
|
||||||
export const useUiStore = create<UIStore>()(
|
export const useUiStore = create<UIStore>()(
|
||||||
devtools(
|
devtools(
|
||||||
(...args) => ({
|
(...args) => ({
|
||||||
...createMainUiSlice(...args),
|
...createMainUiSlice(...args),
|
||||||
...createFormDrawerUiSlice(...args),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'UIStore',
|
name: 'UIStore',
|
||||||
|
|||||||
+11
@@ -4,6 +4,7 @@ import { Flock } from '@/types/api/master-data/flock';
|
|||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
|
|
||||||
export type BaseProjectFlock = {
|
export type BaseProjectFlock = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -30,6 +31,15 @@ export type PeriodFlock = {
|
|||||||
next_period: number;
|
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 ProjectFlock = BaseMetadata & BaseProjectFlock;
|
||||||
|
|
||||||
export type CreateProjectFlockPayload = {
|
export type CreateProjectFlockPayload = {
|
||||||
@@ -39,6 +49,7 @@ export type CreateProjectFlockPayload = {
|
|||||||
fcr_id: number;
|
fcr_id: number;
|
||||||
location_id: number;
|
location_id: number;
|
||||||
kandang_ids: number[];
|
kandang_ids: number[];
|
||||||
|
project_budgets?: ProjectFlockBudget[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateProjectFlockPayload = CreateProjectFlockPayload;
|
export type UpdateProjectFlockPayload = CreateProjectFlockPayload;
|
||||||
|
|||||||
Vendored
+1
-10
@@ -3,13 +3,4 @@ type MainUiSlice = {
|
|||||||
setMainDrawerOpen: (open: boolean) => void;
|
setMainDrawerOpen: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DrawerUiSlice = {
|
export type UIStore = MainUiSlice;
|
||||||
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;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user