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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-4'>
|
<section className='w-full'>
|
||||||
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
|
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
|
|||||||
|
|
||||||
const Chickin = () => {
|
const Chickin = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full p-4'>
|
<section className='w-full'>
|
||||||
<ChickinTable />
|
<ChickinTable />
|
||||||
</section>
|
</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 isEdit = pathname.includes('/detail/edit');
|
||||||
const isDetail = pathname.includes('/detail');
|
const isDetail = pathname.includes('/detail');
|
||||||
const isChickin = pathname.includes('/chickin/add/kandang');
|
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 handleBackdropClick = () => {
|
||||||
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
||||||
|
|||||||
@@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable';
|
|||||||
|
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
|
import { GetMeResponse } from '@/types/api/api-general';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
// 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 {
|
interface RequireAuthProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { setUser, setIsLoadingUser } = useAuth();
|
const { setUser, setIsLoadingUser } = useAuth();
|
||||||
|
|
||||||
const {
|
const { data: userResponse, isLoading: isLoadingUserResponse } =
|
||||||
data: userResponse,
|
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
|
||||||
isLoading: isLoadingUserResponse,
|
'/auth/sso/userinfo',
|
||||||
error: userErrorResponse,
|
httpClientFetcher,
|
||||||
} = useSWRImmutable<
|
{
|
||||||
GetMeResponse & { ok?: boolean },
|
shouldRetryOnError: false,
|
||||||
AxiosError<BaseApiResponse>,
|
revalidateOnFocus: false,
|
||||||
SWRHttpKey
|
revalidateOnReconnect: false,
|
||||||
>('/sso/userinfo', httpClientFetcher, {
|
refreshInterval: 0,
|
||||||
shouldRetryOnError: false,
|
}
|
||||||
revalidateOnFocus: false,
|
);
|
||||||
revalidateOnReconnect: false,
|
|
||||||
refreshInterval: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoadingUser(isLoadingUserResponse);
|
setIsLoadingUser(isLoadingUserResponse);
|
||||||
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isResponseSuccess(userResponse)) {
|
if (isResponseSuccess(userResponse)) {
|
||||||
setUser(userResponse.data);
|
setUser(userResponse.data);
|
||||||
} else if (
|
} else {
|
||||||
isResponseError(userErrorResponse?.response?.data) &&
|
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
|
||||||
typeof window !== 'undefined'
|
// TODO: remove this later, DONT HARDCODE USER DATA
|
||||||
) {
|
setUser(DUMMY_USER);
|
||||||
router.replace(
|
|
||||||
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]);
|
}, [userResponse, setIsLoadingUser, setUser]);
|
||||||
|
|
||||||
if (isLoadingUserResponse && !userResponse && !userErrorResponse) {
|
// TODO: uncomment this later
|
||||||
return (
|
// if (isLoadingUserResponse && !userResponse) {
|
||||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
// return (
|
||||||
<span className='loading loading-spinner loading-xl' />
|
// <div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
</div>
|
// <span className='loading loading-spinner loading-xl' />
|
||||||
);
|
// </div>
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
return <>{isResponseSuccess(userResponse) && children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireAuth;
|
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';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, ReactNode } from 'react';
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
export interface RadioOption {
|
export interface RadioOption {
|
||||||
@@ -8,37 +13,74 @@ export interface RadioOption {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadioInputProps {
|
// DaisyUI Radio Colors
|
||||||
label?: string;
|
export type RadioColor =
|
||||||
bottomLabel?: string;
|
| '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;
|
name: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
options: RadioOption[];
|
color?: RadioColor;
|
||||||
variant?: string;
|
size?: RadioSize;
|
||||||
className?: {
|
|
||||||
wrapper?: string;
|
|
||||||
label?: string;
|
|
||||||
radioWrapper?: string;
|
|
||||||
radio?: string;
|
|
||||||
};
|
|
||||||
isError?: boolean;
|
|
||||||
isValid?: boolean;
|
|
||||||
errorMessage?: string;
|
|
||||||
required?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
startAdornment?: ReactNode;
|
|
||||||
endAdornment?: ReactNode;
|
|
||||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
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,
|
label,
|
||||||
bottomLabel,
|
bottomLabel,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
variant = 'radio-primary',
|
color = 'primary',
|
||||||
|
size = 'md',
|
||||||
className,
|
className,
|
||||||
isError,
|
isError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -46,68 +88,125 @@ const RadioInput = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
}: RadioInputProps) => {
|
children,
|
||||||
return (
|
}: RadioGroupProps) => {
|
||||||
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
const contextValue: RadioGroupContextValue = {
|
||||||
{/* Label atas */}
|
name,
|
||||||
{label && (
|
value,
|
||||||
<label
|
color,
|
||||||
className={cn(
|
size,
|
||||||
'w-full text-sm font-normal leading-5',
|
disabled,
|
||||||
{ 'text-error': isError },
|
onChange,
|
||||||
className?.label
|
onBlur,
|
||||||
)}
|
};
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{required && (
|
|
||||||
<span className='text-error ml-1' title='required'>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Daftar opsi radio */}
|
return (
|
||||||
<div
|
<RadioGroupContext.Provider value={contextValue}>
|
||||||
className={cn(
|
<div className={cn('w-full flex flex-col gap-2', className?.wrapper)}>
|
||||||
'flex flex-row flex-wrap gap-4 items-center',
|
{/* Label atas */}
|
||||||
className?.radioWrapper
|
{label && (
|
||||||
)}
|
|
||||||
>
|
|
||||||
{options.map((option) => (
|
|
||||||
<label
|
<label
|
||||||
key={option.value}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-row items-center gap-2 cursor-pointer',
|
'w-full text-sm font-normal leading-5',
|
||||||
disabled && 'opacity-60 cursor-not-allowed'
|
{ 'text-error': isError },
|
||||||
|
className?.label
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
{label}
|
||||||
type='radio'
|
{required && (
|
||||||
name={name}
|
<span className='text-error ml-1' title='required'>
|
||||||
value={option.value}
|
*
|
||||||
checked={value === option.value}
|
</span>
|
||||||
onChange={onChange}
|
)}
|
||||||
onBlur={onBlur}
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn('radio', variant, className?.radio)}
|
|
||||||
/>
|
|
||||||
<span className='text-sm'>{option.label}</span>
|
|
||||||
</label>
|
</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>
|
</div>
|
||||||
|
</RadioGroupContext.Provider>
|
||||||
{/* 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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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 { Icon } from '@iconify/react';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import RadioInput from '@/components/input/RadioInput';
|
import { RadioGroup } from '@/components/input/RadioInput';
|
||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
|
|
||||||
interface InventoryAdjustmentFormProps {
|
interface InventoryAdjustmentFormProps {
|
||||||
@@ -347,7 +347,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Radio Button Flag Stock */}
|
{/* Radio Button Flag Stock */}
|
||||||
<RadioInput
|
<RadioGroup
|
||||||
name='transaction_type'
|
name='transaction_type'
|
||||||
label='Tipe Transaksi'
|
label='Tipe Transaksi'
|
||||||
options={[
|
options={[
|
||||||
@@ -367,7 +367,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
Boolean(formik.errors.transaction_type)
|
Boolean(formik.errors.transaction_type)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.transaction_type as string}
|
errorMessage={formik.errors.transaction_type as string}
|
||||||
variant='radio-primary'
|
color='primary'
|
||||||
required
|
required
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
formik.values.transaction_type == undefined
|
formik.values.transaction_type == undefined
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const ChickinFormKandang = ({
|
|||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<FormHeader
|
<FormHeader
|
||||||
title={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`}
|
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 && (
|
{approvals && !approvalsLoading && (
|
||||||
|
|||||||
+291
-10
@@ -10,7 +10,7 @@ import SelectInput, {
|
|||||||
import PillBadge from '@/components/PillBadge';
|
import PillBadge from '@/components/PillBadge';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
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 { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -21,6 +21,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
const ProjectFlockChickinDetail = ({
|
const ProjectFlockChickinDetail = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
@@ -101,11 +102,26 @@ const ProjectFlockChickinDetail = ({
|
|||||||
}, [projectFlockId, listProjectFlock]);
|
}, [projectFlockId, listProjectFlock]);
|
||||||
return (
|
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'}`}
|
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
|
||||||
backUrl='/production/project-flock'
|
backUrl={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
|
||||||
/>
|
/> */}
|
||||||
<div className='flex flex-col gap-4 w-full my-4'>
|
{/* <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'>
|
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
@@ -145,8 +161,129 @@ const ProjectFlockChickinDetail = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
<Card
|
{/* 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'
|
title='Informasi Flock'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full bg-white mb-3',
|
wrapper: 'w-full bg-white mb-3',
|
||||||
@@ -231,8 +368,152 @@ const ProjectFlockChickinDetail = ({
|
|||||||
paginationClassName: 'hidden',
|
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'
|
title='Daftar Kandang'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full bg-white',
|
wrapper: 'w-full bg-white',
|
||||||
@@ -351,7 +632,7 @@ const ProjectFlockChickinDetail = ({
|
|||||||
paginationClassName: 'hidden',
|
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 Badge from '@/components/Badge';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
||||||
import Tooltip from '@/components/Tooltip';
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
import {
|
import {
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatDate,
|
formatDate,
|
||||||
@@ -13,6 +15,11 @@ import { Icon } from '@iconify/react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
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 = ({
|
const ProjectFlockDetail = ({
|
||||||
projectFlock,
|
projectFlock,
|
||||||
@@ -20,55 +27,60 @@ const ProjectFlockDetail = ({
|
|||||||
projectFlock: ProjectFlock;
|
projectFlock: ProjectFlock;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const deleteModal = useModal();
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [openBudgets, setOpenBudget] = 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='h-full w-full flex flex-col gap-4'>
|
<div className='h-full w-full flex flex-col gap-4'>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className='flex flex-row justify-between items-center px-4 pt-4'>
|
<DrawerHeader
|
||||||
<div className='flex flex-row h-full gap-2'>
|
leftIcon='mdi:close'
|
||||||
<Link
|
leftIconHref='/production/project-flock'
|
||||||
href={`/production/project-flock`}
|
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`}
|
||||||
className='hover:text-gray-400'
|
>
|
||||||
>
|
<Link
|
||||||
<Icon icon='mdi:close' width={24} height={24} />
|
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
|
||||||
</Link>
|
className='p-0'
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
>
|
||||||
<div className='text-sm text-neutral'>
|
<Tooltip content='Edit' position='bottom'>
|
||||||
Created On {formatDate(projectFlock.created_at, 'MMM DD, YYYY')}
|
<Button variant='link' className='p-0 text-neutral'>
|
||||||
</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'>
|
|
||||||
<Icon icon='mdi:square-edit-outline' width={20} height={20} />
|
<Icon icon='mdi:square-edit-outline' width={20} height={20} />
|
||||||
</Tooltip>
|
</Button>
|
||||||
</Link>
|
</Tooltip>
|
||||||
<Button variant='link' className='p-0 text-error'>
|
</Link>
|
||||||
<Tooltip content='Hapus' position='bottom'>
|
<Button
|
||||||
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
|
variant='link'
|
||||||
</Tooltip>
|
className='p-0 text-error'
|
||||||
</Button>
|
onClick={() => {
|
||||||
</div>
|
deleteModal.openModal();
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip content='Hapus' position='bottom'>
|
||||||
|
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DrawerHeader>
|
||||||
|
|
||||||
{/* Informasi Umum */}
|
{/* Informasi Umum */}
|
||||||
<div className='border-t-1 border-gray-300'>
|
<div className='border-t-1 border-gray-300'>
|
||||||
@@ -79,11 +91,11 @@ const ProjectFlockDetail = ({
|
|||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={
|
color={
|
||||||
projectFlock.approval.step_number == 1
|
projectFlock.approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: projectFlock.approval.step_number == 2
|
: projectFlock.approval?.step_number == 2
|
||||||
? 'success'
|
? 'success'
|
||||||
: projectFlock.approval.step_number >= 3
|
: projectFlock.approval?.step_number >= 3
|
||||||
? 'error'
|
? 'error'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -96,16 +108,16 @@ const ProjectFlockDetail = ({
|
|||||||
width={12}
|
width={12}
|
||||||
height={12}
|
height={12}
|
||||||
color={
|
color={
|
||||||
projectFlock.approval.step_number == 1
|
projectFlock.approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: projectFlock.approval.step_number == 2
|
: projectFlock.approval?.step_number == 2
|
||||||
? 'success'
|
? 'success'
|
||||||
: projectFlock.approval.step_number >= 3
|
: projectFlock.approval?.step_number >= 3
|
||||||
? 'error'
|
? 'error'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{projectFlock.approval.step_name}
|
{projectFlock.approval?.step_name}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -307,27 +319,85 @@ const ProjectFlockDetail = ({
|
|||||||
body: 'p-3',
|
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) => (
|
{projectFlock.kandangs.map((kandang) => (
|
||||||
<div
|
<div
|
||||||
key={kandang.id}
|
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'>
|
<RadioGroupItem
|
||||||
<Icon icon={'mdi:drag'} width={21} height={21} />{' '}
|
value={kandang.id.toString()}
|
||||||
<span className='font-semibold'>{kandang.name}</span>
|
label={kandang.name}
|
||||||
</div>
|
/>
|
||||||
<div className='text-end text-gray-400'>
|
<div className='text-end'>
|
||||||
Created On{' '}
|
<Badge
|
||||||
{formatDate(projectFlock.created_at, 'MMM DD, YYYY')}
|
className={{
|
||||||
|
badge: 'rounded-lg',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kapasitas {kandang.capacity} Ekor
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</RadioGroup>
|
||||||
</Card>
|
</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>
|
</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 { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
|
||||||
interface ProjectFlockFormProps {
|
interface ProjectFlockFormProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -675,28 +677,20 @@ const ProjectFlockForm = ({
|
|||||||
<>
|
<>
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className='flex flex-row justify-between items-center px-4 pt-4'>
|
<DrawerHeader
|
||||||
<div className='flex flex-row h-full gap-2'>
|
leftIcon={formType == 'add' ? 'mdi:close' : 'mdi:arrow-left'}
|
||||||
<Link
|
leftIconSize={24}
|
||||||
href={
|
leftIconHref={
|
||||||
formType == 'add'
|
formType == 'add'
|
||||||
? '/production/project-flock'
|
? '/production/project-flock'
|
||||||
: `/production/project-flock/detail?projectFlockId=${initialValues?.id}`
|
: `/production/project-flock/detail?projectFlockId=${initialValues?.id}`
|
||||||
}
|
}
|
||||||
className='hover:text-gray-400'
|
leftIconClassName='hover:text-gray-400'
|
||||||
>
|
subtitle={formType == 'add' ? 'Add Flock' : 'Update Flock'}
|
||||||
<Icon
|
subtitleClassName='text-sm text-neutral'
|
||||||
icon={formType == 'add' ? 'mdi:close' : 'mdi:arrow-left'}
|
showDivider
|
||||||
width={24}
|
>
|
||||||
height={24}
|
{formType == 'edit' && (
|
||||||
/>
|
|
||||||
</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'>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (initialValues?.id) {
|
if (initialValues?.id) {
|
||||||
@@ -713,8 +707,8 @@ const ProjectFlockForm = ({
|
|||||||
className='justify-start text-sm'
|
className='justify-start text-sm'
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</DrawerHeader>
|
||||||
{projectFlockFormErrorMessage && (
|
{projectFlockFormErrorMessage && (
|
||||||
<div className='my-4'>
|
<div className='my-4'>
|
||||||
<div role='alert' className='alert alert-error'>
|
<div role='alert' className='alert alert-error'>
|
||||||
@@ -770,21 +764,6 @@ const ProjectFlockForm = ({
|
|||||||
<Icon icon='mdi:times' width={24} height={24} />
|
<Icon icon='mdi:times' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -2,10 +2,189 @@ import { BaseApiService } from '@/services/api/base';
|
|||||||
import {
|
import {
|
||||||
BaseProjectFlockKandang,
|
BaseProjectFlockKandang,
|
||||||
ProjectFlockKandang,
|
ProjectFlockKandang,
|
||||||
|
ClosingProjectFlockKandangPayload,
|
||||||
|
CheckClosingResponse,
|
||||||
} from '@/types/api/production/project-flock-kandang';
|
} 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,
|
BaseProjectFlockKandang,
|
||||||
ProjectFlockKandang,
|
ProjectFlockKandang,
|
||||||
unknown
|
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;
|
project_flock_id: number;
|
||||||
kandang_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