From eaf41805d768e54851c3953df4352e458545b8de Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 23 Oct 2025 13:30:27 +0700 Subject: [PATCH 1/2] feat(FE-92-93-105-106): slicing ui chickin DOC and integrate with API --- src/app/production/chickin/add/layout.tsx | 11 + src/app/production/chickin/add/page.tsx | 236 +++++++++++++ src/app/production/chickin/page.tsx | 10 + src/components/input/DateInput.tsx | 137 ++++++++ .../pages/production/chickin/ChickinTable.tsx | 327 ++++++++++++++++++ .../chickin/form/ChickinForm.schema.ts | 9 + .../production/chickin/form/ChickinForm.tsx | 179 ++++++++++ .../project-flock/ProjectFlockTable.tsx | 9 + src/config/constant.ts | 2 +- src/services/api/production.ts | 12 +- src/types/api/production/chickin.d.ts | 20 ++ .../api/production/project-flock-kandang.d.ts | 12 + 12 files changed, 962 insertions(+), 2 deletions(-) create mode 100644 src/app/production/chickin/add/layout.tsx create mode 100644 src/app/production/chickin/add/page.tsx create mode 100644 src/app/production/chickin/page.tsx create mode 100644 src/components/input/DateInput.tsx create mode 100644 src/components/pages/production/chickin/ChickinTable.tsx create mode 100644 src/components/pages/production/chickin/form/ChickinForm.schema.ts create mode 100644 src/components/pages/production/chickin/form/ChickinForm.tsx create mode 100644 src/types/api/production/chickin.d.ts create mode 100644 src/types/api/production/project-flock-kandang.d.ts diff --git a/src/app/production/chickin/add/layout.tsx b/src/app/production/chickin/add/layout.tsx new file mode 100644 index 00000000..b41c70f9 --- /dev/null +++ b/src/app/production/chickin/add/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/chickin/add/page.tsx b/src/app/production/chickin/add/page.tsx new file mode 100644 index 00000000..475fba5a --- /dev/null +++ b/src/app/production/chickin/add/page.tsx @@ -0,0 +1,236 @@ +'use client'; + +import Button from '@/components/Button'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import Modal, { useModal } from '@/components/Modal'; +import ChickinForm from '@/components/pages/production/chickin/form/ChickinForm'; +import Table from '@/components/Table'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { cn } from '@/lib/helper'; +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 { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { Icon } from '@iconify/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +import useSWR from 'swr'; + +const AddChickin = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const projectFlockId = searchParams.get('projectFlockId'); + + // Tables Props + const { state: tableFilterState } = useTableFilter({ + initial: { search: '' }, + paramMap: { page: 'page', pageSize: 'limit' }, + }); + + const [selectedKandang, setSelectedKandang] = useState( + undefined + ); + + // Fetch Data + const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( + projectFlockId, + (id: number) => ProjectFlockApi.getSingle(id) + ); + const { data: listProjectFlock, isLoading: isLoadingListProjectFlock } = + useSWR(`${ProjectFlockApi.basePath}`, ProjectFlockApi.getAllFetcher); + + const getProjectFlockKandangUrl = `/kandangs/lookup`; + const { + data: projectFlockKandang, + isLoading: isLoadingProjectFlockKandang, + mutate: refreshProjectFlockKandang, + } = useSWR(getProjectFlockKandangUrl, () => + ProjectFlockApi.customRequest, 'GET'>( + getProjectFlockKandangUrl, + { + method: 'GET', + params: { + project_flock_id: projectFlockId ?? 0, + kandang_id: selectedKandang?.id, + }, + } + ) + ); + + // Mapping Options + const options = isResponseSuccess(listProjectFlock) + ? listProjectFlock?.data.map((projectFlock) => { + return { + value: projectFlock.id, + label: projectFlock?.flock.name, + }; + }) + : []; + + const chickinModal = useModal(); + + // Use Effect + useEffect(() => { + refreshProjectFlockKandang(); + }, [selectedKandang, refreshProjectFlockKandang]); + + if (!projectFlockId) { + router.back(); + + return ( +
+ +
+ ); + } + + if ( + !isLoadingProjectFlock && + (!projectFlock || isResponseError(projectFlock)) + ) { + router.replace('/404'); + return; + } + + // Handle Function + const handleChickinClick = (kandang: Kandang) => { + setSelectedKandang(kandang); + if (isResponseSuccess(projectFlockKandang) && projectFlockKandang.data.id) { + chickinModal.openModal(); + return; + } + }; + + return ( + <> + {isResponseSuccess(projectFlock) && ( + <> +
+
+ + +
+
+ + router.push( + `/production/chickin/add?projectFlockId=${ + (val as OptionType | null)?.value + }` + ) + } + /> +
+
+
+ + data={projectFlock.data.kandangs} + columns={[ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama Kandang', + }, + { + header: 'Aksi', + cell: (props) => { + return ( + <> + + + ); + }, + }, + ]} + page={undefined} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(projectFlock) && + projectFlock.data.kandangs?.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', + paginationClassName: 'hidden', + }} + /> +
+ +
+

