feat(FE-279): Add functionality closing project flock

This commit is contained in:
randy-ar
2025-12-05 22:55:11 +07:00
parent c69d9dd605
commit 885e4250fd
16 changed files with 1464 additions and 225 deletions
@@ -10,7 +10,7 @@ const AddChickin = () => {
return (
<>
<section className='w-full p-4'>
<section className='w-full'>
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
</section>
</>
@@ -2,7 +2,7 @@ import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
const Chickin = () => {
return (
<section className='w-full p-4'>
<section className='w-full'>
<ChickinTable />
</section>
);
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,63 @@
'use client';
import ProjectFlockClosingForm from '@/components/pages/production/project-flock/closing/ProjectFlockClosingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { ProjectFlockKandangApi } from '@/services/api/production';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const ProjectFlockClosingPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const projectFlockId = searchParams.get('projectFlockId');
const projectFlockKandangId = searchParams.get('projectFlockKandangId');
const { data: projectFlockKandang, isLoading: isLoadingProjectFlockKandang } =
useSWR(projectFlockKandangId, (id: number) =>
ProjectFlockKandangApi.getSingle(id)
);
const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR(
projectFlockId,
(id: number) => ProjectFlockApi.getSingle(id)
);
if (!projectFlockId || !projectFlockKandangId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (
!isLoadingProjectFlock &&
(!projectFlock || isResponseError(projectFlock)) &&
!isLoadingProjectFlockKandang &&
(!projectFlockKandang || isResponseError(projectFlockKandang))
) {
router.replace('/404');
return;
}
return (
<div className='w-full h-full flex flex-col justify-center'>
{isLoadingProjectFlock ||
(isLoadingProjectFlockKandang && (
<span className='loading loading-spinner loading-xl' />
))}
{isResponseSuccess(projectFlock) &&
isResponseSuccess(projectFlockKandang) && (
<ProjectFlockClosingForm
projectFlock={projectFlock.data}
projectFlockKandang={projectFlockKandang.data}
/>
)}
</div>
);
};
export default ProjectFlockClosingPage;
+2 -1
View File
@@ -19,8 +19,9 @@ export default function ProjectFlockLayout({
const isEdit = pathname.includes('/detail/edit');
const isDetail = pathname.includes('/detail');
const isChickin = pathname.includes('/chickin/add/kandang');
const isClosing = pathname.includes('/closing');
const isOpen = isAdd || isEdit || isDetail || isChickin;
const isOpen = isAdd || isEdit || isDetail || isChickin || isClosing;
const handleBackdropClick = () => {
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
+166 -33
View File
@@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
import { isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general';
// TODO: delete this later, DONT HARDCODE USER DATA
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps {
children?: ReactNode;
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth();
const {
data: userResponse,
isLoading: isLoadingUserResponse,
error: userErrorResponse,
} = useSWRImmutable<
GetMeResponse & { ok?: boolean },
AxiosError<BaseApiResponse>,
SWRHttpKey
>('/sso/userinfo', httpClientFetcher, {
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
});
const { data: userResponse, isLoading: isLoadingUserResponse } =
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
'/auth/sso/userinfo',
httpClientFetcher,
{
shouldRetryOnError: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 0,
}
);
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
useEffect(() => {
if (isResponseSuccess(userResponse)) {
setUser(userResponse.data);
} else if (
isResponseError(userErrorResponse?.response?.data) &&
typeof window !== 'undefined'
) {
router.replace(
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
}
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]);
}, [userResponse, setIsLoadingUser, setUser]);
if (isLoadingUserResponse && !userResponse && !userErrorResponse) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
// TODO: uncomment this later
// if (isLoadingUserResponse && !userResponse) {
// return (
// <div className='w-full flex flex-row justify-center items-center p-4'>
// <span className='loading loading-spinner loading-xl' />
// </div>
// );
// }
return <>{isResponseSuccess(userResponse) && children}</>;
return <>{children}</>;
};
export default RequireAuth;
@@ -0,0 +1,104 @@
'use client';
import { Icon } from '@iconify/react';
import Link from 'next/link';
import { ReactNode } from 'react';
import { cn } from '@/lib/helper';
export interface DrawerHeaderProps {
// Left side props
leftIcon?: string;
leftIconSize?: number;
leftIconHref?: string;
leftIconOnClick?: () => void;
leftIconClassName?: string;
// Subtitle/label props
subtitle?: string | ReactNode;
subtitleClassName?: string;
// Right side actions (children)
children?: ReactNode;
// Container props
className?: string;
showDivider?: boolean;
}
const DrawerHeader = ({
leftIcon = 'mdi:close',
leftIconSize = 24,
leftIconHref,
leftIconOnClick,
leftIconClassName,
subtitle,
subtitleClassName,
children,
className,
showDivider = true,
}: DrawerHeaderProps) => {
const renderLeftIcon = () => {
const iconElement = (
<Icon
icon={leftIcon}
width={leftIconSize}
height={leftIconSize}
className={cn('cursor-pointer', leftIconClassName)}
/>
);
if (leftIconHref) {
return (
<Link href={leftIconHref} className='hover:text-gray-400'>
{iconElement}
</Link>
);
}
if (leftIconOnClick) {
return (
<button
onClick={leftIconOnClick}
className='hover:text-gray-400 bg-transparent border-none p-0'
>
{iconElement}
</button>
);
}
return iconElement;
};
return (
<div
className={cn(
'flex flex-row justify-between items-center px-4 pt-4',
className
)}
>
{/* Left Side */}
<div className='flex flex-row h-full gap-2 items-center'>
{renderLeftIcon()}
{showDivider && subtitle && (
<div className='divider divider-horizontal p-0 m-0'></div>
)}
{subtitle && (
<div className={cn('text-sm text-neutral', subtitleClassName)}>
{subtitle}
</div>
)}
</div>
{/* Right Side Actions */}
{children && (
<div className='flex flex-row gap-3 justify-end items-center'>
{children}
</div>
)}
</div>
);
};
export default DrawerHeader;
+174 -75
View File
@@ -1,6 +1,11 @@
'use client';
import { ChangeEventHandler, ReactNode } from 'react';
import {
ChangeEventHandler,
ReactNode,
createContext,
useContext,
} from 'react';
import { cn } from '@/lib/helper';
export interface RadioOption {
@@ -8,37 +13,74 @@ export interface RadioOption {
value: string;
}
export interface RadioInputProps {
label?: string;
bottomLabel?: string;
// DaisyUI Radio Colors
export type RadioColor =
| 'neutral'
| 'primary'
| 'secondary'
| 'accent'
| 'success'
| 'warning'
| 'info'
| 'error';
// DaisyUI Radio Sizes
export type RadioSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
// Context untuk RadioGroup
interface RadioGroupContextValue {
name: string;
value?: string;
options: RadioOption[];
variant?: string;
className?: {
wrapper?: string;
label?: string;
radioWrapper?: string;
radio?: string;
};
isError?: boolean;
isValid?: boolean;
errorMessage?: string;
required?: boolean;
color?: RadioColor;
size?: RadioSize;
disabled?: boolean;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
}
const RadioInput = ({
const RadioGroupContext = createContext<RadioGroupContextValue | undefined>(
undefined
);
const useRadioGroup = () => {
const context = useContext(RadioGroupContext);
if (!context) {
throw new Error('RadioGroupItem must be used within RadioGroup');
}
return context;
};
// RadioGroup Component
export interface RadioGroupProps {
label?: string;
bottomLabel?: string;
name: string;
value?: string;
options?: RadioOption[];
color?: RadioColor;
size?: RadioSize;
className?: {
wrapper?: string;
label?: string;
radioWrapper?: string;
};
isError?: boolean;
errorMessage?: string;
required?: boolean;
disabled?: boolean;
onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
children?: ReactNode;
}
export const RadioGroup = ({
label,
bottomLabel,
name,
value,
options,
variant = 'radio-primary',
color = 'primary',
size = 'md',
className,
isError,
errorMessage,
@@ -46,68 +88,125 @@ const RadioInput = ({
disabled = false,
onChange,
onBlur,
}: RadioInputProps) => {
return (
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
{/* Label atas */}
{label && (
<label
className={cn(
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
{label}
{required && (
<span className='text-error ml-1' title='required'>
*
</span>
)}
</label>
)}
children,
}: RadioGroupProps) => {
const contextValue: RadioGroupContextValue = {
name,
value,
color,
size,
disabled,
onChange,
onBlur,
};
{/* Daftar opsi radio */}
<div
className={cn(
'flex flex-row flex-wrap gap-4 items-center',
className?.radioWrapper
)}
>
{options.map((option) => (
return (
<RadioGroupContext.Provider value={contextValue}>
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
{/* Label atas */}
{label && (
<label
key={option.value}
className={cn(
'flex flex-row items-center gap-2 cursor-pointer',
disabled && 'opacity-60 cursor-not-allowed'
'w-full text-sm font-normal leading-5',
{ 'text-error': isError },
className?.label
)}
>
<input
type='radio'
name={name}
value={option.value}
checked={value === option.value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
className={cn('radio', variant, className?.radio)}
/>
<span className='text-sm'>{option.label}</span>
{label}
{required && (
<span className='text-error ml-1' title='required'>
*
</span>
)}
</label>
))}
)}
{/* Daftar opsi radio */}
<div
className={cn(
'flex flex-row flex-wrap gap-4 items-center',
className?.radioWrapper
)}
>
{/* Jika options diberikan, render otomatis */}
{options &&
options.map((option) => (
<RadioGroupItem
key={option.value}
value={option.value}
label={option.label}
/>
))}
{/* Atau gunakan children untuk custom rendering */}
{children}
</div>
{/* Label bawah */}
{!isError && bottomLabel && (
<p className='text-sm opacity-60'>{bottomLabel}</p>
)}
{/* Pesan error */}
{isError && errorMessage && (
<p className='text-sm text-error'>{errorMessage}</p>
)}
</div>
{/* Label bawah */}
{!isError && bottomLabel && (
<p className='text-sm opacity-60'>{bottomLabel}</p>
)}
{/* Pesan error */}
{isError && errorMessage && (
<p className='text-sm text-error'>{errorMessage}</p>
)}
</div>
</RadioGroupContext.Provider>
);
};
export default RadioInput;
// RadioGroupItem Component
export interface RadioGroupItemProps {
value: string;
label?: string;
className?: string;
disabled?: boolean;
color?: RadioColor;
size?: RadioSize;
}
export const RadioGroupItem = ({
value,
label,
className,
disabled: itemDisabled,
color: itemColor,
size: itemSize,
}: RadioGroupItemProps) => {
const {
name,
value: groupValue,
color: groupColor,
size: groupSize,
disabled: groupDisabled,
onChange,
onBlur,
} = useRadioGroup();
const isDisabled = itemDisabled ?? groupDisabled;
const radioColor = itemColor ?? groupColor;
const radioSize = itemSize ?? groupSize;
return (
<label
className={cn(
'flex flex-row items-center gap-2 cursor-pointer',
isDisabled && 'opacity-60 cursor-not-allowed',
className
)}
>
<input
type='radio'
name={name}
value={value}
checked={groupValue === value}
onChange={onChange}
onBlur={onBlur}
disabled={isDisabled}
className={cn('radio', `radio-${radioColor}`, `radio-${radioSize}`)}
/>
{label && <span className='text-sm'>{label}</span>}
</label>
);
};
@@ -24,7 +24,7 @@ import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import TextInput from '@/components/input/TextInput';
import RadioInput from '@/components/input/RadioInput';
import { RadioGroup } from '@/components/input/RadioInput';
import TextArea from '@/components/input/TextArea';
interface InventoryAdjustmentFormProps {
@@ -347,7 +347,7 @@ const InventoryAdjustmentForm = ({
/>
{/* Radio Button Flag Stock */}
<RadioInput
<RadioGroup
name='transaction_type'
label='Tipe Transaksi'
options={[
@@ -367,7 +367,7 @@ const InventoryAdjustmentForm = ({
Boolean(formik.errors.transaction_type)
}
errorMessage={formik.errors.transaction_type as string}
variant='radio-primary'
color='primary'
required
bottomLabel={
formik.values.transaction_type == undefined
@@ -46,7 +46,7 @@ const ChickinFormKandang = ({
<div className='flex flex-col gap-4'>
<FormHeader
title={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`}
backUrl={`/production/project-flock/chickin/add?projectFlockId=${initialValues?.project_flock?.id}`}
backUrl={`/production/project-flock/detail?projectFlockId=${initialValues?.project_flock?.id}`}
/>
{approvals && !approvalsLoading && (
@@ -10,7 +10,7 @@ import SelectInput, {
import PillBadge from '@/components/PillBadge';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { cn, formatDate, formatTitleCase } from '@/lib/helper';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { ProjectFlockKandangApi } from '@/services/api/production';
import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -21,6 +21,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { FormHeader } from '@/components/helper/form/FormHeader';
import Link from 'next/link';
const ProjectFlockChickinDetail = ({
projectFlockId,
@@ -101,11 +102,26 @@ const ProjectFlockChickinDetail = ({
}, [projectFlockId, listProjectFlock]);
return (
<>
<FormHeader
{/* Header */}
<div className='flex flex-row justify-between items-center px-4 py-4'>
<div className='flex flex-row items-center h-full gap-2'>
<Link
href={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
className='hover:text-gray-400'
>
<Icon icon='mdi:arrow-left' width={24} height={24} />
</Link>
<div className='divider divider-horizontal p-0 m-0'></div>
<div className='text-sm text-neutral'>
Chick In {projectFlock?.flock_name}
</div>
</div>
</div>
{/* <FormHeader
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
backUrl='/production/project-flock'
/>
<div className='flex flex-col gap-4 w-full my-4'>
backUrl={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
/> */}
{/* <div className='flex flex-col gap-4 w-full my-4'>
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
<SelectInput
required
@@ -145,8 +161,129 @@ const ProjectFlockChickinDetail = ({
}
/>
</div>
</div>
<Card
</div> */}
{/* Informasi Umum */}
{projectFlock && (
<div className='border-t-1 border-gray-300'>
<div className='p-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Informasi Umum</h2>
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color={
projectFlock.approval.step_number == 1
? 'neutral'
: projectFlock.approval.step_number == 2
? 'success'
: projectFlock.approval.step_number >= 3
? 'error'
: undefined
}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
projectFlock.approval.step_number == 1
? 'neutral'
: projectFlock.approval.step_number == 2
? 'success'
: projectFlock.approval.step_number >= 3
? 'error'
: undefined
}
/>{' '}
{projectFlock.approval.step_name}
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='neutral'
variant='soft'
className={{ badge: 'rounded-lg px-2' }}
>
<Icon icon='mdi:bookmark' width={12} height={12} />
{` ${formatTitleCase(projectFlock.category)}`}
</Badge>
</div>
{/* Information Grid */}
<div className='grid grid-cols-3 gap-4'>
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:account' /> Submitted
</div>
<div className='col-span-2'>
<Badge
variant='soft'
color='neutral'
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon icon='mdi:account-circle' width={14} height={14} />{' '}
{projectFlock.created_user.name}
</Badge>
</div>
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon={'mdi:clock'} /> History
</div>
<div className='col-span-2'>
<Button variant='outline' className='py-1 text-sm'>
See History{' '}
<Icon
icon='mdi:arrow-top-right-thin'
width={11}
height={11}
/>
</Button>
</div>
{/* BARIS 1 */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
</div>
<div className='col-span-2'>{projectFlock.area.name}</div>
{/* BARIS 2 */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
</div>
<div className='col-span-2'>{projectFlock.location.name}</div>
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
</div>
<div className='col-span-2'>{projectFlock.fcr.name}</div>
{/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:circle-slice-8' />{' '}
Kategori
</div>
<div className='col-span-2'>
{formatTitleCase(projectFlock.category)}
</div>
</div>
</div>
</div>
)}
{/* <Card
title='Informasi Flock'
className={{
wrapper: 'w-full bg-white mb-3',
@@ -231,8 +368,152 @@ const ProjectFlockChickinDetail = ({
paginationClassName: 'hidden',
}}
/>
</Card>
<Card
</Card> */}
{/* Card Kandangs */}
<div className='border-t-1 border-gray-300'>
<div className='p-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Daftar Kandang</h2>
{isResponseSuccess(listProjectFlock) ? (
<>
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color={'success'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={'success'}
/>{' '}
Disetujui (
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.filter(
(k) => k.approval?.step_number == 1
).length}
)
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
variant='soft'
color={'neutral'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={'neutral'}
/>{' '}
Pengajuan (
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.filter(
(k) => k.approval?.step_number == 2
).length}
)
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='error'
variant='soft'
className={{ badge: 'rounded-lg px-2' }}
>
<Icon
icon={`mdi:circle`}
width={12}
height={12}
color='error'
/>
Belum Chickin (
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.filter(
(k) => k.approval == null
).length}
)
</Badge>
</div>
{/* Card Kandang */}
<Card
variant='bordered'
className={{
wrapper: 'w-full',
body: 'p-3',
}}
>
<div className='flex flex-col gap-6'>
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.map((kandang) => (
<div
key={kandang.id}
className='flex flex-row justify-between items-center'
>
<div className='flex flex-row gap-2 items-center cursor-pointer text-gray-400'>
<Badge
variant='soft'
color={
kandang.approval
? kandang.approval.step_number == 1
? 'success'
: 'neutral'
: 'error'
}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
kandang.approval
? kandang.approval.step_number == 1
? 'success'
: 'neutral'
: 'gray'
}
/>
</Badge>
<span className='font-semibold'>
{kandang.kandang.name}
</span>
</div>
<Button
variant='outline'
className='py-1 text-sm'
onClick={() => {
handleChickinClick(kandang);
}}
disabled={projectFlock?.approval?.step_number === 1}
>
Chick In{' '}
<Icon
icon='mdi:arrow-top-right-thin'
width={11}
height={11}
/>
</Button>
</div>
))}
</div>
</Card>
</>
) : (
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
Pilih project flock terlebih dahulu...
</span>
</div>
)}
</div>
</div>
{/* <Card
title='Daftar Kandang'
className={{
wrapper: 'w-full bg-white',
@@ -351,7 +632,7 @@ const ProjectFlockChickinDetail = ({
paginationClassName: 'hidden',
}}
/>
</Card>
</Card> */}
</>
);
};
@@ -0,0 +1,297 @@
'use client';
import Button from '@/components/Button';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import Table from '@/components/Table';
import Badge from '@/components/Badge';
import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProjectFlock } from '@/types/api/production/project-flock';
import {
ClosingExpense,
ProjectFlockKandang,
} from '@/types/api/production/project-flock-kandang';
import { Purchase } from '@/types/api/purchase/purchase';
import { Icon } from '@iconify/react';
import useSWR from 'swr';
import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useMemo, useState } from 'react';
import toast from 'react-hot-toast';
const ProjectFlockClosingForm = ({
projectFlock,
projectFlockKandang,
}: {
projectFlock: ProjectFlock;
projectFlockKandang: ProjectFlockKandang;
}) => {
const closeModal = useModal();
const isCanClose = projectFlock.approval.step_number <= 2;
const [isClosingLoading, setIsClosingLoading] = useState(false);
const { data: closingData, isLoading } = useSWR(
`${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`,
() => ProjectFlockKandangApi.checkClosing(projectFlockKandang.id)
);
const confirmationModalCloseClickHandler = async () => {
setIsClosingLoading(true);
const deleteProjectFlockRes = await ProjectFlockKandangApi.closing(
projectFlock?.id as number,
{
closed_date: formatDate(new Date(), 'yyyy-MM-dd'),
action: isCanClose ? 'close' : 'unclose',
}
);
if (isResponseSuccess(deleteProjectFlockRes)) {
toast.success(deleteProjectFlockRes?.message as string);
}
if (isResponseError(deleteProjectFlockRes)) {
toast.error(deleteProjectFlockRes?.message as string);
}
setIsClosingLoading(false);
closeModal.closeModal();
};
const errorStock = useMemo(() => {
return isResponseSuccess(closingData)
? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0)
: false;
}, [closingData]);
const errorExpense = useMemo(() => {
return isResponseSuccess(closingData)
? closingData?.data?.expenses.every((expense) => expense.step < 5)
: false;
}, [closingData]);
const isCanCloseValid = !errorStock && !errorExpense;
return (
<>
<DrawerHeader
leftIcon='mdi:arrow-left'
leftIconHref={`/production/project-flock/detail?projectFlockId=${projectFlock.id}`}
subtitle={`Close ${projectFlock.flock_name}`}
></DrawerHeader>
{/* Informasi Kandang */}
<div className='divider'></div>
<div className='px-4 pb-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Informasi Kandang</h2>
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color='success'
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon icon='mdi:circle' width={12} height={12} color='success' />{' '}
Aktif
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='neutral'
variant='soft'
className={{ badge: 'rounded-lg px-2' }}
>
<Icon icon='mdi:home' width={12} height={12} />
{` Kapasitas ${formatNumber(projectFlockKandang.kandang.capacity)} Ekor`}
</Badge>
</div>
{/* Information Grid */}
<div className='grid grid-cols-3 gap-4'>
{/* Area */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
</div>
<div className='col-span-2'>{projectFlock.area.name}</div>
{/* Lokasi */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
</div>
<div className='col-span-2'>{projectFlock.location.name}</div>
{/* Kandang */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Kandang
</div>
<div className='col-span-2'>{projectFlockKandang.kandang.name}</div>
{/* Jumlah DOC */}
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Jumlah DOC
</div>
<div className='col-span-2'>
{formatNumber(
projectFlockKandang.chickins?.reduce(
(total, chickin) => total + chickin.usage_qty,
0
) ?? 0
)}{' '}
Ekor
</div>
</div>
</div>
{/* Table Biaya */}
<div className='divider'></div>
<div className='px-4 pb-4'>
<h2 className='text-2xl font-semibold'>Biaya</h2>
<Table<ClosingExpense>
data={
isResponseSuccess(closingData) ? closingData.data?.expenses : []
}
columns={[
{
header: 'PO Number',
accessorKey: 'po_number',
},
{
header: 'Total',
accessorKey: 'total',
},
{
header: 'Status',
accessorKey: 'status',
cell(props) {
return (
<Badge
className={{
badge: 'rounded-lg',
}}
variant='soft'
color={
props.row.original.step < 5
? props.row.original.step == 1
? 'neutral'
: 'success'
: 'error'
}
>
{formatTitleCase(props.row.original.status)}
</Badge>
);
},
},
]}
className={{
containerClassName: cn('my-4'),
tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120',
tableClassName: 'font-inter w-full table-sm min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-3 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-3 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
{errorExpense && (
<div className='text-center text-error'>
*Pastikan semua biaya sudah selesai sebelum melakukan closing.
</div>
)}
</div>
{/* Table Persediaan Gudang */}
<div className='divider'></div>
<div className='px-4 pb-4'>
<h2 className='text-2xl font-semibold'>Persediaan Gudang</h2>
<Table<ProductWarehouse>
data={
isResponseSuccess(closingData)
? closingData.data?.stock_remaining
: []
}
columns={[
{
header: 'Product',
accessorKey: 'product.name',
},
{
header: 'Kategori',
accessorKey: 'product.product_category.name',
},
{
header: 'Quantity',
accessorKey: 'quantity',
},
{
header: 'UOM',
accessorKey: 'product.uom.name',
},
]}
className={{
containerClassName: cn('my-4'),
tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120',
tableClassName: 'font-inter w-full table-sm min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-3 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-3 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
{errorStock && (
<div className='text-center text-error'>
*Masih ada sisa stock yang belum dihabiskan.
</div>
)}
</div>
<div className='p-4 mt-6'>
<Button
className='w-full'
color='error'
isLoading={isLoading}
disabled={!isCanCloseValid}
onClick={() => closeModal.openModal()}
>
<Icon icon='mdi:checkbox-marked-circle-outline' />{' '}
{isCanClose ? 'Close' : 'Unclose'}
</Button>
</div>
<ConfirmationModal
ref={closeModal.ref}
type='error'
text={`Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isClosingLoading,
onClick: confirmationModalCloseClickHandler,
}}
/>
</>
);
};
export default ProjectFlockClosingForm;
@@ -1,7 +1,9 @@
import Badge from '@/components/Badge';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
import Tooltip from '@/components/Tooltip';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import {
formatCurrency,
formatDate,
@@ -13,6 +15,11 @@ import { Icon } from '@iconify/react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import toast from 'react-hot-toast';
const ProjectFlockDetail = ({
projectFlock,
@@ -20,55 +27,60 @@ const ProjectFlockDetail = ({
projectFlock: ProjectFlock;
}) => {
const router = useRouter();
const deleteModal = useModal();
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [openBudgets, setOpenBudget] = useState(false);
const [selectedKandangId, setSelectedKamdangId] = useState<string | null>(
null
);
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
const deleteProjectFlockRes = await ProjectFlockApi.delete(
projectFlock?.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);
};
return (
<>
<div className='h-full w-full flex flex-col gap-4'>
{/* Header */}
<div className='flex flex-row justify-between items-center px-4 pt-4'>
<div className='flex flex-row h-full gap-2'>
<Link
href={`/production/project-flock`}
className='hover:text-gray-400'
>
<Icon icon='mdi:close' width={24} height={24} />
</Link>
<div className='divider divider-horizontal p-0 m-0'></div>
<div className='text-sm text-neutral'>
Created On {formatDate(projectFlock.created_at, 'MMM DD, YYYY')}
</div>
</div>
<div className='flex flex-row gap-3 justify-end'>
{projectFlock?.approval?.step_number == 2 && (
<Link
href={`/production/project-flock/chickin/add?projectFlockId=${projectFlock?.id}`}
className='text-success'
>
<Tooltip content='Chick In' position='bottom'>
<Icon
icon='mdi:checkbox-marked-outline'
width={20}
height={20}
data-tip={'Chick In'}
/>
</Tooltip>
</Link>
)}
<Link
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
>
<Tooltip content='Edit' position='bottom'>
<DrawerHeader
leftIcon='mdi:close'
leftIconHref='/production/project-flock'
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`}
>
<Link
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
className='p-0'
>
<Tooltip content='Edit' position='bottom'>
<Button variant='link' className='p-0 text-neutral'>
<Icon icon='mdi:square-edit-outline' width={20} height={20} />
</Tooltip>
</Link>
<Button variant='link' className='p-0 text-error'>
<Tooltip content='Hapus' position='bottom'>
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
</Tooltip>
</Button>
</div>
</div>
</Button>
</Tooltip>
</Link>
<Button
variant='link'
className='p-0 text-error'
onClick={() => {
deleteModal.openModal();
}}
>
<Tooltip content='Hapus' position='bottom'>
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
</Tooltip>
</Button>
</DrawerHeader>
{/* Informasi Umum */}
<div className='border-t-1 border-gray-300'>
@@ -79,11 +91,11 @@ const ProjectFlockDetail = ({
<Badge
variant='soft'
color={
projectFlock.approval.step_number == 1
projectFlock.approval?.step_number == 1
? 'neutral'
: projectFlock.approval.step_number == 2
: projectFlock.approval?.step_number == 2
? 'success'
: projectFlock.approval.step_number >= 3
: projectFlock.approval?.step_number >= 3
? 'error'
: undefined
}
@@ -96,16 +108,16 @@ const ProjectFlockDetail = ({
width={12}
height={12}
color={
projectFlock.approval.step_number == 1
projectFlock.approval?.step_number == 1
? 'neutral'
: projectFlock.approval.step_number == 2
: projectFlock.approval?.step_number == 2
? 'success'
: projectFlock.approval.step_number >= 3
: projectFlock.approval?.step_number >= 3
? 'error'
: undefined
}
/>{' '}
{projectFlock.approval.step_name}
{projectFlock.approval?.step_name}
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
@@ -307,27 +319,85 @@ const ProjectFlockDetail = ({
body: 'p-3',
}}
>
<div className='flex flex-col gap-6'>
<RadioGroup
name='gender'
className={{
radioWrapper: 'grid grid-cols-1 gap-6',
}}
onChange={(e) => setSelectedKamdangId(e.target.value)}
value={selectedKandangId?.toString()}
size='md'
color='neutral'
>
{projectFlock.kandangs.map((kandang) => (
<div
key={kandang.id}
className='flex flex-row justify-between items-center'
className={`grid grid-cols-2 gap-6 cursor-pointer hover:text-gray-800`}
onClick={() => setSelectedKamdangId(kandang.id.toString())}
>
<div className='flex flex-row gap-2 items-center cursor-pointer text-gray-400'>
<Icon icon={'mdi:drag'} width={21} height={21} />{' '}
<span className='font-semibold'>{kandang.name}</span>
</div>
<div className='text-end text-gray-400'>
Created On{' '}
{formatDate(projectFlock.created_at, 'MMM DD, YYYY')}
<RadioGroupItem
value={kandang.id.toString()}
label={kandang.name}
/>
<div className='text-end'>
<Badge
className={{
badge: 'rounded-lg',
}}
>
Kapasitas {kandang.capacity} Ekor
</Badge>
</div>
</div>
))}
</div>
</RadioGroup>
</Card>
<div className='grid grid-cols-4 gap-3'>
<Link
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandangId}&projectFlockId=${projectFlock.id}`}
className='m-0 p-0'
>
<Button
className='w-full px-2 py-1 text-sm'
variant='outline'
color='success'
disabled={!selectedKandangId}
>
Chickin <Icon icon='mdi:checkbox-marked-outline' />
</Button>
</Link>
<Link
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandangId}`}
className='m-0 p-0'
>
<Button
className='w-full px-2 py-1 text-sm'
variant='outline'
color='error'
disabled={!selectedKandangId}
>
Close <Icon icon='mdi:checkbox-marked-circle-outline' />
</Button>
</Link>
</div>
</div>
</div>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${projectFlock?.flock_name} - ${projectFlock?.area?.name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</>
);
};
@@ -48,6 +48,8 @@ import ProjectFlockKandangTable from '@/components/pages/production/project-floc
import { Nonstock } from '@/types/api/master-data/nonstock';
import { useUiStore } from '@/stores/ui/ui.store';
import Link from 'next/link';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { formatDate } from '@/lib/helper';
interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail';
@@ -675,28 +677,20 @@ const ProjectFlockForm = ({
<>
<section className='w-full'>
{/* Header */}
<div className='flex flex-row justify-between items-center px-4 pt-4'>
<div className='flex flex-row h-full gap-2'>
<Link
href={
formType == 'add'
? '/production/project-flock'
: `/production/project-flock/detail?projectFlockId=${initialValues?.id}`
}
className='hover:text-gray-400'
>
<Icon
icon={formType == 'add' ? 'mdi:close' : 'mdi:arrow-left'}
width={24}
height={24}
/>
</Link>
<div className='divider divider-horizontal p-0 m-0'></div>
<div className='text-sm text-neutral'>
{formType == 'add' ? 'Add Flock' : 'Update Flock'}
</div>
</div>
<div className='flex flex-row justify-end'>
<DrawerHeader
leftIcon={formType == 'add' ? 'mdi:close' : 'mdi:arrow-left'}
leftIconSize={24}
leftIconHref={
formType == 'add'
? '/production/project-flock'
: `/production/project-flock/detail?projectFlockId=${initialValues?.id}`
}
leftIconClassName='hover:text-gray-400'
subtitle={formType == 'add' ? 'Add Flock' : 'Update Flock'}
subtitleClassName='text-sm text-neutral'
showDivider
>
{formType == 'edit' && (
<Button
onClick={() => {
if (initialValues?.id) {
@@ -713,8 +707,8 @@ const ProjectFlockForm = ({
className='justify-start text-sm'
/>
</Button>
</div>
</div>
)}
</DrawerHeader>
{projectFlockFormErrorMessage && (
<div className='my-4'>
<div role='alert' className='alert alert-error'>
@@ -770,21 +764,6 @@ const ProjectFlockForm = ({
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button>
{initialValues?.approval?.step_number == 2 && (
<Button
variant='outline'
color='success'
className='w-full sm:w-fit'
onClick={() => {
router.push(
`/production/project-flock/chickin/add?projectFlockId=${initialValues?.id}`
);
}}
>
<Icon icon='mdi:home-import-outline' width={18} height={18} />
Chickin
</Button>
)}
</div>
)}
<form
@@ -2,10 +2,189 @@ import { BaseApiService } from '@/services/api/base';
import {
BaseProjectFlockKandang,
ProjectFlockKandang,
ClosingProjectFlockKandangPayload,
CheckClosingResponse,
} from '@/types/api/production/project-flock-kandang';
import { BaseApiResponse } from '@/types/api/api-general';
import { httpClient } from '@/services/http/client';
import axios from 'axios';
export const ProjectFlockKandangApi = new BaseApiService<
export class ProjectFlockKandangService extends BaseApiService<
BaseProjectFlockKandang,
ProjectFlockKandang,
unknown
>('project-flock-kandang');
> {
constructor(basePath: string = '') {
super(basePath);
}
/**
* Close or Unclose Project Flock Kandang
*/
async closing(
id: number,
payload: ClosingProjectFlockKandangPayload
): Promise<BaseApiResponse<{ message: string }> | undefined> {
try {
const path = `${this.basePath}/${id}/closing`;
const headers = {
'Content-Type': 'application/json',
...(this.header ?? {}),
};
return await httpClient<BaseApiResponse<{ message: string }>>(path, {
method: 'POST',
body: payload,
headers,
});
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<{ message: string }>>(error)) {
return error.response?.data;
}
return undefined;
}
}
/**
* Check Closing Requirements for Project Flock Kandang
* TODO: Replace with actual API call when backend is ready
*/
async checkClosing(
id: number
): Promise<BaseApiResponse<CheckClosingResponse> | undefined> {
// Dummy data - replace with actual API call when backend is ready
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 200,
status: 'success',
message: 'Cek persyaratan closing kandang',
data: {
unfinished_expenses: 2,
stock_remaining: [
{
id: 1,
product_id: 1,
warehouse_id: 1,
quantity: 0,
product: {
id: 1,
name: 'Pakan Starter',
brand: 'Brand A',
sku: 'PKN-STR-001',
product_price: 15000,
selling_price: 17000,
tax: 0,
expiry_period: 365,
flags: ['active'],
uom: {
id: 1,
name: 'Kg',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
},
product_category: {
id: 1,
name: 'Pakan',
code: 'PKN',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
},
suppliers: [],
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
},
warehouse: {
id: 1,
name: 'Gudang Utama',
type: 'AREA',
area: {
id: 1,
name: 'Area 1',
},
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
},
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin User',
},
created_at: '2025-01-01',
updated_at: '2025-01-01',
},
],
expenses: [
{
id: 1,
po_number: 'PO-BOP-LTI-00001',
category: 'NON-BOP',
total: 110000,
status: 'SELESAI',
step_name: 'Approval Finance',
step: 5,
reference_number: 'BOP-LTI-00001',
},
{
id: 3,
po_number: 'PO-BOP-LTI-00003',
category: 'BOP',
total: 110000,
status: 'SELESAI',
step_name: 'Approval Finance',
step: 5,
reference_number: 'BOP-LTI-00003',
},
],
},
});
}, 500); // Simulate network delay
});
/*
// Original API call - uncomment when backend is ready
try {
const path = `${this.basePath}/${id}/closing/check`;
return await httpClient<BaseApiResponse<CheckClosingResponse>>(path, {
method: 'GET',
});
} catch (error: unknown) {
if (axios.isAxiosError<BaseApiResponse<CheckClosingResponse>>(error)) {
return error.response?.data;
}
return undefined;
}
*/
}
}
export const ProjectFlockKandangApi = new ProjectFlockKandangService(
'/production/project-flock-kandangs'
);
+22
View File
@@ -39,3 +39,25 @@ export type LookupProjectFlockKandangPayload = {
project_flock_id: number;
kandang_id: number;
};
export type ClosingProjectFlockKandangPayload = {
action: 'close' | 'unclose';
closed_date?: string; // YYYY-MM-DD, DD-MM-YYYY, or RFC3339
};
export type ClosingExpense = {
id: number;
po_number: string;
category: string;
total: number;
status: string;
step_name: string;
step: number;
reference_number: string;
};
export type CheckClosingResponse = {
unfinished_expenses: number;
stock_remaining: ProductWarehouse[];
expenses: ClosingExpense[];
};