refactor(FE): change project flock form, detail and chickin view using drawer

This commit is contained in:
randy-ar
2025-11-28 16:41:01 +07:00
parent 22ce1b1142
commit 892bb19dfd
11 changed files with 325 additions and 196 deletions
@@ -12,8 +12,6 @@ const DetailInventoryAdjustment = () => {
// Ambil data dari router state
useEffect(() => {
console.log('Router State');
console.log(window.history.state);
const state = window.history.state?.usr as
| { inventoryAdjustment?: InventoryAdjustment }
| undefined;
@@ -26,9 +24,6 @@ const DetailInventoryAdjustment = () => {
const finalData = inventoryAdjustment;
console.log('Final Data');
console.log(finalData);
if (!finalData) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
@@ -1,8 +1,16 @@
'use client';
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import React, { useImperativeHandle } from 'react';
import toast from 'react-hot-toast';
const AddProjectFlock = () => {
// useImperativeHandle(ref, () => ({
// validate() {
// toast.success('Validating');
// return false;
// },
// }));
return (
<section className='w-full p-4 flex flex-row justify-center'>
<ProjectFlockForm formType='add' />
@@ -0,0 +1,55 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import Drawer from '@/components/Drawer';
import React, { ReactNode } from 'react';
import ProjectFlockTable from '@/components/pages/production/project-flock/ProjectFlockTable';
export default function ProjectFlockLayout({
children,
}: {
children: ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const isAdd = pathname.endsWith('/add');
const isEdit = pathname.includes('/detail/edit');
const isDetail = pathname.includes('/detail');
const isChickin = pathname.includes('/chickin/add/kandang');
const isOpen = isAdd || isEdit || isDetail || isChickin;
// const childRef = useRef<ProjectFlockFormRef>(null);
const handleBackdropClick = () => {
// const isValid = childRef.current?.validate(); // 🔥 trigger validation child
// if (!isValid) {
// toast.error('Form belum valid, Drawer tidak bisa close');
// return;
// }
router.push('/production/project-flock');
};
return (
<>
{/* List page always rendered */}
<div>
<ProjectFlockTable />
</div>
{/* Render Drawer only on /add */}
<Drawer
open={isOpen}
setOpen={(v) => {
if (!v) router.push('/production/project-flock');
}}
closeOnBackdropClick={false}
onBackdropClick={handleBackdropClick}
variant='right'
sidebarContent={isOpen && <div className='p-4'>{children}</div>}
/>
</>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@ import ProjectFlockTable from '@/components/pages/production/project-flock/Proje
const ProjectFlock = () => {
return (
<section className='w-full p-4'>
<section className='size-full p-4'>
<ProjectFlockTable />
</section>
);
+103 -8
View File
@@ -10,28 +10,102 @@ interface DrawerProps {
open: boolean;
setOpen: (newOpenState: boolean) => void;
openOnLarge?: boolean;
variant?: 'sidebar' | 'left' | 'right';
zIndex?: string;
className?: DrawerClassName;
onBackdropClick?: () => void;
closeOnBackdropClick?: boolean;
}
type DrawerClassName = {
drawer?: string;
drawerContent?: string;
drawerSide?: string;
drawerOverlay?: string;
drawerSidebarContent?: string;
};
const Drawer = ({
children,
sidebarContent,
open,
setOpen,
openOnLarge,
variant = 'sidebar',
zIndex = '20',
className,
onBackdropClick,
closeOnBackdropClick = true,
}: DrawerProps) => {
const getDrawerClassNames = (): DrawerClassName => {
const baseClassNames = {
drawer: 'drawer',
drawerContent: 'drawer-content',
drawerSide: 'drawer-side',
drawerOverlay: 'drawer-overlay',
drawerSidebarContent: 'min-h-full bg-base-100',
};
if (variant === 'sidebar') {
return {
...baseClassNames,
drawerSidebarContent: cn(
baseClassNames.drawerSidebarContent,
'w-full max-w-[300px] lg:w-[300px]'
),
};
} else if (variant === 'right') {
return {
...baseClassNames,
drawer: cn(baseClassNames.drawer, 'drawer-end'),
drawerSide: cn(
baseClassNames.drawerSide,
'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21'
),
drawerSidebarContent: cn(
baseClassNames.drawerSidebarContent,
'w-full min-w-120 sm:w-fit'
),
};
} else if (variant === 'left') {
return {
...baseClassNames,
drawerSide: cn(
baseClassNames.drawerSide,
'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21'
),
drawerSidebarContent: cn(
baseClassNames.drawerSidebarContent,
'w-full min-w-120 sm:w-fit'
),
};
}
return baseClassNames; // Fallback for default or unknown variant
};
const varianClassName = getDrawerClassNames();
const toggleDrawer = () => {
setOpen(!open);
};
const closeDrawer = () => {
setOpen(false);
if (closeOnBackdropClick) {
setOpen(false);
}
onBackdropClick && onBackdropClick();
};
return (
<div
className={cn('drawer', {
'lg:drawer-open': openOnLarge,
})}
className={cn(
'drawer',
{
'lg:drawer-open': openOnLarge,
},
varianClassName?.drawer,
className?.drawer
)}
>
<input
type='checkbox'
@@ -40,16 +114,37 @@ const Drawer = ({
className='drawer-toggle'
/>
<div className='drawer-content'>{children}</div>
{/* Drawer Content */}
<div
className={cn(varianClassName?.drawerContent, className?.drawerContent)}
>
{children}
</div>
<div className='drawer-side border-r border-solid border-gray-200 z-20'>
{/* Drawer Side */}
<div
className={cn(
varianClassName?.drawerSide,
className?.drawerSide,
zIndex
)}
>
<label
aria-label='close sidebar'
className='drawer-overlay'
className={cn(
varianClassName?.drawerOverlay,
className?.drawerOverlay
)}
onClick={closeDrawer}
/>
<div className='min-h-full w-full max-w-[300px] lg:w-[300px] bg-base-100'>
{/* Sidebar Content */}
<div
className={cn(
varianClassName?.drawerSidebarContent,
className?.drawerContent
)}
>
{sidebarContent}
</div>
</div>
@@ -156,8 +156,6 @@ export const recalculate = (
field: string,
values: ProductCalculationFields
) => {
console.log('Values');
console.log(values);
const { qty, unit_price, total_price, avg_weight, total_weight } = values;
const result: Partial<ProductCalculationFields> = {};
if (field == 'unit_price' || field == 'total_price' || field == 'qty') {
@@ -174,8 +172,6 @@ export const recalculate = (
result.avg_weight = Number(total_weight) / Number(qty);
}
}
console.log('Result');
console.log(result);
return result;
};
export const getSubmitField = (values: ProductCalculationFields) => {
@@ -327,8 +323,6 @@ const MarketingForm = ({
})
.filter((item) => Boolean(item)),
} as UpdateDeliveryOrderPayload);
console.log('PAYLOAD');
console.log(payload);
switch (formType) {
case 'add':
await createMarketingHandler(payload as CreateSalesOrderPayload);
@@ -352,7 +346,6 @@ const MarketingForm = ({
// ================== FORM REPEATER HANDLER ==================
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
setIsLoading(true);
console.log(values);
const createMarketingRes = await SalesOrderApi.create(values);
if (isResponseSuccess(createMarketingRes)) {
toast.success(createMarketingRes?.message as string);
@@ -365,7 +358,6 @@ const MarketingForm = ({
};
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
setIsLoading(true);
console.log(values);
const updateMarketingRes = await SalesOrderApi.update(
initialValues?.id as number,
values
@@ -381,10 +373,8 @@ const MarketingForm = ({
};
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
setIsLoading(true);
console.log(initialValues?.id);
const createDeliveryRes = await DeliveryOrderApi.create(values);
if (isResponseSuccess(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.success(createDeliveryRes?.message as string);
setDeliveryOrderValues(
createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
@@ -397,20 +387,17 @@ const MarketingForm = ({
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
}
if (isResponseError(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.error(createDeliveryRes?.message as string);
}
setIsLoading(false);
};
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
setIsLoading(true);
console.log(initialValues?.id);
const updateDeliveryRes = await DeliveryOrderApi.update(
initialValues?.id as number,
values
);
if (isResponseSuccess(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.success(updateDeliveryRes?.message as string);
setDeliveryOrderValues(
mergeSOwithDO(
@@ -426,7 +413,6 @@ const MarketingForm = ({
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
}
if (isResponseError(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.error(updateDeliveryRes?.message as string);
}
setIsLoading(false);
@@ -435,16 +421,13 @@ const MarketingForm = ({
// ================== MARKETING HANDLER ==================
const deleteMarketingHandler = async () => {
setIsLoading(true);
console.log(initialValues?.id);
const deleteMarketingRes = await MarketingApi.delete(
initialValues?.id as number
);
if (isResponseSuccess(deleteMarketingRes)) {
console.log(deleteMarketingRes);
toast.success(deleteMarketingRes?.message as string);
}
if (isResponseError(deleteMarketingRes)) {
console.log(deleteMarketingRes);
toast.error(deleteMarketingRes?.message as string);
}
setIsLoading(false);
@@ -306,7 +306,6 @@ const SupplierForm = ({
label='Hatchery'
value={hatcheryOptionsValues}
onChange={(val) => {
console.log(val); // pastikan val = array of { value, label }
setHatcheryOptionValues(val as OptionType[]);
}}
isError={
@@ -16,15 +16,11 @@ import { cn } from '@/lib/helper';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { BaseApiResponse } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang';
import {
ProjectFlockApprovalPayload,
ProjectFlock,
} from '@/types/api/production/project-flock';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table';
import { ChangeEventHandler, useState } from 'react';
import { ChangeEventHandler, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
@@ -266,10 +262,10 @@ const ProjectFlockTable = () => {
<div className='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'>
<Button
href='/production/project-flock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
href='/production/project-flock/add'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
@@ -24,7 +24,6 @@ import {
UpdateProjectFlockFormSchema,
} from '@/components/pages/production/project-flock/form/ProjectFlockForm.schema';
import {
ProjectFlockApprovalPayload,
CreateProjectFlockPayload,
ProjectFlock,
} from '@/types/api/production/project-flock';
@@ -43,6 +42,7 @@ import ApprovalSteps, {
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import NumberInput from '@/components/input/NumberInput';
import Card from '@/components/Card';
interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail';
@@ -259,6 +259,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(createProjectFlockRes)) {
toast.success(createProjectFlockRes?.message as string);
handleReset();
router.push('/production/project-flock');
}
if (isResponseError(createProjectFlockRes)) {
@@ -276,6 +277,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(updateProjectFlockRes)) {
toast.success(updateProjectFlockRes?.message as string);
handleReset();
router.push('/production/project-flock');
}
if (isResponseError(updateProjectFlockRes)) {
@@ -283,6 +285,15 @@ const ProjectFlockForm = ({
toast.error(updateProjectFlockRes?.message as string);
}
};
const handleReset = () => {
formik.resetForm();
setSelectedArea('');
setSelectedLocation('');
setDisabledLocation(true);
setOpenSelectKandangs(false);
setOptionsKandang([]);
formikSetValues(formikInitialValues);
};
// Formik InitialValue
const formikInitialValues = useMemo<ProjectFlockFormValues>(() => {
@@ -557,6 +568,16 @@ const ProjectFlockForm = ({
const inputPeriod =
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
// expose method validate() ke parent
// TODO: Buat Store untuk kirim props formik.isValid ke parent (layout.tsx)
// useImperativeHandle(ref, () => ({
// validate() {
// formik.validateForm();
// const isValid = formik.isValid;
// return isValid;
// },
// }));
return (
<>
<section className='w-full'>
@@ -653,158 +674,139 @@ const ProjectFlockForm = ({
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
<div className='card bg-base-100 shadow w-full mb-6'>
<div className='card-body'>
<div className='card-title mb-4'>Informasi Umum</div>
<div className='grid sm:grid-cols-2 gap-4'>
<SelectInput
required
label='Area'
value={formik.values.area as OptionType}
onChange={areaChangeHandler}
options={optionsArea}
isLoading={isLoadingAreas}
isError={
formik.touched.area_id && Boolean(formik.errors.area_id)
}
errorMessage={formik.errors.area_id as string}
isClearable
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Flock'
value={
formik.values.flock_name
? ({
label: formik.values.flock_name,
value: optionsFlock.find((flock) => {
return flock.label === formik.values.flock_name;
})?.value,
} as OptionType)
: undefined
}
onChange={(val) => {
optionChangeHandler(val, 'flock');
setSelectedFlock((val as OptionType)?.label);
formik.setFieldValue(
'flock_name',
(val as OptionType)?.label
);
}}
options={optionsFlock}
isLoading={isLoadingFlocks}
isError={
formik.touched.flock_name &&
Boolean(formik.errors.flock_name)
}
errorMessage={formik.errors.flock_name as string}
isClearable
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Lokasi'
value={formik.values.location as OptionType}
onChange={locationChangeHandler}
options={
selectedArea != '' || initialValues?.area?.id
? optionsLocation
: []
}
isLoading={isLoadingLocations}
isError={
formik.touched.location_id &&
Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.location_id as string}
isClearable
isDisabled={formType === 'detail' || disabledLocation}
/>
<SelectInput
required
label='FCR'
value={formik.values.fcr as OptionType}
onChange={(val) => {
optionChangeHandler(val, 'fcr');
}}
options={optionsFcr}
isLoading={isLoadingFcrs}
isError={
formik.touched.fcr_id && Boolean(formik.errors.fcr_id)
}
errorMessage={formik.errors.fcr_id as string}
isClearable
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Kategori'
value={formik.values.category_option as OptionType}
onChange={categoryChangeHandler}
options={FLOCK_CATEGORY_OPTIONS}
isError={
formik.touched.category && Boolean(formik.errors.category)
}
errorMessage={formik.errors.category as string}
isClearable
isDisabled={formType === 'detail'}
/>
<NumberInput
name='period'
label='Periode'
disabled
readOnly
placeholder='Period'
value={selectedLocation ? inputPeriod : ''}
/>
</div>
</div>
</div>
<div className='card bg-base-100 shadow w-full'>
<div className='card-body'>
<Collapse
title={
<div className='card-actions justify-between w-full'>
<div className='card-title'>Pilih Kandang</div>
<Button
variant='link'
className={`text-primary rotate-${
openSelectKandangs ? '180' : '0'
} transition-transform hover:text-inherit me-3`}
>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
/>
</Button>
</div>
<Card
title='Informasi Umum'
variant='bordered'
className={{
wrapper: 'w-full mb-4',
}}
>
<div className='grid sm:grid-cols-2 gap-4'>
<SelectInput
required
label='Area'
value={formik.values.area as OptionType}
onChange={areaChangeHandler}
options={optionsArea}
isLoading={isLoadingAreas}
isError={
formik.touched.area_id && Boolean(formik.errors.area_id)
}
className='sm:w-full'
titleClassName='w-full p-0!'
onOpenChange={setOpenSelectKandangs}
open={openSelectKandangs}
>
<div className='overflow-x-auto'>
{isLoadingKandang && (
<span className='loading loading-dots loading-xl'></span>
)}
<ProjectFlockKandangTable
listPeriods={
isResponseSuccess(periodFlocks) ? periodFlocks.data : []
}
listKandang={optionsKandang}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
selectedIds={formik.values.kandang_ids}
formType={formType}
initialValues={initialValues}
/>
</div>
</Collapse>
errorMessage={formik.errors.area_id as string}
isClearable
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Flock'
value={
formik.values.flock_name
? ({
label: formik.values.flock_name,
value: optionsFlock.find((flock) => {
return flock.label === formik.values.flock_name;
})?.value,
} as OptionType)
: undefined
}
onChange={(val) => {
optionChangeHandler(val, 'flock');
setSelectedFlock((val as OptionType)?.label);
formik.setFieldValue(
'flock_name',
(val as OptionType)?.label
);
}}
options={optionsFlock}
isLoading={isLoadingFlocks}
isError={
formik.touched.flock_name && Boolean(formik.errors.flock_name)
}
errorMessage={formik.errors.flock_name as string}
isClearable
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Lokasi'
value={formik.values.location as OptionType}
onChange={locationChangeHandler}
options={
selectedArea != '' || initialValues?.area?.id
? optionsLocation
: []
}
isLoading={isLoadingLocations}
isError={
formik.touched.location_id &&
Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.location_id as string}
isClearable
isDisabled={formType === 'detail' || disabledLocation}
/>
<SelectInput
required
label='FCR'
value={formik.values.fcr as OptionType}
onChange={(val) => {
optionChangeHandler(val, 'fcr');
}}
options={optionsFcr}
isLoading={isLoadingFcrs}
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
errorMessage={formik.errors.fcr_id as string}
isClearable
isDisabled={formType === 'detail'}
/>
<SelectInput
required
label='Kategori'
value={formik.values.category_option as OptionType}
onChange={categoryChangeHandler}
options={FLOCK_CATEGORY_OPTIONS}
isError={
formik.touched.category && Boolean(formik.errors.category)
}
errorMessage={formik.errors.category as string}
isClearable
isDisabled={formType === 'detail'}
/>
<NumberInput
name='period'
label='Periode'
disabled
readOnly
placeholder='Period'
value={selectedLocation ? inputPeriod : ''}
/>
</div>
</div>
</Card>
<Card
collapsible
title='Pilih Kandang'
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='overflow-x-auto duration-300 ease-in-out'>
{isLoadingKandang && (
<span className='loading loading-dots loading-xl'></span>
)}
<ProjectFlockKandangTable
listPeriods={
isResponseSuccess(periodFlocks) ? periodFlocks.data : []
}
listKandang={optionsKandang}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
selectedIds={formik.values.kandang_ids}
formType={formType}
initialValues={initialValues}
/>
</div>
</Card>
<div className='flex flex-row justify-center gap-2 flex-wrap my-6'>
{formType !== 'detail' && (
@@ -912,4 +914,6 @@ const ProjectFlockForm = ({
);
};
ProjectFlockForm.displayName = 'ProjectFlockForm';
export default ProjectFlockForm;
@@ -144,8 +144,6 @@ const ProjectFlockKandangTable = ({
accessorFn: (row) => row.location?.name,
header: 'Periode',
cell: (props) => {
console.log('listPeriods');
console.log(listPeriods);
const period =
listPeriods.length > 0
? listPeriods.find((p) => p.id == props.row.original.id)
-4
View File
@@ -793,8 +793,6 @@ export class ExpenseApiService extends BaseApiService<
sentPayload.set(pair[0], pair[1]);
}
console.log({ sentPayload });
return {
code: 200,
status: 'success',
@@ -815,8 +813,6 @@ export class ExpenseApiService extends BaseApiService<
sentPayload.set(pair[0], pair[1]);
}
console.log({ sentPayload });
return {
code: 200,
status: 'success',