+ Chickin Kandang - {selectedKandang?.name} +

+ +
+ {isResponseSuccess(projectFlockKandang) && + !isLoadingProjectFlockKandang && ( + + )} +
+ + )} + + ); +}; + +export default AddChickin; diff --git a/src/app/production/chickin/page.tsx b/src/app/production/chickin/page.tsx new file mode 100644 index 00000000..ad662f65 --- /dev/null +++ b/src/app/production/chickin/page.tsx @@ -0,0 +1,10 @@ +import ChickinTable from "@/components/pages/production/chickin/ChickinTable"; + +const Chickin = () => { + return ( +
+ +
+ ); +} +export default Chickin; \ No newline at end of file diff --git a/src/components/input/DateInput.tsx b/src/components/input/DateInput.tsx new file mode 100644 index 00000000..0ef80fee --- /dev/null +++ b/src/components/input/DateInput.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { + ChangeEventHandler, + FocusEventHandler, + ReactNode, +} from 'react'; + +import { cn } from '@/lib/helper'; + +export interface DateInputProps { + label?: string; + bottomLabel?: string; + name: string; + value?: string; + placeholder?: string; + min?: string; + max?: string; + className?: { + wrapper?: string; + label?: string; + inputWrapper?: string; + input?: string; + }; + isError?: boolean; + isValid?: boolean; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + isLoading?: boolean; + errorMessage?: string; + startAdornment?: ReactNode; + endAdornment?: ReactNode; + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; +} + +const DateInput = ({ + label, + bottomLabel, + name, + value, + placeholder, + min, + max, + className, + isError, + isValid, + errorMessage, + startAdornment, + endAdornment, + disabled = false, + required = false, + onChange, + onBlur, + readOnly = false, + isLoading = false, +}: DateInputProps) => { + return ( +
+ {label && ( + + )} + +
+ {startAdornment && startAdornment} + + + + {(isLoading || endAdornment) && ( +
+ {isLoading && } + {endAdornment && endAdornment} +
+ )} +
+ + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError && errorMessage && ( +

{errorMessage}

+ )} +
+ ); +}; + +export default DateInput; diff --git a/src/components/pages/production/chickin/ChickinTable.tsx b/src/components/pages/production/chickin/ChickinTable.tsx new file mode 100644 index 00000000..5c9f8431 --- /dev/null +++ b/src/components/pages/production/chickin/ChickinTable.tsx @@ -0,0 +1,327 @@ +'use client'; + +import Button from '@/components/Button'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import Modal, { 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 { TableRowOptions } from '@/components/table/TableRowOptions'; +import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; +import { TableToolbar } from '@/components/table/TableToolbar'; +import { ROWS_OPTIONS } from '@/config/constant'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn } from '@/lib/helper'; +import { ChickinApi, ProjectFlockApi } from '@/services/api/production'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { Chickin } from '@/types/api/production/chickin'; +import { Icon } from '@iconify/react'; +import { CellContext, SortingState } from '@tanstack/react-table'; +import { useState } from 'react'; +import useSWR from 'swr'; +import ChickinForm from './form/ChickinForm'; + +const ChickinTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + search: 'search', + }, + }); + + const [sorting, setSorting] = useState([]); + const [selectedChickin, setSelectedChickin] = useState( + undefined + ); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const deleteModal = useModal(); + const chickinModal = useModal(); + + // Data Fetching + const { + data: chickins, + isLoading, + mutate: refreshChickins, + } = useSWR( + `${ChickinApi.basePath}${getTableFilterQueryString()}`, + ChickinApi.getAllFetcher + ); + const { + data: projectFlocks, + isLoading: isLoadingProjectFlocks, + } = useSWR( + `${ProjectFlockApi.basePath}${getTableFilterQueryString()}`, + ProjectFlockApi.getAllFetcher + ); + + const searchChangeHandler = (event: React.ChangeEvent) => { + updateFilter('search', event.target.value); + setPage(1); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + setPageSize(newVal.value as number); + setPage(1); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + try { + await ChickinApi.delete(selectedChickin?.id as number); + refreshChickins(); + deleteModal.closeModal(); + } finally { + setIsDeleteLoading(false); + } + }; + + return ( + <> +
+
+
+ + +
+ +
+
+ + data={isResponseSuccess(chickins) ? chickins?.data : []} + columns={[ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorFn: (row) => row.project_flock_kandang?.kandang.name, + header: 'Kandang', + }, + { + accessorFn: (row) => row.quantity, + header: 'Jumlah Chickin', + }, + { + accessorFn: (row) => row.chick_in_date, + header: 'Tanggal Chickin', + cell: (props) => { + if (props.row.original.chick_in_date) { + return new Date(props.row.original.chick_in_date).toLocaleDateString( + 'id-ID' + ); + } else { + return '-'; + } + } + }, + { + accessorFn: (row) => row.note, + header: 'Catatan', + }, + { + 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 = () => { + setSelectedChickin(props.row.original); + deleteModal.openModal(); + }; + + const editClickHandler = () => { + setSelectedChickin(props.row.original); + chickinModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(chickins) ? chickins?.meta?.page : 0} + totalItems={ + isResponseSuccess(chickins) ? chickins?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(chickins) && chickins?.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', + }} + /> + + +
+

+ Chickin Kandang - { selectedChickin?.project_flock_kandang && selectedChickin?.project_flock_kandang.kandang?.name} +

+ +
+ { + refreshChickins() + chickinModal.closeModal() + }}/> +
+ + ); +}; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + editClickHandler, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + editClickHandler: () => void; + deleteClickHandler: () => void; +}) => { + return ( +
+ + + +
+ ); +}; + +export default ChickinTable; diff --git a/src/components/pages/production/chickin/form/ChickinForm.schema.ts b/src/components/pages/production/chickin/form/ChickinForm.schema.ts new file mode 100644 index 00000000..c7e8f4c6 --- /dev/null +++ b/src/components/pages/production/chickin/form/ChickinForm.schema.ts @@ -0,0 +1,9 @@ +import * as Yup from 'yup'; + +export const ChickinFormSchema = Yup.object({ + chick_in_date: Yup.string().required('Tanggal masuk wajib diisi!'), +}) + +export type ChickinFormValues = Yup.InferType; + +export const UpdateChickinFormSchema = ChickinFormSchema; \ No newline at end of file diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx new file mode 100644 index 00000000..c0a3bdcb --- /dev/null +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -0,0 +1,179 @@ +'use client'; + +import Button from '@/components/Button'; +import { + Chickin, + CreateChickinPayload, + UpdateChickinPayload, +} from '@/types/api/production/chickin'; +import { + ChickinFormSchema, + ChickinFormValues, + UpdateChickinFormSchema, +} from '@/components/pages/production/chickin/form/ChickinForm.schema'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useFormik } from 'formik'; +import { ChickinApi } from '@/services/api/production'; +import DateInput from '@/components/input/DateInput'; +import { isResponseError } from '@/lib/api-helper'; +import toast from 'react-hot-toast'; +import { Icon } from '@iconify/react'; + +interface ChickinFormProps { + formType?: 'add' | 'detail' | 'edit'; + initialValues?: Chickin; + afterSubmit?: () => void; +} + +const ChickinForm = ({ + formType = 'add', + initialValues, + afterSubmit, +}: ChickinFormProps) => { + // Helper Function + const formatDateForInput = (dateString?: string): string => { + if (!dateString) return ''; + return new Date(dateString).toISOString().split('T')[0]; + }; + + // State + const [chickinFormErrorMessage, setChickinFormErrorMessage] = useState(''); + + // Initial Value + const formikInitialValue = useMemo(() => { + return { + chick_in_date: formatDateForInput(initialValues?.chick_in_date) ?? '', + }; + }, [initialValues]); + + // Handle Submit Function + const handleCreate = useCallback( + async ( + payload: CreateChickinPayload, + afterSubmit: (() => void) | undefined + ) => { + const res = await ChickinApi.create(payload); + if (isResponseError(res)) { + setChickinFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + afterSubmit?.(); + }, + [] + ); + const handleUpdate = useCallback( + async ( + payload: UpdateChickinPayload, + afterSubmit: (() => void) | undefined + ) => { + const res = await ChickinApi.update( + payload.project_flock_kandang_id as number, + payload + ); + if (isResponseError(res)) { + setChickinFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + afterSubmit?.(); + }, + [] + ); + + // Formik + const formik = useFormik({ + initialValues: formikInitialValue, + enableReinitialize: true, + validationSchema: + formType === 'edit' ? UpdateChickinFormSchema : ChickinFormSchema, + onSubmit: async (values) => { + // reset error message + setChickinFormErrorMessage(''); + + if (initialValues?.project_flock_kandang?.id == undefined) { + return; + } + + // create payload + const payload = { + chick_in_date: values.chick_in_date, + project_flock_kandang_id: initialValues?.project_flock_kandang?.id, + }; + + // cek type form yang disubmit + switch (formType) { + case 'add': + handleCreate(payload, afterSubmit); + break; + case 'edit': + handleUpdate(payload, afterSubmit); + break; + default: + break; + } + }, + }); + + // Initialize Formik + const { setValues: formikSetValues } = formik; + useEffect(() => { + formikSetValues(formikInitialValue); + }, [formikSetValues, formikInitialValue]); + + return ( + <> +
+ + {initialValues?.project_flock_kandang?.id == undefined && ( +

Project Flock Kandang tidak ditemukan.

+ )} + {chickinFormErrorMessage && ( +
{ + setChickinFormErrorMessage(''); + }} + > + + {chickinFormErrorMessage} +
+ )} +
+ + +
+ + + ); +}; + +export default ChickinForm; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index af057fb8..412c47ed 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -56,6 +56,15 @@ const RowOptionsMenu = ({ Detail + {/*
-
+
@@ -135,6 +144,9 @@ const AddChickin = () => { }` ) } + onInputChange={(val) => { + setSearchProjectFlock(val); + }} />
@@ -224,6 +236,7 @@ const AddChickin = () => { created_at: projectFlock.data.created_at, updated_at: projectFlock.data.updated_at, }} + afterSubmit={handleAfterSubmit} /> )} diff --git a/src/components/pages/production/chickin/ChickinTable.tsx b/src/components/pages/production/chickin/ChickinTable.tsx index 5c9f8431..e2b527d3 100644 --- a/src/components/pages/production/chickin/ChickinTable.tsx +++ b/src/components/pages/production/chickin/ChickinTable.tsx @@ -2,15 +2,13 @@ import Button from '@/components/Button'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import { OptionType } from '@/components/input/SelectInput'; import Modal, { 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 { TableRowOptions } from '@/components/table/TableRowOptions'; import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; -import { TableToolbar } from '@/components/table/TableToolbar'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; diff --git a/src/components/pages/production/chickin/form/ChickinForm.schema.ts b/src/components/pages/production/chickin/form/ChickinForm.schema.ts index c7e8f4c6..42d3b6ea 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.schema.ts +++ b/src/components/pages/production/chickin/form/ChickinForm.schema.ts @@ -2,6 +2,8 @@ import * as Yup from 'yup'; export const ChickinFormSchema = Yup.object({ chick_in_date: Yup.string().required('Tanggal masuk wajib diisi!'), + note: Yup.string().required('Catatan wajib diisi!'), + quantity: Yup.number().min(1, 'Jumlah wajib diisi!').required('Jumlah wajib diisi!'), }) export type ChickinFormValues = Yup.InferType; diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index c0a3bdcb..c6674df4 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -18,6 +18,8 @@ import DateInput from '@/components/input/DateInput'; import { isResponseError } from '@/lib/api-helper'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; +import TextArea from '@/components/input/TextArea'; +import TextInput from '@/components/input/TextInput'; interface ChickinFormProps { formType?: 'add' | 'detail' | 'edit'; @@ -43,6 +45,8 @@ const ChickinForm = ({ const formikInitialValue = useMemo(() => { return { chick_in_date: formatDateForInput(initialValues?.chick_in_date) ?? '', + note: initialValues?.note ?? `Catatan Chickin ${initialValues?.project_flock_kandang?.project_flock.flock.name}`, + quantity: initialValues?.quantity ?? 1, }; }, [initialValues]); @@ -140,6 +144,29 @@ const ChickinForm = ({ } errorMessage={formik.errors.chick_in_date} /> + +