diff --git a/src/app/globals.css b/src/app/globals.css
index 79af241b..97be6978 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -2,6 +2,43 @@
@plugin "daisyui";
@import '../styles/daisyui.css';
+@plugin "daisyui/theme" {
+ name: "lti";
+ default: false;
+ prefersdark: false;
+ color-scheme: "light";
+ --color-base-100: oklch(98% 0.001 106.423);
+ --color-base-200: oklch(97% 0.001 106.424);
+ --color-base-300: oklch(92% 0.003 48.717);
+ --color-base-content: oklch(22.389% 0.031 278.072);
+ --color-primary: oklch(60% 0.126 221.723);
+ --color-primary-content: oklch(100% 0 0);
+ --color-secondary: oklch(52% 0.105 223.128);
+ --color-secondary-content: oklch(100% 0 0);
+ --color-accent: oklch(45% 0.085 224.283);
+ --color-accent-content: oklch(100% 0 0);
+ --color-neutral: oklch(39% 0.07 227.392);
+ --color-neutral-content: oklch(100% 0 0);
+ --color-info: oklch(58% 0.158 241.966);
+ --color-info-content: oklch(100% 0 0);
+ --color-success: oklch(62% 0.194 149.214);
+ --color-success-content: oklch(100% 0 0);
+ --color-warning: oklch(85% 0.199 91.936);
+ --color-warning-content: oklch(0% 0 0);
+ --color-error: oklch(57% 0.245 27.325);
+ --color-error-content: oklch(100% 0 0);
+ --radius-selector: 0rem;
+ --radius-field: 0.25rem;
+ --radius-box: 0.25rem;
+ --size-selector: 0.21875rem;
+ --size-field: 0.1875rem;
+ --border: 1px;
+ --depth: 0;
+ --noise: 0;
+}
+
+
+
:root {
--color-primary: #1f74bf;
}
diff --git a/src/app/inventory/adjustment/detail/layout.tsx b/src/app/inventory/adjustment/detail/layout.tsx
new file mode 100644
index 00000000..b41c70f9
--- /dev/null
+++ b/src/app/inventory/adjustment/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from "@/components/helper/SuspenseHelper"
+
+const Layout = ({
+ children
+}: Readonly<{
+ children: React.ReactNode
+}>) => {
+ return {children}
+}
+
+export default Layout;
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index ef28da38..c19b8a77 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -28,7 +28,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
{children}
diff --git a/src/app/master-data/flock/add/page.tsx b/src/app/master-data/flock/add/page.tsx
new file mode 100644
index 00000000..5ee3958e
--- /dev/null
+++ b/src/app/master-data/flock/add/page.tsx
@@ -0,0 +1,11 @@
+import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
+
+const AddFlock = () => {
+ return (
+
+ );
+}
+
+export default AddFlock;
diff --git a/src/app/master-data/flock/detail/edit/page.tsx b/src/app/master-data/flock/detail/edit/page.tsx
new file mode 100644
index 00000000..c9651727
--- /dev/null
+++ b/src/app/master-data/flock/detail/edit/page.tsx
@@ -0,0 +1,49 @@
+'use client'
+
+import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
+import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
+import { FlockApi } from "@/services/api/master-data";
+import { useRouter, useSearchParams } from "next/navigation";
+import useSWR from "swr";
+
+const FlockEdit = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ // Get Query Params
+ const flockId = searchParams.get('flockId');
+
+ // Fetch Data
+ const { data: flock, isLoading: isLoadingFlock } = useSWR(
+ flockId,
+ (id: number) => FlockApi.getSingle(id)
+ );
+
+ if (!flockId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoadingFlock && (!flock || isResponseError(flock))) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingFlock && (
+
+ )}
+ {!isLoadingFlock && isResponseSuccess(flock) && (
+
+ )}
+
+ );
+}
+
+export default FlockEdit;
\ No newline at end of file
diff --git a/src/app/master-data/flock/detail/layout.tsx b/src/app/master-data/flock/detail/layout.tsx
new file mode 100644
index 00000000..b41c70f9
--- /dev/null
+++ b/src/app/master-data/flock/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from "@/components/helper/SuspenseHelper"
+
+const Layout = ({
+ children
+}: Readonly<{
+ children: React.ReactNode
+}>) => {
+ return {children}
+}
+
+export default Layout;
\ No newline at end of file
diff --git a/src/app/master-data/flock/detail/page.tsx b/src/app/master-data/flock/detail/page.tsx
new file mode 100644
index 00000000..8a805911
--- /dev/null
+++ b/src/app/master-data/flock/detail/page.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+import FlockForm from "@/components/pages/master-data/flock/form/FlockForm";
+import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
+import { FlockApi } from "@/services/api/master-data";
+import { useRouter, useSearchParams } from "next/navigation";
+import useSWR from "swr";
+
+const FlockDetail = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ // Get Query Params
+ const flockId = searchParams.get('flockId');
+
+ // Fetch Data
+ const { data: flock, isLoading: isLoadingFlock } = useSWR(flockId, (id: number) => FlockApi.getSingle(id));
+
+ if(!flockId){
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if(!isLoadingFlock && (!flock || isResponseError(flock))){
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingFlock && (
+
+ )}
+ {!isLoadingFlock && isResponseSuccess(flock) && (
+
+ )}
+
+ );
+}
+
+export default FlockDetail;
\ No newline at end of file
diff --git a/src/app/master-data/flock/page.tsx b/src/app/master-data/flock/page.tsx
new file mode 100644
index 00000000..b317091a
--- /dev/null
+++ b/src/app/master-data/flock/page.tsx
@@ -0,0 +1,11 @@
+import FlockTable from "@/components/pages/master-data/flock/FlocksTable";
+
+const Flock = () => {
+ return (
+
+ );
+}
+
+export default Flock;
diff --git a/src/app/production/project-flock/add/page.tsx b/src/app/production/project-flock/add/page.tsx
new file mode 100644
index 00000000..60141d80
--- /dev/null
+++ b/src/app/production/project-flock/add/page.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm";
+
+const AddProjectFlock = () => {
+ return (
+
+ );
+}
+
+export default AddProjectFlock;
\ No newline at end of file
diff --git a/src/app/production/project-flock/detail/edit/page.tsx b/src/app/production/project-flock/detail/edit/page.tsx
new file mode 100644
index 00000000..858d0ca8
--- /dev/null
+++ b/src/app/production/project-flock/detail/edit/page.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+
+import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm";
+import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
+import { ProjectFlockApi } from "@/services/api/production";
+import { useRouter, useSearchParams } from "next/navigation";
+import useSWR from "swr";
+
+const ProjectFlockEdit = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const projectFlockId = searchParams.get("projectFlockId");
+
+ const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR(
+ projectFlockId,
+ (id: number) => ProjectFlockApi.getSingle(id)
+ );
+
+ if(!projectFlockId){
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if(!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))){
+ router.replace("/404");
+ return;
+ }
+
+ return (
+
+ {isLoadingCostumer &&
}
+ {!isLoadingCostumer && isResponseSuccess(projectFlock) && (
+
+ )}
+
+ )
+}
+
+export default ProjectFlockEdit;
\ No newline at end of file
diff --git a/src/app/production/project-flock/detail/layout.tsx b/src/app/production/project-flock/detail/layout.tsx
new file mode 100644
index 00000000..b41c70f9
--- /dev/null
+++ b/src/app/production/project-flock/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from "@/components/helper/SuspenseHelper"
+
+const Layout = ({
+ children
+}: Readonly<{
+ children: React.ReactNode
+}>) => {
+ return {children}
+}
+
+export default Layout;
\ No newline at end of file
diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx
new file mode 100644
index 00000000..5efe83d8
--- /dev/null
+++ b/src/app/production/project-flock/detail/page.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+
+import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm";
+import { isResponseError, isResponseSuccess } from "@/lib/api-helper";
+import { ProjectFlockApi } from "@/services/api/production";
+import { useRouter, useSearchParams } from "next/navigation";
+import useSWR from "swr";
+
+const ProjectFlockDetail = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const projectFlockId = searchParams.get("projectFlockId");
+
+ const { data: projectFlock, isLoading: isLoadingCostumer } = useSWR(
+ projectFlockId,
+ (id: number) => ProjectFlockApi.getSingle(id)
+ );
+
+ if(!projectFlockId){
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ if(!isLoadingCostumer && (!projectFlock || isResponseError(projectFlock))){
+ router.replace("/404");
+ return;
+ }
+
+ return (
+
+ {isLoadingCostumer &&
}
+ {!isLoadingCostumer && isResponseSuccess(projectFlock) && (
+
+ )}
+
+ )
+}
+
+export default ProjectFlockDetail;
\ No newline at end of file
diff --git a/src/app/production/project-flock/page.tsx b/src/app/production/project-flock/page.tsx
new file mode 100644
index 00000000..fdb8775d
--- /dev/null
+++ b/src/app/production/project-flock/page.tsx
@@ -0,0 +1,12 @@
+import ProjectFlockForm from "@/components/pages/production/project-flock/form/ProjectFlockForm"
+import ProjectFlockTable from "@/components/pages/production/project-flock/ProjectFlockTable";
+
+const ProjectFlock = () => {
+ return (
+
+ );
+}
+
+export default ProjectFlock;
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index e901b765..7cad5b58 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -45,7 +45,7 @@ const Button = ({
'btn-warning': color === 'warning',
'btn-error': color === 'error',
},
- 'h-fit justify-center items-center gap-2 rounded-xl p-2 text-base transition-all'
+ 'h-fit justify-center items-center gap-2 rounded p-2 text-base transition-all'
);
return (
diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx
index 43a3f622..b35ad7dd 100644
--- a/src/components/input/SelectInput.tsx
+++ b/src/components/input/SelectInput.tsx
@@ -1,12 +1,6 @@
'use client';
-import {
- ComponentType,
- ReactNode,
- useEffect,
- useMemo,
- useState,
-} from 'react';
+import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
import Select, {
OptionProps,
GroupBase,
@@ -98,10 +92,7 @@ const SelectInput = (props: SelectInputProps) => {
return { ...base, IndicatorSeparator: () => null };
}, [isAnimated]);
- const internalInputChangeHandler = (
- val: string,
- meta: InputActionMeta
- ) => {
+ const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') setInternalInputValue(val);
if (meta.action === 'menu-close') setInternalInputValue('');
};
@@ -113,9 +104,7 @@ const SelectInput = (props: SelectInputProps) => {
const SelectComponent = createables ? CreatableSelect : Select;
/** 🎯 handleChange tanpa any */
- const handleChange = (
- val: MultiValue | SingleValue
- ): void => {
+ const handleChange = (val: MultiValue | SingleValue | null): void => {
if (!val) {
onChange?.(null);
return;
@@ -145,15 +134,15 @@ const SelectInput = (props: SelectInputProps) => {
>
{label}
{required && (
-
- *
+
+ *
)}
)}
>
- instanceId="select"
+ instanceId='select'
value={value ?? (isMulti ? [] : null)}
onChange={handleChange}
options={options}
@@ -225,9 +214,9 @@ const SelectInput = (props: SelectInputProps) => {
}}
/>
- {isError && {errorMessage}
}
+ {isError && {errorMessage}
}
{!isError && bottomLabel && (
- {bottomLabel}
+ {bottomLabel}
)}
);
diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx
index 1bb1692d..9a19ced1 100644
--- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx
+++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx
@@ -13,7 +13,7 @@ import toast from 'react-hot-toast';
import {
InventoryAdjustmentFormSchema,
InventoryAdjustmentFormValues,
-} from './InventoryAdjustmentForm.schema';
+} from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema';
import useSWR from 'swr';
import {
ProductApi,
diff --git a/src/components/pages/master-data/customer/form/CustomerForm.tsx b/src/components/pages/master-data/customer/form/CustomerForm.tsx
index 533e0c38..ac848834 100644
--- a/src/components/pages/master-data/customer/form/CustomerForm.tsx
+++ b/src/components/pages/master-data/customer/form/CustomerForm.tsx
@@ -11,7 +11,7 @@ import {
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
-import { CustomerFormSchema, CustomerFormValues, UpdateCustomerFormSchema } from './CustomerForm.schema';
+import { CustomerFormSchema, CustomerFormValues, UpdateCustomerFormSchema } from '@/components/pages/master-data/customer/form/CustomerForm.schema';
import { useFormik } from 'formik';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx
new file mode 100644
index 00000000..b0684a1a
--- /dev/null
+++ b/src/components/pages/master-data/flock/FlocksTable.tsx
@@ -0,0 +1,278 @@
+'use client';
+
+import { CellContext, ColumnDef } from '@tanstack/react-table';
+import { Flock } from '@/types/api/master-data/flock';
+import { cn } from '@/lib/helper';
+import Button from '@/components/Button';
+import { Icon } from '@iconify/react';
+import { useTableFilter } from '@/services/hooks/useTableFilter';
+import { useState } from 'react';
+import useSWR from 'swr';
+import { FlockApi } from '@/services/api/master-data';
+import { useModal } from '@/components/Modal';
+import RowDropdownOptions from '@/components/table/RowDropdownOptions';
+import RowCollapseOptions from '@/components/table/RowCollapseOptions';
+import toast from 'react-hot-toast';
+import DebouncedTextInput from '@/components/input/DebouncedTextInput';
+import SelectInput, { OptionType } from '@/components/input/SelectInput';
+import { ROWS_OPTIONS } from '@/config/constant';
+import Table from '@/components/Table';
+import { isResponseSuccess } from '@/lib/api-helper';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+
+const RowsOptions = ({
+ type = 'dropdown',
+ props,
+ deleteClickHandler,
+}: {
+ type: 'dropdown' | 'collapse';
+ props: CellContext;
+ deleteClickHandler: () => void;
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const FlockTable = () => {
+ const {
+ state: tableFilterState,
+ updateFilter,
+ setPage,
+ setPageSize,
+ toQueryString: getTableFilterQueryString,
+ } = useTableFilter({
+ initial: { search: '', nameSort: '' },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ nameSort: 'sort_name',
+ },
+ });
+
+ // Fetch Data
+ const {
+ data: flocks,
+ isLoading,
+ mutate: refreshFlocks,
+ } = useSWR(
+ `${FlockApi.basePath}${getTableFilterQueryString()}`,
+ FlockApi.getAllFetcher
+ );
+
+ // State
+ const deleteModal = useModal();
+ const [selectedFlock, setSelectedFlock] = useState(
+ undefined
+ );
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+
+ // Columns Definition
+ const flocksColumns: ColumnDef[] = [
+ {
+ header: '#',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'name',
+ header: 'Nama',
+ },
+ {
+ accessorKey: 'created_at',
+ header: 'Dibuat pada',
+ cell: (props) =>
+ new Date(props.row.original.created_at).toLocaleDateString(),
+ },
+ {
+ header: 'Aksi',
+ cell: (props) => {
+ const currentPageSize = props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ const deleteClickHandler = () => {
+ setSelectedFlock(props.row.original);
+ deleteModal.openModal();
+ };
+
+ return (
+ <>
+ {currentPageSize > 2 && (
+
+
+
+ )}
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ];
+
+ // Handler
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+
+ await FlockApi.delete(selectedFlock?.id as number);
+ refreshFlocks();
+
+ deleteModal.closeModal();
+ toast.success('Successfully delete Flock!');
+ setIsDeleteLoading(false);
+ };
+ const searchChangeHandler = (e: React.ChangeEvent) => {
+ updateFilter('search', e.target.value);
+ };
+ const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+ setPageSize(newVal.value as number);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ data={isResponseSuccess(flocks) ? flocks?.data : []}
+ columns={flocksColumns}
+ pageSize={tableFilterState.pageSize}
+ page={isResponseSuccess(flocks) ? flocks?.meta?.page : 0}
+ totalItems={
+ isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0
+ }
+ onPageChange={setPage}
+ isLoading={isLoading}
+ className={{
+ containerClassName: cn({
+ 'mb-20': isResponseSuccess(flocks) && flocks?.data?.length === 0,
+ }),
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
+ }}
+ />
+
+
+ >
+ );
+};
+
+export default FlockTable;
\ No newline at end of file
diff --git a/src/components/pages/master-data/flock/form/FlockForm.schema.ts b/src/components/pages/master-data/flock/form/FlockForm.schema.ts
new file mode 100644
index 00000000..76445610
--- /dev/null
+++ b/src/components/pages/master-data/flock/form/FlockForm.schema.ts
@@ -0,0 +1,14 @@
+import * as Yup from 'yup';
+
+export const FlockFormSchema = Yup.object({
+ name: Yup.string()
+ .required('Nama wajib diisi!')
+ .matches(
+ /^[\p{L}\p{N}\s]+$/u,
+ 'Nama tidak boleh mengandung simbol'
+ ),
+});
+
+export const UpdateFlockFormSchema = FlockFormSchema;
+
+export type FlockFormValues = Yup.InferType;
diff --git a/src/components/pages/master-data/flock/form/FlockForm.tsx b/src/components/pages/master-data/flock/form/FlockForm.tsx
new file mode 100644
index 00000000..cc227fa6
--- /dev/null
+++ b/src/components/pages/master-data/flock/form/FlockForm.tsx
@@ -0,0 +1,217 @@
+'use client'
+
+import { useModal } from '@/components/Modal';
+import { FlockApi } from '@/services/api/master-data';
+import { Flock } from '@/types/api/master-data/flock';
+import { useRouter } from 'next/navigation';
+import { useEffect, useMemo, useState } from 'react';
+import { FlockFormSchema, FlockFormValues, UpdateFlockFormSchema } from '@/components/pages/master-data/flock/form/FlockForm.schema';
+import { useFormik } from 'formik';
+import Button from '@/components/Button';
+import { Icon } from '@iconify/react';
+import TextInput from '@/components/input/TextInput';
+import { cn } from '@/lib/helper';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+
+interface FlockCustomProps {
+ formType?: 'add' | 'edit' | 'detail';
+ initialValues?: Flock;
+}
+
+const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
+ const router = useRouter();
+ const deleteModal = useModal();
+
+ // State
+ const [flockFormErrorMessage, setFlockFormErrorMessage] = useState('');
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+
+ // Handler
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+
+ await FlockApi.delete(initialValues?.id as number);
+
+ deleteModal.closeModal();
+ setIsDeleteLoading(false);
+ router.push('/master-data/flock');
+ };
+
+ // Initital Value
+ const formikInitialValue = useMemo(() => {
+ return {
+ name: initialValues?.name ?? '',
+ };
+ }, [initialValues]);
+
+ // Formik
+ const formik = useFormik({
+ initialValues: formikInitialValue,
+ enableReinitialize: true,
+ validationSchema: formType === 'edit' ? UpdateFlockFormSchema : FlockFormSchema,
+ onSubmit: async (values) => {
+ // reset error message
+ setFlockFormErrorMessage('');
+
+ // create payload
+ const payload = {
+ name: values.name,
+ };
+
+ // cek type form yang disubmit
+ switch (formType) {
+ case 'add':
+ await FlockApi.create(payload);
+ break;
+ case 'edit':
+ await FlockApi.update(initialValues?.id as number, payload);
+ break;
+ default:
+ break;
+ }
+
+ router.push('/master-data/flock');
+ },
+ });
+
+ // Initialize Formik
+ const { setValues: formikSetValues } = formik;
+ useEffect(() => {
+ formikSetValues(formikInitialValue);
+ }, [formikSetValues, formikInitialValue]);
+
+ // Render
+ return (
+ <>
+
+
+
+
+
+ {formType === 'add' && 'Tambah Flock'}
+ {formType === 'edit' && 'Ubah Flock'}
+ {formType === 'detail' && 'Detail Flock'}
+
+
+
+
+
+ {formType !== 'add' && (
+
+ )}
+ >
+ );
+};
+
+export default FlockForm;
diff --git a/src/components/pages/master-data/supplier/form/SupplierForm.tsx b/src/components/pages/master-data/supplier/form/SupplierForm.tsx
index 74c4da27..e400ead2 100644
--- a/src/components/pages/master-data/supplier/form/SupplierForm.tsx
+++ b/src/components/pages/master-data/supplier/form/SupplierForm.tsx
@@ -15,7 +15,7 @@ import {
SupplierFormSchema,
SupplierFormValues,
UpdateSupplierFormSchema,
-} from './SupplierForm.schema';
+} from '@/components/pages/master-data/supplier/form/SupplierForm.schema';
import { useFormik } from 'formik';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { Icon } from '@iconify/react';
diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx
new file mode 100644
index 00000000..af057fb8
--- /dev/null
+++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx
@@ -0,0 +1,579 @@
+'use client';
+
+import Button from '@/components/Button';
+import DebouncedTextInput from '@/components/input/DebouncedTextInput';
+import SelectInput, { OptionType } from '@/components/input/SelectInput';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import Table from '@/components/Table';
+import RowCollapseOptions from '@/components/table/RowCollapseOptions';
+import RowDropdownOptions from '@/components/table/RowDropdownOptions';
+import { ROWS_OPTIONS } from '@/config/constant';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { cn } from '@/lib/helper';
+import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
+import { ProjectFlockApi } from '@/services/api/production';
+import { useTableFilter } from '@/services/hooks/useTableFilter';
+import { BaseApiResponse } from '@/types/api/api-general';
+import { Kandang } from '@/types/api/master-data/kandang';
+import { ProjectFlock } from '@/types/api/production/project-flock';
+import { Icon } from '@iconify/react';
+import {
+ CellContext,
+ ColumnDef,
+ SortingState,
+} from '@tanstack/react-table';
+import { ChangeEventHandler, useState } from 'react';
+import toast from 'react-hot-toast';
+import useSWR from 'swr';
+
+const RowOptionsMenu = ({
+ type = 'dropdown',
+ props,
+ deleteClickHandler,
+}: {
+ type: 'dropdown' | 'collapse';
+ props: CellContext;
+ deleteClickHandler: () => void;
+}) => {
+ return (
+
+
+ {/* */}
+
+
+ );
+};
+
+const ProjectFlockTable = () => {
+ const {
+ state: tableFilterState,
+ updateFilter,
+ setPage,
+ setPageSize,
+ toQueryString: getTableFilterQueryString,
+ } = useTableFilter({
+ initial: {
+ search: '',
+ areaFilter: '',
+ locationFilter: '',
+ kandangFilter: '',
+ periodFilter: '',
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ search: 'search',
+ areaFilter: 'area_id',
+ locationFilter: 'location_id',
+ kandangFilter: 'kandang_id',
+ periodFilter: 'period',
+ },
+ });
+ const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
+ const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
+ const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
+
+ const [selectedArea, setSelectedArea] = useState(null);
+ const [selectedLocation, setSelectedLocation] = useState(
+ null
+ );
+ const [selectedKandang, setSelectedKandang] = useState(
+ null
+ );
+ const [periodInputValue, setPeriodInputValue] = useState(null);
+
+ // Fetch Data
+ const {
+ data: projectFlocks,
+ isLoading,
+ mutate: refreshProjectFlocks,
+ } = useSWR(
+ `${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
+ ProjectFlockApi.getAllFetcher
+ );
+
+ const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
+ search: areaSelectInputValue,
+ limit: '100',
+ }).toString()}`;
+ const {
+ data: areas,
+ isLoading: isLoadingAreas,
+ } = useSWR(areaUrl, AreaApi.getAllFetcher);
+
+ const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
+ search: locationSelectInputValue,
+ area_id: selectedArea != null ? selectedArea.value.toString() : '',
+ limit: '100',
+ }).toString()}`;
+ const {
+ data: locations,
+ isLoading: isLoadingLocations,
+ } = useSWR(locationUrl, LocationApi.getAllFetcher);
+
+ const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
+ search: kandangSelectInputValue,
+ location_id:
+ selectedLocation != null ? selectedLocation.value.toString() : '',
+ limit: '100',
+ }).toString()}`;
+ const {
+ data: kandangs,
+ isLoading: isLoadingKandang,
+ } = useSWR(kandangUrl, KandangApi.getAllFetcher);
+
+ // Data to Options Mapping
+ const optionsArea = isResponseSuccess(areas)
+ ? areas?.data.map((area) => ({
+ value: area.id,
+ label: area.name,
+ }))
+ : [];
+ const optionsKandang = isResponseSuccess(kandangs)
+ ? kandangs?.data.map((kandang) => ({
+ value: kandang.id,
+ label: kandang.name,
+ }))
+ : [];
+ const optionsLocation = isResponseSuccess(locations)
+ ? locations?.data.map((location) => ({
+ value: location.id,
+ label: location.name,
+ }))
+ : [];
+
+ // State
+ const [sorting, setSorting] = useState([]);
+ const [selectedProjectFlock, setSelectedProjectFlock] =
+ useState();
+ const deleteModal = useModal();
+ const confirmModal = useModal();
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [selectedIds, setSelectedIds] = useState([]);
+ const [selectedFlocks, setSelectedFlocks] = useState([]);
+ const [isApproveLoading, setIsApproveLoading] = useState(false);
+
+ // Columns
+ const projectFlocksColumns: ColumnDef[] = [
+ {
+ id: 'select',
+ header: () => {
+ const allSelected =
+ isResponseSuccess(projectFlocks) &&
+ projectFlocks.data.length > 0 &&
+ selectedIds.length === projectFlocks.data.length;
+
+ return (
+ handleSelectAll(e.target.checked)}
+ />
+ );
+ },
+ cell: (props) => {
+ const id = props.row.original.id;
+ const isChecked = selectedIds.includes(id);
+
+ return (
+ handleSelectRow(id, e.target.checked)}
+ />
+ );
+ },
+ },
+
+ {
+ accessorKey: 'flock.name',
+ header: 'Flock',
+ },
+ {
+ accessorKey: 'area.name',
+ header: 'Area',
+ },
+ {
+ accessorKey: 'location.name',
+ header: 'Lokasi',
+ },
+ {
+ accessorKey: 'fcr.name',
+ header: 'FCR',
+ },
+ {
+ accessorKey: 'category',
+ header: 'Kategori',
+ },
+ {
+ header: 'Kandang',
+ cell: (props) => {
+ const kandang = props.row.original.kandangs;
+ if (kandang) {
+ const kandangNames = kandang.map((k: Kandang) => k.name);
+ return (
+
+ {kandangNames.length > 0 ? kandangNames.join(', ') : 'Tidak ada'}
+
+ );
+ } else {
+ return '-';
+ }
+ },
+ },
+ {
+ accessorKey: 'period',
+ header: 'Periode',
+ },
+ {
+ accessorKey: 'created_at',
+ header: 'Dibuat pada',
+ cell: (props) =>
+ new Date(props.row.original.created_at).toLocaleDateString(),
+ },
+ {
+ header: 'Aksi',
+ cell: (props) => {
+ const currentPageSize = props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ const deleteClickHandler = () => {
+ setSelectedProjectFlock(props.row.original);
+ deleteModal.openModal();
+ };
+
+ return (
+ <>
+ {currentPageSize > 2 && (
+
+
+
+ )}
+
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ];
+
+ // Handler
+ const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+ setPageSize(newVal.value as number);
+ };
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+
+ await ProjectFlockApi.delete(selectedProjectFlock?.id as number);
+ refreshProjectFlocks();
+
+ deleteModal.closeModal();
+ toast.success('Successfully delete Project Flock!');
+ setIsDeleteLoading(false);
+ };
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ updateFilter('search', e.target.value);
+ };
+ const handleSelectAll = (checked: boolean) => {
+ if (checked && isResponseSuccess(projectFlocks)) {
+ const allIds = projectFlocks.data.map((item) => item.id);
+ setSelectedIds(allIds);
+ setSelectedFlocks(projectFlocks.data);
+ } else {
+ setSelectedIds([]);
+ setSelectedFlocks([]);
+ }
+ };
+
+ const handleSelectRow = (id: number, checked: boolean) => {
+ if (!isResponseSuccess(projectFlocks)) return;
+
+ const targetFlock = projectFlocks.data.find((item) => item.id === id);
+
+ if (!targetFlock) return;
+
+ if (checked) {
+ setSelectedIds((prev) => [...prev, id]);
+ setSelectedFlocks((prev) => [...(prev || []), targetFlock]);
+ } else {
+ setSelectedIds((prev) => prev.filter((val) => val !== id));
+ setSelectedFlocks((prev) =>
+ (prev || []).filter((flock) => flock.id !== id)
+ );
+ }
+ };
+
+ const confirmationModalApproveClickHandler = async () => {
+ setIsApproveLoading(true);
+ const approveProjectFlockRes = await ProjectFlockApi.customRequest<
+ BaseApiResponse,
+ 'POST'
+ >(`/approve`, {
+ method: 'POST',
+ payload: 'POST',
+ params: {
+ ids: selectedFlocks.map((flock) => flock.id).join(','),
+ },
+ });
+
+ if (isResponseSuccess(approveProjectFlockRes)) {
+ toast.success('Project Flock berhasil di-approve!');
+ confirmModal.closeModal();
+ }
+ if (isResponseError(approveProjectFlockRes)) {
+ toast.error(approveProjectFlockRes?.message as string);
+ confirmModal.closeModal();
+ }
+ setIsApproveLoading(false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setSelectedArea(val as OptionType);
+ updateFilter(
+ 'areaFilter',
+ (val as OptionType)?.value.toString()
+ );
+ }}
+ onInputChange={setAreaSelectInputValue}
+ isClearable
+ />
+ {
+ setSelectedLocation(val as OptionType);
+ updateFilter(
+ 'locationFilter',
+ (val as OptionType)?.value.toString()
+ );
+ }}
+ onInputChange={setLocationSelectInputValue}
+ isClearable
+ />
+ {
+ setSelectedKandang(val as OptionType);
+ updateFilter(
+ 'kandangFilter',
+ (val as OptionType)?.value.toString()
+ );
+ }}
+ onInputChange={setKandangSelectInputValue}
+ isClearable
+ />
+ {
+ setPeriodInputValue(parseInt(e.target.value));
+ updateFilter('periodFilter', e.target.value);
+ }}
+ />
+
+
+
+
+
+ data={isResponseSuccess(projectFlocks) ? projectFlocks?.data : []}
+ columns={projectFlocksColumns}
+ pageSize={tableFilterState.pageSize}
+ page={
+ isResponseSuccess(projectFlocks) ? projectFlocks?.meta?.page : 0
+ }
+ totalItems={
+ isResponseSuccess(projectFlocks)
+ ? projectFlocks?.meta?.total_results
+ : 0
+ }
+ onPageChange={setPage}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ className={{
+ containerClassName: cn({
+ 'mb-20':
+ isResponseSuccess(projectFlocks) &&
+ projectFlocks?.data?.length === 0,
+ }),
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
+ }}
+ />
+
+
+
+
+
+ 0
+ ? `Apakah anda yakin ingin approve Project Flock berikut? (${selectedFlocks
+ .map(
+ (flock) =>
+ `${flock.flock?.name ?? '(Tanpa nama)'} - ${
+ flock.area?.name ?? '-'
+ }`
+ )
+ .join(', ')})`
+ : 'Tidak ada Project Flock yang dipilih.'
+ }
+ secondaryButton={{
+ text: 'Tidak',
+ }}
+ primaryButton={{
+ text: 'Ya',
+ color: 'success',
+ onClick: confirmationModalApproveClickHandler,
+ isLoading: isApproveLoading,
+ }}
+ />
+ >
+ );
+};
+
+export default ProjectFlockTable;
diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
new file mode 100644
index 00000000..162282fb
--- /dev/null
+++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
@@ -0,0 +1,61 @@
+import * as Yup from 'yup';
+
+export const ProjectFlockFormSchema = Yup.object({
+ // Flock
+ flock: Yup.object({
+ value: Yup.number().required('ID Flock wajib diisi!'),
+ label: Yup.string().required('Nama Flock wajib diisi!'),
+ }).nullable(),
+ flock_id: Yup.number()
+ .min(1, 'Flock wajib diisi!')
+ .required('Flock wajib diisi!'),
+
+ // Area
+ area: Yup.object({
+ value: Yup.number().required('ID Area wajib diisi!'),
+ label: Yup.string().required('Nama Area wajib diisi!'),
+ }).nullable(),
+ area_id: Yup.number()
+ .min(1, 'Area wajib diisi!')
+ .required('Area wajib diisi!'),
+
+ // Kategori
+ category_option: Yup.object({
+ value: Yup.string().required('Nilai Kategori 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!'),
+
+ period: Yup.number()
+ .required('Periode wajib diisi!')
+ .typeError('Periode harus berupa angka')
+ .min(1, 'Minimal periode adalah 1'),
+
+ kandang_ids: Yup.array()
+ .of(Yup.number().typeError('Kandang tidak valid!'))
+ .min(1, 'Minimal harus ada 1 kandang!')
+ .required('Kandang wajib diisi!'),
+});
+
+export type ProjectFlockFormValues = Yup.InferType<
+ typeof ProjectFlockFormSchema
+>;
+
+export const UpdateProjectFlockFormSchema = ProjectFlockFormSchema;
diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
new file mode 100644
index 00000000..ccc3fadc
--- /dev/null
+++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
@@ -0,0 +1,814 @@
+'use client';
+
+import Button from '@/components/Button';
+import SelectInput, { OptionType } from '@/components/input/SelectInput';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import {
+ AreaApi,
+ FcrApi,
+ FlockApi,
+ KandangApi,
+ LocationApi,
+} from '@/services/api/master-data';
+import { Icon } from '@iconify/react';
+import { useFormik } from 'formik';
+import { useRouter } from 'next/navigation';
+import { useEffect, useMemo, useState } from 'react';
+import useSWR from 'swr';
+import {
+ ProjectFlockFormSchema,
+ ProjectFlockFormValues,
+ UpdateProjectFlockFormSchema,
+} from '@/components/pages/production/project-flock/form/ProjectFlockForm.schema';
+import {
+ CreateProjectFlockPayload,
+ PeriodFlock,
+ ProjectFlock,
+} from '@/types/api/production/project-flock';
+import toast from 'react-hot-toast';
+import TextInput from '@/components/input/TextInput';
+import { Kandang } from '@/types/api/master-data/kandang';
+import Collapse from '@/components/Collapse';
+import { ProjectFlockApi } from '@/services/api/production';
+import { BaseApiResponse } from '@/types/api/api-general';
+import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+
+interface ProjectFlockFormProps {
+ formType?: 'add' | 'edit' | 'detail';
+ initialValues?: ProjectFlock;
+}
+
+const ProjectFlockForm = ({
+ formType = 'add',
+ initialValues,
+}: ProjectFlockFormProps) => {
+ // State
+ const router = useRouter();
+ const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] =
+ useState('');
+ const [selectedArea, setSelectedArea] = useState('');
+
+ const [selectedLocation, setSelectedLocation] = useState('');
+ const [disabledLocation, setDisabledLocation] = useState(true);
+ const [optionsLocation, setOptionsLocation] = useState([]);
+
+ const [openSelectKandangs, setOpenSelectKandangs] = useState(
+ initialValues?.kandangs && initialValues?.kandangs?.length > 0
+ );
+ const [optionsKandang, setOptionsKandang] = useState(
+ initialValues?.kandangs ?? []
+ );
+
+ const [selectedFlock, setSelectedFlock] = useState(
+ initialValues?.flock?.id ?? 0
+ );
+
+ const deleteModal = useModal();
+ const confirmModal = useModal();
+
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [isApproveLoading, setIsApproveLoading] = useState(false);
+
+ // Fetch Data
+ const flockUrl = `${FlockApi.basePath}?${new URLSearchParams({
+ search: '',
+ }).toString()}`;
+ const { data: flocks, isLoading: isLoadingFlocks } = useSWR(
+ flockUrl,
+ FlockApi.getAllFetcher
+ );
+
+ const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
+ search: '',
+ }).toString()}`;
+ const { data: areas, isLoading: isLoadingAreas } = useSWR(
+ areaUrl,
+ AreaApi.getAllFetcher
+ );
+
+ const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
+ search: '',
+ area_id: selectedArea,
+ }).toString()}`;
+ const { data: locations, isLoading: isLoadingLocations } = useSWR(
+ locationUrl,
+ LocationApi.getAllFetcher
+ );
+
+ const fcrUrl = `${FcrApi.basePath}?${new URLSearchParams({
+ search: '',
+ }).toString()}`;
+ const { data: fcrs, isLoading: isLoadingFcrs } = useSWR(
+ fcrUrl,
+ FcrApi.getAllFetcher
+ );
+
+ const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
+ search: '',
+ location_id: selectedLocation == '' ? '0' : selectedLocation,
+ }).toString()}`;
+ const { data: kandang, isLoading: isLoadingKandang } = useSWR(
+ kandangUrl,
+ KandangApi.getAllFetcher
+ );
+
+ const getPeriodFlocksUrl = `flocks/${selectedFlock}/periods`;
+
+ const { data: periodFlocks, isLoading: isLoadingPeriodFlocks } = useSWR(
+ getPeriodFlocksUrl,
+ () =>
+ ProjectFlockApi.customRequest, 'GET'>(
+ getPeriodFlocksUrl,
+ { method: 'GET' }
+ )
+ );
+
+ // Map Data to Options
+ const optionsArea = isResponseSuccess(areas)
+ ? areas?.data.map((area) => ({
+ value: area.id,
+ label: area.name,
+ }))
+ : [];
+ const optionsFcr = isResponseSuccess(fcrs)
+ ? fcrs?.data.map((fcr) => ({
+ value: fcr.id,
+ label: fcr.name,
+ }))
+ : [];
+ const optionsFlock = isResponseSuccess(flocks)
+ ? flocks?.data.map((flock) => ({
+ value: flock.id,
+ label: flock.name,
+ }))
+ : [];
+
+ useEffect(() => {
+ if (isResponseSuccess(locations)) {
+ const options = locations.data.map((location) => ({
+ value: location.id,
+ label: location.name,
+ }));
+ setOptionsLocation(options);
+ }
+ }, [locations, setSelectedLocation]);
+
+ useEffect(() => {
+ if (isResponseSuccess(kandang)) {
+ if (selectedLocation) {
+ setOptionsKandang(kandang.data);
+ setOpenSelectKandangs(true);
+ } else {
+ setOptionsKandang([]);
+ setOpenSelectKandangs(false);
+ formik.setFieldValue('kandang_ids', []);
+ }
+ }
+ }, [kandang]);
+
+ // Options Handler
+ const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
+ formik.setFieldValue('area_id', (val as OptionType)?.value);
+ formik.setFieldValue('area', val);
+
+ formik.setFieldTouched('area_id', true);
+
+ setSelectedArea((val as OptionType)?.value as string);
+ setSelectedLocation('');
+ const disabled = (val as OptionType)?.value == null;
+ setDisabledLocation(disabled);
+
+ formik.setFieldValue('location', null);
+ formik.setFieldValue('location_id', 0);
+ formik.setFieldTouched('location', false);
+ formik.setFieldTouched('location_id', false);
+ };
+
+ const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
+ setSelectedLocation((val as OptionType)?.value as string);
+ optionChangeHandler(val, 'location');
+ formik.setFieldValue('kandang_ids', []);
+ };
+
+ const optionChangeHandler = (
+ val: OptionType | OptionType[] | null,
+ inputName: string
+ ) => {
+ formik.setFieldValue(inputName, val);
+ formik.setFieldValue(
+ `${inputName}_id`,
+ val ? (val as OptionType)?.value : 0
+ );
+
+ formik.setFieldTouched(`${inputName}_id`, true);
+ };
+
+ const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
+ formik.setFieldValue('category', (val as OptionType)?.value);
+ formik.setFieldValue('category_option', val);
+ formik.setFieldTouched('category', true);
+ };
+
+ const kandangChangeHandler = (event: React.ChangeEvent) => {
+ const { value, checked } = event.target;
+ if (checked) {
+ formik.setFieldValue(
+ 'kandang_ids',
+ formik.values.kandang_ids.concat(parseInt(value))
+ );
+ } else {
+ formik.setFieldValue(
+ 'kandang_ids',
+ formik.values.kandang_ids.filter((id) => id !== parseInt(value))
+ );
+ }
+ };
+ const kandangCheckAll = (event: React.ChangeEvent) => {
+ const { checked } = event.target;
+ if (checked) {
+ formik.setFieldValue(
+ 'kandang_ids',
+ optionsKandang
+ .filter(
+ (kandang) =>
+ kandang.status === 'NON_ACTIVE' ||
+ formik.values.kandang_ids.includes(kandang.id)
+ )
+ .map((kandang) => kandang.id)
+ );
+ } else {
+ formik.setFieldValue('kandang_ids', []);
+ }
+ };
+
+ // Submit Handler
+ const createProjectFlockHandler = async (
+ payload: CreateProjectFlockPayload
+ ) => {
+ const createProjectFlockRes = await ProjectFlockApi.create(payload);
+
+ if (isResponseSuccess(createProjectFlockRes)) {
+ toast.success(createProjectFlockRes?.message as string);
+ router.push('/production/project-flock');
+ }
+ if (isResponseError(createProjectFlockRes)) {
+ setProjectFlockFormErrorMessage(createProjectFlockRes?.message as string);
+ toast.error(createProjectFlockRes?.message as string);
+ }
+ };
+ const updateProjectFlockHandler = async (
+ payload: CreateProjectFlockPayload
+ ) => {
+ const updateProjectFlockRes = await ProjectFlockApi.update(
+ initialValues?.id as number,
+ payload
+ );
+
+ if (isResponseSuccess(updateProjectFlockRes)) {
+ toast.success(updateProjectFlockRes?.message as string);
+ router.push('/production/project-flock');
+ }
+ if (isResponseError(updateProjectFlockRes)) {
+ setProjectFlockFormErrorMessage(updateProjectFlockRes?.message as string);
+ toast.error(updateProjectFlockRes?.message as string);
+ }
+ };
+
+ // Formik InitialValue
+ const formikInitialValues = useMemo(() => {
+ return {
+ name: initialValues?.name ?? '',
+ flock: initialValues?.flock
+ ? {
+ value: initialValues.flock.id,
+ label: initialValues.flock.name,
+ }
+ : null,
+ area: initialValues?.area
+ ? {
+ value: initialValues.area.id,
+ label: initialValues.area.name,
+ }
+ : null,
+ category_option: initialValues?.category
+ ? {
+ value: initialValues.category,
+ label: initialValues.category,
+ }
+ : null,
+ fcr: initialValues?.fcr
+ ? {
+ value: initialValues.fcr.id,
+ label: initialValues.fcr.name,
+ }
+ : null,
+ location: initialValues?.location
+ ? {
+ value: initialValues.location.id,
+ label: initialValues.location.name,
+ }
+ : null,
+ flock_id: initialValues?.flock?.id ?? 0,
+ area_id: initialValues?.area?.id ?? 0,
+ category: initialValues?.category as NonNullable<
+ 'GROWING' | 'LAYING' | undefined
+ >,
+ fcr_id: initialValues?.fcr?.id ?? 0,
+ location_id: initialValues?.location?.id ?? 0,
+ period: initialValues?.period ?? 0,
+ kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
+ | number
+ | undefined
+ )[],
+ };
+ }, [initialValues]);
+
+ // Formik
+ const formik = useFormik({
+ initialValues: formikInitialValues,
+ enableReinitialize: true,
+ validationSchema:
+ formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema,
+ validateOnBlur: true,
+ validateOnChange: true,
+ validateOnMount: true,
+ onSubmit: async (values) => {
+ setProjectFlockFormErrorMessage('');
+ const payload: CreateProjectFlockPayload = {
+ flock_id: values.flock_id as number,
+ area_id: values.area_id as number,
+ category: values.category as string,
+ fcr_id: values.fcr_id as number,
+ location_id: values.location_id as number,
+ period: values.period as number,
+ kandang_ids: values.kandang_ids as number[],
+ };
+
+ switch (formType) {
+ case 'add':
+ await createProjectFlockHandler(payload);
+ break;
+ case 'edit':
+ await updateProjectFlockHandler(payload);
+ break;
+ default:
+ break;
+ }
+ },
+ });
+
+ const { setValues: formikSetValues } = formik;
+ // Effect Initial
+ useEffect(() => {
+ if (formType == 'detail') {
+ formik.setFieldValue('area', {
+ value: initialValues?.area.id,
+ label: initialValues?.area.name,
+ });
+ formik.setFieldValue('area_id', initialValues?.area_id);
+ if (initialValues?.area_id) {
+ setSelectedArea(initialValues?.area_id.toString() as string);
+ }
+
+ formik.setFieldValue('period', initialValues?.period);
+ }
+ }, [initialValues, setSelectedArea, formType]);
+
+ useEffect(() => {
+ formikSetValues(formikInitialValues);
+ }, [formikSetValues, formikInitialValues]);
+
+ // Aktifkan lokasi jika formType = 'detail'
+ useEffect(() => {
+ if (formType === 'detail') {
+ setDisabledLocation(false);
+ }
+ }, [formType]);
+
+ // Set lokasi otomatis berdasarkan initialValues saat formType = 'detail'
+ useEffect(() => {
+ if (formType != 'add' && initialValues?.location?.id) {
+ setSelectedLocation(initialValues.location?.id.toString());
+ setDisabledLocation(false); // biar dropdown lokasi aktif juga
+ }
+ }, [formType, initialValues]);
+
+ useEffect(() => {
+ formik.validateForm();
+ }, [formik.values]);
+
+ useEffect(() => {
+ if(isResponseSuccess(periodFlocks)){
+ formik.setFieldValue('period', periodFlocks.data.next_period);
+ }
+ }, [periodFlocks]);
+
+ // Actions handler
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+ const deleteProjectFlockRes = await ProjectFlockApi.delete(
+ initialValues?.id as number
+ );
+
+ if (isResponseSuccess(deleteProjectFlockRes)) {
+ toast.success(deleteProjectFlockRes?.message as string);
+ router.push('/production/project-flock');
+ }
+ if (isResponseError(deleteProjectFlockRes)) {
+ toast.error(deleteProjectFlockRes?.message as string);
+ }
+ setIsDeleteLoading(false);
+ };
+
+ const confirmationModalApproveClickHandler = async () => {
+ setIsApproveLoading(true);
+ const approveProjectFlockRes = await ProjectFlockApi.customRequest<
+ BaseApiResponse,
+ 'POST'
+ >(`/${initialValues?.id}/approve`, {
+ method: 'POST',
+ });
+
+ if (isResponseSuccess(approveProjectFlockRes)) {
+ toast.success('Project Flock berhasil di-approve!');
+ confirmModal.closeModal();
+ }
+ if (isResponseError(approveProjectFlockRes)) {
+ toast.error(approveProjectFlockRes?.message as string);
+ confirmModal.closeModal();
+ }
+ setIsApproveLoading(false);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {formType === 'add' && 'Tambah Project Flock'}
+ {formType === 'detail' && 'Detail Project Flock'}
+
+
+ {projectFlockFormErrorMessage && (
+
+
+
+ {projectFlockFormErrorMessage}
+
+
+
+ )}
+ {formType == 'detail' && (
+
+
+
+ )}
+
+ {formType != 'add' && (
+
+
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+
+export default ProjectFlockForm;
diff --git a/src/config/constant.ts b/src/config/constant.ts
index 8f712726..053a50cc 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -12,6 +12,29 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
icon: 'gg:chart',
},
+ {
+ title: 'Production',
+ link: '/production',
+ icon: 'material-symbols:conveyor-belt-outline-rounded',
+ submenu: [
+ {
+ title: 'List Flock',
+ link: '/production/project-flock',
+ icon: 'material-symbols:list-alt-add-outline-rounded',
+ },
+ {
+ title: 'Chick In',
+ link: '/production/chick-in',
+ icon: 'mdi:home-import-outline',
+ },
+ {
+ title: 'Recording',
+ link: '/production/recording',
+ icon: 'mdi:clipboard-text',
+ },
+ ],
+ },
+
{
title: 'Persediaan',
link: '/inventory',
@@ -100,10 +123,16 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
link: '/master-data/supplier',
icon: 'material-symbols:add-business-outline-rounded',
},
+ {
+ title: 'Flock',
+ link: '/master-data/flock',
+ icon: 'material-symbols:raven-outline-rounded'
+ },
],
},
] as const;
+
export const ROWS_OPTIONS = [
{
label: '10',
@@ -160,6 +189,17 @@ export const CATEGORY_OPTIONS = [
},
];
+export const FLOCK_CATEGORY_OPTIONS = [
+ {
+ label: 'GROWING',
+ value: 'GROWING',
+ },
+ {
+ label: 'LAYING',
+ value: 'LAYING',
+ },
+];
+
export const PRODUCT_FLAG_OPTIONS = [
{ label: 'DOC', value: 'DOC' },
{ label: 'PAKAN', value: 'PAKAN' },
diff --git a/src/services/api/base.ts b/src/services/api/base.ts
index c4dd826e..5ccabdd7 100644
--- a/src/services/api/base.ts
+++ b/src/services/api/base.ts
@@ -89,4 +89,40 @@ export class BaseApiService {
return undefined;
}
}
+
+ async customRequest(
+ endpoint: string,
+ options?: {
+ method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
+ payload?: PayloadType;
+ params?: Record;
+ }
+ ): Promise {
+ try {
+ const urlBase = endpoint.startsWith('http')
+ ? endpoint
+ : `${this.basePath.replace(/\/$/, '')}/${endpoint.replace(/^\//, '')}`;
+
+ const url = options?.params
+ ? `${urlBase}?${new URLSearchParams(
+ Object.entries(options.params).reduce((acc, [key, value]) => {
+ if (value !== undefined) acc[key] = String(value);
+ return acc;
+ }, {} as Record)
+ )}`
+ : urlBase;
+
+ const res = await httpClient(url, {
+ method: options?.method || 'GET',
+ body: options?.payload,
+ });
+
+ return res;
+ } catch (error: unknown) {
+ if (axios.isAxiosError(error)) {
+ return error.response?.data;
+ }
+ return undefined;
+ }
+ }
}
diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts
index dce528e7..854bb8f3 100644
--- a/src/services/api/master-data.ts
+++ b/src/services/api/master-data.ts
@@ -59,6 +59,11 @@ import {
Fcr,
UpdateFcrPayload,
} from '@/types/api/master-data/fcr';
+import {
+ CreateFlockPayload,
+ Flock,
+ UpdateFlockPayload,
+} from '@/types/api/master-data/flock';
export const UomApi = new BaseApiService<
Uom,
@@ -130,3 +135,9 @@ export const FcrApi = new BaseApiService<
CreateFcrPayload,
UpdateFcrPayload
>('/master-data/fcrs');
+
+export const FlockApi = new BaseApiService<
+ Flock,
+ CreateFlockPayload,
+ UpdateFlockPayload
+>('/master-data/flocks');
\ No newline at end of file
diff --git a/src/services/api/production.ts b/src/services/api/production.ts
new file mode 100644
index 00000000..06e51c2c
--- /dev/null
+++ b/src/services/api/production.ts
@@ -0,0 +1,11 @@
+import {
+ ProjectFlock,
+ CreateProjectFlockPayload,
+} from '@/types/api/production/project-flock';
+import { BaseApiService } from '@/services/api/base';
+
+export const ProjectFlockApi = new BaseApiService<
+ ProjectFlock,
+ CreateProjectFlockPayload,
+ unknown
+>('/production/project_flocks');
\ No newline at end of file
diff --git a/src/stores/ui/ui.store.ts b/src/stores/ui/ui.store.ts
index 2e64dcc1..49554bc9 100644
--- a/src/stores/ui/ui.store.ts
+++ b/src/stores/ui/ui.store.ts
@@ -4,7 +4,7 @@ import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { UIStore } from '@/types/stores';
-import { createMainUiSlice } from './slices/main.slice';
+import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
export const useUiStore = create()(
devtools(
diff --git a/src/types/api/inventory/adjustment.d.ts b/src/types/api/inventory/adjustment.d.ts
index 9d995919..852389fe 100644
--- a/src/types/api/inventory/adjustment.d.ts
+++ b/src/types/api/inventory/adjustment.d.ts
@@ -1,5 +1,5 @@
import { Product } from '@/types/api/master-data/product';
-import { Warehouse } from '../master-data/warehouse';
+import { Warehouse } from '@/types/api/master-data/warehouse';
export type BaseInventoryAdjustment = {
id: number;
diff --git a/src/types/api/master-data/flock.d.ts b/src/types/api/master-data/flock.d.ts
new file mode 100644
index 00000000..3ac5d390
--- /dev/null
+++ b/src/types/api/master-data/flock.d.ts
@@ -0,0 +1,14 @@
+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
diff --git a/src/types/api/master-data/kandang.d.ts b/src/types/api/master-data/kandang.d.ts
index e05006d1..17cbbee7 100644
--- a/src/types/api/master-data/kandang.d.ts
+++ b/src/types/api/master-data/kandang.d.ts
@@ -5,6 +5,7 @@ import { BaseUser } from '@/types/api/user';
export type BaseKandang = {
id: number;
name: string;
+ status: string;
location: BaseLocation;
pic: BaseUser;
};
diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts
new file mode 100644
index 00000000..306c32f1
--- /dev/null
+++ b/src/types/api/production/project-flock.d.ts
@@ -0,0 +1,44 @@
+import { Area } from "@/types/api/master-data/area";
+import { Fcr } from "@/types/api/master-data/fcr";
+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 { BaseMetadata } from "@/types/api/api-general";
+
+export type BaseProjectFlock = {
+ id: number;
+ name: string;
+ status: string;
+ flock: Flock;
+ flock_id: number;
+ area: Area;
+ area_id: number;
+ category: string;
+ fcr: Fcr;
+ fcr_id: number;
+ location: Location;
+ location_id: number;
+ period: number;
+ kandang_ids: number[];
+ kandangs: Kandang[];
+}
+
+export type PeriodFlock = {
+ flock: Flock;
+ next_period: number;
+}
+
+
+export type ProjectFlock = BaseMetadata & BaseProjectFlock
+
+export type CreateProjectFlockPayload = {
+ flock_id: number;
+ area_id: number;
+ category: string;
+ fcr_id: number;
+ location_id: number;
+ period: number;
+ kandang_ids: number[];
+}
+
+export type UpdateProjectFlockPayload = CreateProjectFlockPayload;
\ No newline at end of file