mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-279): Add functionality closing project flock
This commit is contained in:
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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 && (
|
||||
|
||||
+291
-10
@@ -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'
|
||||
);
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user