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 // Ambil data dari router state
useEffect(() => { useEffect(() => {
console.log('Router State');
console.log(window.history.state);
const state = window.history.state?.usr as const state = window.history.state?.usr as
| { inventoryAdjustment?: InventoryAdjustment } | { inventoryAdjustment?: InventoryAdjustment }
| undefined; | undefined;
@@ -26,9 +24,6 @@ const DetailInventoryAdjustment = () => {
const finalData = inventoryAdjustment; const finalData = inventoryAdjustment;
console.log('Final Data');
console.log(finalData);
if (!finalData) { if (!finalData) {
return ( return (
<div className='w-full flex flex-row justify-center items-center p-4'> <div className='w-full flex flex-row justify-center items-center p-4'>
@@ -1,8 +1,16 @@
'use client'; 'use client';
import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm';
import React, { useImperativeHandle } from 'react';
import toast from 'react-hot-toast';
const AddProjectFlock = () => { const AddProjectFlock = () => {
// useImperativeHandle(ref, () => ({
// validate() {
// toast.success('Validating');
// return false;
// },
// }));
return ( return (
<section className='w-full p-4 flex flex-row justify-center'> <section className='w-full p-4 flex flex-row justify-center'>
<ProjectFlockForm formType='add' /> <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 = () => { const ProjectFlock = () => {
return ( return (
<section className='w-full p-4'> <section className='size-full p-4'>
<ProjectFlockTable /> <ProjectFlockTable />
</section> </section>
); );
+101 -6
View File
@@ -10,28 +10,102 @@ interface DrawerProps {
open: boolean; open: boolean;
setOpen: (newOpenState: boolean) => void; setOpen: (newOpenState: boolean) => void;
openOnLarge?: boolean; 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 = ({ const Drawer = ({
children, children,
sidebarContent, sidebarContent,
open, open,
setOpen, setOpen,
openOnLarge, openOnLarge,
variant = 'sidebar',
zIndex = '20',
className,
onBackdropClick,
closeOnBackdropClick = true,
}: DrawerProps) => { }: 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 = () => { const toggleDrawer = () => {
setOpen(!open); setOpen(!open);
}; };
const closeDrawer = () => { const closeDrawer = () => {
if (closeOnBackdropClick) {
setOpen(false); setOpen(false);
}
onBackdropClick && onBackdropClick();
}; };
return ( return (
<div <div
className={cn('drawer', { className={cn(
'drawer',
{
'lg:drawer-open': openOnLarge, 'lg:drawer-open': openOnLarge,
})} },
varianClassName?.drawer,
className?.drawer
)}
> >
<input <input
type='checkbox' type='checkbox'
@@ -40,16 +114,37 @@ const Drawer = ({
className='drawer-toggle' 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 <label
aria-label='close sidebar' aria-label='close sidebar'
className='drawer-overlay' className={cn(
varianClassName?.drawerOverlay,
className?.drawerOverlay
)}
onClick={closeDrawer} 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} {sidebarContent}
</div> </div>
</div> </div>
@@ -156,8 +156,6 @@ export const recalculate = (
field: string, field: string,
values: ProductCalculationFields values: ProductCalculationFields
) => { ) => {
console.log('Values');
console.log(values);
const { qty, unit_price, total_price, avg_weight, total_weight } = values; const { qty, unit_price, total_price, avg_weight, total_weight } = values;
const result: Partial<ProductCalculationFields> = {}; const result: Partial<ProductCalculationFields> = {};
if (field == 'unit_price' || field == 'total_price' || field == 'qty') { if (field == 'unit_price' || field == 'total_price' || field == 'qty') {
@@ -174,8 +172,6 @@ export const recalculate = (
result.avg_weight = Number(total_weight) / Number(qty); result.avg_weight = Number(total_weight) / Number(qty);
} }
} }
console.log('Result');
console.log(result);
return result; return result;
}; };
export const getSubmitField = (values: ProductCalculationFields) => { export const getSubmitField = (values: ProductCalculationFields) => {
@@ -327,8 +323,6 @@ const MarketingForm = ({
}) })
.filter((item) => Boolean(item)), .filter((item) => Boolean(item)),
} as UpdateDeliveryOrderPayload); } as UpdateDeliveryOrderPayload);
console.log('PAYLOAD');
console.log(payload);
switch (formType) { switch (formType) {
case 'add': case 'add':
await createMarketingHandler(payload as CreateSalesOrderPayload); await createMarketingHandler(payload as CreateSalesOrderPayload);
@@ -352,7 +346,6 @@ const MarketingForm = ({
// ================== FORM REPEATER HANDLER ================== // ================== FORM REPEATER HANDLER ==================
const createMarketingHandler = async (values: CreateSalesOrderPayload) => { const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
setIsLoading(true); setIsLoading(true);
console.log(values);
const createMarketingRes = await SalesOrderApi.create(values); const createMarketingRes = await SalesOrderApi.create(values);
if (isResponseSuccess(createMarketingRes)) { if (isResponseSuccess(createMarketingRes)) {
toast.success(createMarketingRes?.message as string); toast.success(createMarketingRes?.message as string);
@@ -365,7 +358,6 @@ const MarketingForm = ({
}; };
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => { const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
setIsLoading(true); setIsLoading(true);
console.log(values);
const updateMarketingRes = await SalesOrderApi.update( const updateMarketingRes = await SalesOrderApi.update(
initialValues?.id as number, initialValues?.id as number,
values values
@@ -381,10 +373,8 @@ const MarketingForm = ({
}; };
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => { const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
setIsLoading(true); setIsLoading(true);
console.log(initialValues?.id);
const createDeliveryRes = await DeliveryOrderApi.create(values); const createDeliveryRes = await DeliveryOrderApi.create(values);
if (isResponseSuccess(createDeliveryRes)) { if (isResponseSuccess(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.success(createDeliveryRes?.message as string); toast.success(createDeliveryRes?.message as string);
setDeliveryOrderValues( setDeliveryOrderValues(
createDeliveryRes.data?.delivery_order?.flatMap((delivery) => createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
@@ -397,20 +387,17 @@ const MarketingForm = ({
router.push(`/marketing/detail?marketingId=${initialValues?.id}`); router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
} }
if (isResponseError(createDeliveryRes)) { if (isResponseError(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.error(createDeliveryRes?.message as string); toast.error(createDeliveryRes?.message as string);
} }
setIsLoading(false); setIsLoading(false);
}; };
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => { const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
setIsLoading(true); setIsLoading(true);
console.log(initialValues?.id);
const updateDeliveryRes = await DeliveryOrderApi.update( const updateDeliveryRes = await DeliveryOrderApi.update(
initialValues?.id as number, initialValues?.id as number,
values values
); );
if (isResponseSuccess(updateDeliveryRes)) { if (isResponseSuccess(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.success(updateDeliveryRes?.message as string); toast.success(updateDeliveryRes?.message as string);
setDeliveryOrderValues( setDeliveryOrderValues(
mergeSOwithDO( mergeSOwithDO(
@@ -426,7 +413,6 @@ const MarketingForm = ({
router.push(`/marketing/detail?marketingId=${initialValues?.id}`); router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
} }
if (isResponseError(updateDeliveryRes)) { if (isResponseError(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.error(updateDeliveryRes?.message as string); toast.error(updateDeliveryRes?.message as string);
} }
setIsLoading(false); setIsLoading(false);
@@ -435,16 +421,13 @@ const MarketingForm = ({
// ================== MARKETING HANDLER ================== // ================== MARKETING HANDLER ==================
const deleteMarketingHandler = async () => { const deleteMarketingHandler = async () => {
setIsLoading(true); setIsLoading(true);
console.log(initialValues?.id);
const deleteMarketingRes = await MarketingApi.delete( const deleteMarketingRes = await MarketingApi.delete(
initialValues?.id as number initialValues?.id as number
); );
if (isResponseSuccess(deleteMarketingRes)) { if (isResponseSuccess(deleteMarketingRes)) {
console.log(deleteMarketingRes);
toast.success(deleteMarketingRes?.message as string); toast.success(deleteMarketingRes?.message as string);
} }
if (isResponseError(deleteMarketingRes)) { if (isResponseError(deleteMarketingRes)) {
console.log(deleteMarketingRes);
toast.error(deleteMarketingRes?.message as string); toast.error(deleteMarketingRes?.message as string);
} }
setIsLoading(false); setIsLoading(false);
@@ -306,7 +306,6 @@ const SupplierForm = ({
label='Hatchery' label='Hatchery'
value={hatcheryOptionsValues} value={hatcheryOptionsValues}
onChange={(val) => { onChange={(val) => {
console.log(val); // pastikan val = array of { value, label }
setHatcheryOptionValues(val as OptionType[]); setHatcheryOptionValues(val as OptionType[]);
}} }}
isError={ isError={
@@ -16,15 +16,11 @@ import { cn } from '@/lib/helper';
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { BaseApiResponse } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
import { import { ProjectFlock } from '@/types/api/production/project-flock';
ProjectFlockApprovalPayload,
ProjectFlock,
} from '@/types/api/production/project-flock';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table'; import { CellContext, SortingState } from '@tanstack/react-table';
import { ChangeEventHandler, useState } from 'react'; import { ChangeEventHandler, useRef, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import useSWR from 'swr'; 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='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'> <div className='flex flex-col sm:flex-row gap-3 w-full'>
<Button <Button
href='/production/project-flock/add'
variant='outline' variant='outline'
color='primary' color='primary'
className='w-full sm:w-fit' className='w-full sm:w-fit'
href='/production/project-flock/add'
> >
<Icon icon='ic:round-plus' width={24} height={24} /> <Icon icon='ic:round-plus' width={24} height={24} />
Tambah Tambah
@@ -24,7 +24,6 @@ import {
UpdateProjectFlockFormSchema, UpdateProjectFlockFormSchema,
} from '@/components/pages/production/project-flock/form/ProjectFlockForm.schema'; } from '@/components/pages/production/project-flock/form/ProjectFlockForm.schema';
import { import {
ProjectFlockApprovalPayload,
CreateProjectFlockPayload, CreateProjectFlockPayload,
ProjectFlock, ProjectFlock,
} from '@/types/api/production/project-flock'; } from '@/types/api/production/project-flock';
@@ -43,6 +42,7 @@ import ApprovalSteps, {
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line'; import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import Card from '@/components/Card';
interface ProjectFlockFormProps { interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail'; formType?: 'add' | 'edit' | 'detail';
@@ -259,6 +259,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(createProjectFlockRes)) { if (isResponseSuccess(createProjectFlockRes)) {
toast.success(createProjectFlockRes?.message as string); toast.success(createProjectFlockRes?.message as string);
handleReset();
router.push('/production/project-flock'); router.push('/production/project-flock');
} }
if (isResponseError(createProjectFlockRes)) { if (isResponseError(createProjectFlockRes)) {
@@ -276,6 +277,7 @@ const ProjectFlockForm = ({
if (isResponseSuccess(updateProjectFlockRes)) { if (isResponseSuccess(updateProjectFlockRes)) {
toast.success(updateProjectFlockRes?.message as string); toast.success(updateProjectFlockRes?.message as string);
handleReset();
router.push('/production/project-flock'); router.push('/production/project-flock');
} }
if (isResponseError(updateProjectFlockRes)) { if (isResponseError(updateProjectFlockRes)) {
@@ -283,6 +285,15 @@ const ProjectFlockForm = ({
toast.error(updateProjectFlockRes?.message as string); toast.error(updateProjectFlockRes?.message as string);
} }
}; };
const handleReset = () => {
formik.resetForm();
setSelectedArea('');
setSelectedLocation('');
setDisabledLocation(true);
setOpenSelectKandangs(false);
setOptionsKandang([]);
formikSetValues(formikInitialValues);
};
// Formik InitialValue // Formik InitialValue
const formikInitialValues = useMemo<ProjectFlockFormValues>(() => { const formikInitialValues = useMemo<ProjectFlockFormValues>(() => {
@@ -557,6 +568,16 @@ const ProjectFlockForm = ({
const inputPeriod = const inputPeriod =
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod; (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 ( return (
<> <>
<section className='w-full'> <section className='w-full'>
@@ -653,9 +674,13 @@ const ProjectFlockForm = ({
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
onReset={formik.handleReset} onReset={formik.handleReset}
> >
<div className='card bg-base-100 shadow w-full mb-6'> <Card
<div className='card-body'> title='Informasi Umum'
<div className='card-title mb-4'>Informasi Umum</div> variant='bordered'
className={{
wrapper: 'w-full mb-4',
}}
>
<div className='grid sm:grid-cols-2 gap-4'> <div className='grid sm:grid-cols-2 gap-4'>
<SelectInput <SelectInput
required required
@@ -695,8 +720,7 @@ const ProjectFlockForm = ({
options={optionsFlock} options={optionsFlock}
isLoading={isLoadingFlocks} isLoading={isLoadingFlocks}
isError={ isError={
formik.touched.flock_name && formik.touched.flock_name && Boolean(formik.errors.flock_name)
Boolean(formik.errors.flock_name)
} }
errorMessage={formik.errors.flock_name as string} errorMessage={formik.errors.flock_name as string}
isClearable isClearable
@@ -730,9 +754,7 @@ const ProjectFlockForm = ({
}} }}
options={optionsFcr} options={optionsFcr}
isLoading={isLoadingFcrs} isLoading={isLoadingFcrs}
isError={ isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
formik.touched.fcr_id && Boolean(formik.errors.fcr_id)
}
errorMessage={formik.errors.fcr_id as string} errorMessage={formik.errors.fcr_id as string}
isClearable isClearable
isDisabled={formType === 'detail'} isDisabled={formType === 'detail'}
@@ -759,34 +781,16 @@ const ProjectFlockForm = ({
value={selectedLocation ? inputPeriod : ''} value={selectedLocation ? inputPeriod : ''}
/> />
</div> </div>
</div> </Card>
</div> <Card
<div className='card bg-base-100 shadow w-full'> collapsible
<div className='card-body'> title='Pilih Kandang'
<Collapse variant='bordered'
title={ className={{
<div className='card-actions justify-between w-full'> wrapper: '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 <div className='overflow-x-auto duration-300 ease-in-out'>
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
/>
</Button>
</div>
}
className='sm:w-full'
titleClassName='w-full p-0!'
onOpenChange={setOpenSelectKandangs}
open={openSelectKandangs}
>
<div className='overflow-x-auto'>
{isLoadingKandang && ( {isLoadingKandang && (
<span className='loading loading-dots loading-xl'></span> <span className='loading loading-dots loading-xl'></span>
)} )}
@@ -802,9 +806,7 @@ const ProjectFlockForm = ({
initialValues={initialValues} initialValues={initialValues}
/> />
</div> </div>
</Collapse> </Card>
</div>
</div>
<div className='flex flex-row justify-center gap-2 flex-wrap my-6'> <div className='flex flex-row justify-center gap-2 flex-wrap my-6'>
{formType !== 'detail' && ( {formType !== 'detail' && (
@@ -912,4 +914,6 @@ const ProjectFlockForm = ({
); );
}; };
ProjectFlockForm.displayName = 'ProjectFlockForm';
export default ProjectFlockForm; export default ProjectFlockForm;
@@ -144,8 +144,6 @@ const ProjectFlockKandangTable = ({
accessorFn: (row) => row.location?.name, accessorFn: (row) => row.location?.name,
header: 'Periode', header: 'Periode',
cell: (props) => { cell: (props) => {
console.log('listPeriods');
console.log(listPeriods);
const period = const period =
listPeriods.length > 0 listPeriods.length > 0
? listPeriods.find((p) => p.id == props.row.original.id) ? 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]); sentPayload.set(pair[0], pair[1]);
} }
console.log({ sentPayload });
return { return {
code: 200, code: 200,
status: 'success', status: 'success',
@@ -815,8 +813,6 @@ export class ExpenseApiService extends BaseApiService<
sentPayload.set(pair[0], pair[1]); sentPayload.set(pair[0], pair[1]);
} }
console.log({ sentPayload });
return { return {
code: 200, code: 200,
status: 'success', status: 'success',