)}
- {startAdornment && startAdornment}
+ {startAdornment && startAdornment}
-
diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx
index eec312c1..fa80cb4f 100644
--- a/src/components/input/TextInput.tsx
+++ b/src/components/input/TextInput.tsx
@@ -87,7 +87,7 @@ const TextInput = ({
=
+ Yup.object({
+ transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
+
+ flockSource: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ }).required('Flock asal wajib diisi!'),
+
+ flockDestination: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ }).required('Flock tujuan wajib diisi!'),
+
+ totalQuantity: Yup.number()
+ .min(1, 'Jumlah transfer minimal 1')
+ .max(
+ Yup.ref('maxTotalQuantity'),
+ ({ max }) => `Kuantitas maksimal ${max}!`
+ )
+ .required('Jumlah transfer wajib diisi!'),
+
+ maxTotalQuantity: Yup.number()
+ .min(1, 'Jumlah transfer minimal 1')
+ .required('Jumlah transfer wajib diisi!'),
+
+ kandangs: Yup.array()
+ .of(
+ Yup.object({
+ kandang: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ }).required('Kandang wajib diisi!'),
+
+ quantity: Yup.number()
+ .min(0, 'Kuantitas minimal 0!')
+ .max(
+ Yup.ref('maxQuantity'),
+ ({ max }) => `Kuantitas maksimal ${max}!`
+ )
+ .required('Kuantitas wajib diisi!'),
+
+ maxQuantity: Yup.number().min(1).required(), // internal helper field
+ })
+ )
+ .min(1, 'Minimal 1 kandang terisi!')
+ .required('Kandang wajib diisi!'),
+
+ reason: Yup.string().required('Alasan transfer wajib diisi!'),
+ });
+
+export const UpdateTransferToLayingFormSchema = TransferToLayingFormSchema;
+
+export type TransferToLayingFormValues = Yup.InferType<
+ typeof TransferToLayingFormSchema
+>;
diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
new file mode 100644
index 00000000..c4428a72
--- /dev/null
+++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
@@ -0,0 +1,562 @@
+'use client';
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useFormik } from 'formik';
+import { toast } from 'react-hot-toast';
+import useSWR from 'swr';
+
+import { Icon } from '@iconify/react';
+import Button from '@/components/Button';
+import TextInput from '@/components/input/TextInput';
+import SelectInput, {
+ OptionType,
+ // useSelect,
+} from '@/components/input/SelectInput';
+import TextArea from '@/components/input/TextArea';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+
+import {
+ TransferToLayingFormSchema,
+ TransferToLayingFormValues,
+ UpdateTransferToLayingFormSchema,
+} from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import {
+ TransferToLaying,
+ CreateTransferToLayingPayload,
+ UpdateTransferToLayingPayload,
+} from '@/types/api/production/transfer-to-laying';
+import { cn } from '@/lib/helper';
+
+import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
+
+interface TransferToLayingFormProps {
+ type?: 'add' | 'edit' | 'detail';
+ initialValues?: TransferToLaying;
+}
+
+const TransferToLayingForm = ({
+ type = 'add',
+ initialValues,
+}: TransferToLayingFormProps) => {
+ const router = useRouter();
+ const deleteModal = useModal();
+
+ const [formErrorMessage, setFormErrorMessage] = useState('');
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+
+ const createTransferToLayingHandler = useCallback(
+ async (payload: CreateTransferToLayingPayload) => {
+ console.log('Create transfer to laying:', { payload });
+
+ toast.success('Berhasil menambahkan data transfer ke laying!');
+ },
+ [router]
+ );
+
+ const updateTransferToLayingHandler = useCallback(
+ async (
+ transferToLayingId: number,
+ payload: UpdateTransferToLayingPayload
+ ) => {
+ console.log(
+ `Update transfer to laying with ID of ${transferToLayingId}:`,
+ { payload }
+ );
+
+ toast.success('Berhasil mengubah data transfer ke laying!');
+ },
+ [router]
+ );
+
+ const formikInitialValues = useMemo
(() => {
+ return {
+ transfer_date: initialValues?.transfer_date ?? '',
+ flockSource: initialValues?.flock_source
+ ? {
+ value: initialValues?.flock_source.id,
+ label: initialValues?.flock_source.name,
+ }
+ : undefined,
+ flockDestination: initialValues?.flock_destination
+ ? {
+ value: initialValues?.flock_destination.id,
+ label: initialValues?.flock_destination.name,
+ }
+ : undefined,
+ totalQuantity: initialValues?.quantity ?? undefined,
+
+ kandangs: initialValues?.kandangs
+ ? initialValues.kandangs.map((kandang) => ({
+ kandang: {
+ value: kandang.kandang.id,
+ label: kandang.kandang.name,
+ },
+ quantity: kandang.quantity,
+ }))
+ : [],
+
+ reason: initialValues?.reason ?? undefined,
+ };
+ }, [initialValues]);
+
+ const formik = useFormik({
+ initialValues: formikInitialValues,
+ validationSchema:
+ type === 'edit'
+ ? UpdateTransferToLayingFormSchema
+ : TransferToLayingFormSchema,
+ onSubmit: async (values) => {
+ console.log({ values });
+
+ setFormErrorMessage('');
+
+ const transferToLayingPayload: CreateTransferToLayingPayload = {
+ transfer_date: values.transfer_date as string,
+ flock_source_id: values.flockSource?.value as number,
+ flock_destination_id: values.flockDestination?.value as number,
+ totalQuantity: values.totalQuantity as number,
+
+ kandangs: values.kandangs?.map((kandang) => ({
+ kandang_id: kandang.kandang.value,
+ quantity: kandang.quantity,
+ })) as {
+ kandang_id: number;
+ quantity: number;
+ }[],
+
+ reason: values.reason as string,
+ };
+
+ switch (type) {
+ case 'add':
+ await createTransferToLayingHandler(transferToLayingPayload);
+ break;
+
+ case 'edit':
+ await updateTransferToLayingHandler(
+ initialValues?.id as number,
+ transferToLayingPayload
+ );
+ break;
+ }
+ },
+ });
+
+ const { setValues: formikSetValues, values: formikValues } = formik;
+ const { kandangs: kandangsValue } = formikValues;
+
+ const deleteTransferToLayingClickHandler = () => {
+ deleteModal.openModal();
+ };
+
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+
+ deleteModal.closeModal();
+ toast.success('Successfully delete TransferToLaying!');
+
+ setIsDeleteLoading(false);
+ };
+
+ const isRepeaterInputError = (
+ column: keyof TransferToLayingFormValues['kandangs'][0],
+ idx: number
+ ) => {
+ return (
+ formik.touched.kandangs?.[idx]?.[column] &&
+ Boolean(
+ formik.errors.kandangs?.[idx] instanceof Object &&
+ formik.errors.kandangs?.[idx]?.[column]
+ )
+ );
+ };
+
+ const repeaterInputErrorMessage = (
+ column: keyof TransferToLayingFormValues['kandangs'][0],
+ idx: number
+ ) => {
+ return (formik.errors.kandangs?.[idx] as Record)?.[column];
+ };
+
+ // TODO: remove dummy data and use real data
+ // Flock Source
+ // const {
+ // inputValue: flockSourceInputValue,
+ // setInputValue: setFlockSourceInputValue,
+ // options: flockSourceOptions,
+ // isLoadingOptions: isLoadingFlockSourceOptions,
+ // } = useSelect('/transfer-to-laying/production/get-flock-source', 'id', 'name');
+
+ // TODO: remove this dummy data
+ const { data: flockSources, isLoading: isLoadingFlockSourceOptions } = useSWR(
+ 'test',
+ () => TransferToLayingApi.getFlockSource()
+ );
+
+ const flockSourceOptions = isResponseSuccess(flockSources)
+ ? flockSources?.data.map((flockSource) => ({
+ value: flockSource.id,
+ label: flockSource.name,
+ }))
+ : [];
+
+ const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
+ // Get flock source data for total quantity and kandang
+ const flockSource =
+ isResponseSuccess(flockSources) && val !== null
+ ? flockSources.data.find(
+ (item) => item.id === (val as OptionType).value
+ )
+ : undefined;
+
+ // Set total quantity and kandangs
+ if (flockSource) {
+ const formattedKandangs = flockSource.kandangs.map((item) => ({
+ kandang: {
+ value: item.kandang.id,
+ label: item.kandang.name,
+ },
+ quantity: '',
+ maxQuantity: item.quantity,
+ }));
+
+ formik.setFieldValue('totalQuantity', flockSource.totalQuantity);
+ formik.setFieldValue('maxTotalQuantity', flockSource.totalQuantity);
+ formik.setFieldValue('kandangs', formattedKandangs);
+ } else {
+ formik.setFieldValue('totalQuantity', undefined);
+ formik.setFieldValue('kandangs', undefined);
+ formik.setFieldValue('reason', '');
+ }
+
+ formik.setFieldTouched('flockSource', true);
+ formik.setFieldValue('flockSource', val);
+ };
+
+ // TODO: remove dummy data and use real data
+ // Flock Destination
+ // const {
+ // inputValue: flockDestinationInputValue,
+ // setInputValue: setFlockDestinationInputValue,
+ // options: flockDestinationOptions,
+ // isLoadingOptions: isLoadingFlockDestinationOptions,
+ // } = useSelect('/transfer-to-laying/production/get-flock-destination', 'id', 'name');
+
+ // TODO: remove this dummy data
+ const {
+ data: flockDestinations,
+ isLoading: isLoadingFlockDestinationOptions,
+ } = useSWR('test', () => TransferToLayingApi.getFlockSource());
+
+ const flockDestinationOptions = isResponseSuccess(flockDestinations)
+ ? flockDestinations?.data.map((flockDestination) => ({
+ value: flockDestination.id,
+ label: flockDestination.name,
+ }))
+ : [];
+
+ const flockDestinationChangeHandler = (
+ val: OptionType | OptionType[] | null
+ ) => {
+ formik.setFieldTouched('flockDestination', true);
+ formik.setFieldValue('flockDestination', val);
+ };
+
+ useEffect(() => {
+ formikSetValues(formikInitialValues);
+ }, [formikSetValues, formikInitialValues]);
+
+ useEffect(() => {
+ // calculate total quantity if kandangs quantity change
+ if (kandangsValue && kandangsValue.length > 0) {
+ let newTotalQuantity = 0;
+
+ kandangsValue.forEach((item) => {
+ newTotalQuantity += item.quantity as number;
+ });
+
+ formik.setFieldValue('totalQuantity', newTotalQuantity);
+ formik.validateField('totalQuantity');
+ }
+ }, [formikSetValues, kandangsValue]);
+
+ return (
+ <>
+
+
+
+
+ Kembali
+
+
+
+ {type === 'add' && 'Tambah Transfer ke Laying'}
+ {type === 'edit' && 'Edit Transfer ke Laying'}
+ {type === 'detail' && 'Detail Transfer ke Laying'}
+
+
+
+
+
+
+ {type !== 'add' && (
+
+ )}
+ >
+ );
+};
+
+export default TransferToLayingForm;
diff --git a/src/config/constant.ts b/src/config/constant.ts
index 053a50cc..b7afc19a 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -13,7 +13,7 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
},
{
- title: 'Production',
+ title: 'Produksi',
link: '/production',
icon: 'material-symbols:conveyor-belt-outline-rounded',
submenu: [
@@ -32,6 +32,11 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
link: '/production/recording',
icon: 'mdi:clipboard-text',
},
+ {
+ title: 'Transfer ke Laying',
+ link: '/production/transfer-to-laying',
+ icon: 'streamline:transfer-van',
+ },
],
},
@@ -40,11 +45,11 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
link: '/inventory',
icon: 'mdi:warehouse',
submenu: [
- {
- title: 'Product',
- link: '/inventory/product',
- icon: 'mdi:package-variant-closed',
- },
+ // {
+ // title: 'Product',
+ // link: '/inventory/product',
+ // icon: 'mdi:package-variant-closed',
+ // },
{
title: 'Penyesuaian Stok',
link: '/inventory/adjustment',
@@ -126,13 +131,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
{
title: 'Flock',
link: '/master-data/flock',
- icon: 'material-symbols:raven-outline-rounded'
+ icon: 'material-symbols:raven-outline-rounded',
},
],
},
] as const;
-
export const ROWS_OPTIONS = [
{
label: '10',
diff --git a/src/lib/helper.ts b/src/lib/helper.ts
index d85b7b16..0f827e8a 100644
--- a/src/lib/helper.ts
+++ b/src/lib/helper.ts
@@ -27,3 +27,37 @@ export const formatCurrency = (
maximumFractionDigits,
}).format(value);
};
+
+/**
+ * Retrieves a nested value from an object using a dot-delimited key path.
+ * Supports array indexes (e.g., "users.0.name") and returns a default value
+ * if the path does not exist.
+ *
+ * @param obj - The source object to search.
+ * @param path - Dot-delimited key string (e.g., "user.address.city").
+ * @param defaultValue - Optional value to return if the key path is not found.
+ * @returns The value found at the specified path, or the default value.
+ */
+export function getByPath(
+ obj: T,
+ path: string,
+ defaultValue?: D
+): unknown | D {
+ if (obj == null) return defaultValue as D;
+ if (!path) return obj as unknown;
+
+ const segments = path.split('.').filter(Boolean);
+ let cur: { [key: string]: unknown } = obj;
+
+ for (const seg of segments) {
+ if (cur == null) return defaultValue as D;
+ const key: string | number =
+ Array.isArray(cur) && /^\d+$/.test(seg) ? Number(seg) : seg;
+ if (Object(cur) !== cur || !(key in cur)) {
+ return defaultValue as D;
+ }
+ cur = cur[key] as { [key: string]: unknown };
+ }
+
+ return cur as unknown;
+}
diff --git a/src/services/api/production/transfer-to-laying.ts b/src/services/api/production/transfer-to-laying.ts
new file mode 100644
index 00000000..69b4db73
--- /dev/null
+++ b/src/services/api/production/transfer-to-laying.ts
@@ -0,0 +1,187 @@
+import { sleep } from '@/lib/helper';
+import { BaseApiService } from '@/services/api/base';
+import { BaseApiResponse } from '@/types/api/api-general';
+
+import { FlockWithKandangs } from '@/types/api/master-data/flock';
+
+// TODO: delete this dummy data
+const FLOCK_SOURCE_DUMMY_DATA: BaseApiResponse = {
+ code: 200,
+ status: 'success',
+ message: 'Get all projectflocks successfully',
+ meta: {
+ page: 1,
+ limit: 10,
+ total_pages: 1,
+ total_results: 2,
+ },
+ data: [
+ {
+ id: 2,
+ name: 'Flock Banten',
+ totalQuantity: 300,
+ kandangs: [
+ {
+ kandang: {
+ id: 3,
+ name: 'Cikaum 1',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 100,
+ },
+ {
+ kandang: {
+ id: 4,
+ name: 'Cikaum 2',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 150,
+ },
+ {
+ kandang: {
+ id: 5,
+ name: 'Cikaum 3',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 50,
+ },
+ ],
+ },
+
+ {
+ id: 3,
+ name: 'Flock Priangan',
+ totalQuantity: 200,
+ kandangs: [
+ {
+ kandang: {
+ id: 3,
+ name: 'Cikaum 1',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'Priangan',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 100,
+ },
+ {
+ kandang: {
+ id: 4,
+ name: 'Cikaum 2',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 100,
+ },
+ ],
+ },
+ ],
+};
+
+export class TransferToLayingService<
+ T,
+ CreatePayloadGeneric,
+ UpdatePayloadGeneric
+> extends BaseApiService {
+ constructor(basePath: string = '') {
+ super(basePath);
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ async getFlockSource(): Promise<
+ BaseApiResponse | undefined
+ > {
+ try {
+ // const getFlockSourcePath = `${this.basePath}/${flockSourcePath}`;
+ // const getSingleRes = await httpClient(getFlockSourcePath);
+ // return getSingleRes;
+
+ await sleep(500);
+
+ return FLOCK_SOURCE_DUMMY_DATA;
+ } catch (error) {
+ // if (axios.isAxiosError>(error)) {
+ // return error.response?.data;
+ // }
+
+ return undefined;
+ }
+ }
+}
+
+export const TransferToLayingApi = new TransferToLayingService<
+ unknown,
+ unknown,
+ unknown
+>('');
diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css
index 9a148fb4..dadaa2fa 100644
--- a/src/styles/daisyui.css
+++ b/src/styles/daisyui.css
@@ -8,4 +8,8 @@
--step-bg: var(--color-error);
--step-fg: var(--color-error-content);
}
+
+ .table :where(th, td) {
+ vertical-align: top;
+ }
}
diff --git a/src/types/api/master-data/flock.d.ts b/src/types/api/master-data/flock.d.ts
index 3ac5d390..daa7babf 100644
--- a/src/types/api/master-data/flock.d.ts
+++ b/src/types/api/master-data/flock.d.ts
@@ -1,14 +1,26 @@
-import { BaseMetadata } from "@/types/api/api-general";
+import { BaseMetadata } from '@/types/api/api-general';
export type BaseFlock = {
id: number;
name: string;
-}
+};
export type Flock = BaseMetadata & BaseFlock;
export type CreateFlockPayload = {
name: string;
-}
+};
-export type UpdateFlockPayload = CreateFlockPayload;
\ No newline at end of file
+export type UpdateFlockPayload = CreateFlockPayload;
+
+// ---------------------------------------
+// TODO: adjust this later after Transfer to Laying API done
+import { BaseKandang } from '@/types/api/master-data/kandang';
+
+export type FlockWithKandangs = BaseFlock & {
+ totalQuantity: number;
+ kandangs: {
+ kandang: BaseKandang;
+ quantity: number;
+ }[];
+};
diff --git a/src/types/api/production/transfer-to-laying.d.ts b/src/types/api/production/transfer-to-laying.d.ts
new file mode 100644
index 00000000..77d35d8f
--- /dev/null
+++ b/src/types/api/production/transfer-to-laying.d.ts
@@ -0,0 +1,37 @@
+import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general';
+import { Kandang } from '@/types/api/master-data/kandang';
+
+export type BaseTransferToLaying = {
+ id: number;
+ transfer_date: string;
+ flock_source: {
+ id: number;
+ name: string;
+ };
+ flock_destination: {
+ id: number;
+ name: string;
+ };
+ quantity: number;
+ kandangs: {
+ kandang: Kandang;
+ quantity: number;
+ }[];
+ reason: string;
+};
+
+export type TransferToLaying = BaseMetadata & BaseTransferToLaying;
+
+export type CreateTransferToLayingPayload = {
+ transfer_date: string;
+ flock_source_id: number;
+ flock_destination_id: number;
+ totalQuantity: number;
+ kandangs: {
+ kandang_id: number;
+ quantity: number;
+ }[];
+ reason: string;
+};
+
+export type UpdateTransferToLayingPayload = CreateTransferToLayingPayload;