mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
Merge branch 'feat/FE/US-281/TASK-316-317-slicing-ui-and-integrate-api-daily-recording-growing-uniformity-page' into 'development'
[FEAT/FE][US#281/TASK-316-317] Uniformity Page and Adjustment Purchase Accept Issue See merge request mbugroup/lti-web-client!128
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
import UniformityForm from '@/components/pages/production/uniformity/form/UniformityForm';
|
||||||
|
|
||||||
|
const AddUniformity = () => {
|
||||||
|
return <UniformityForm formType='add' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUniformity;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import UniformityDetail from '@/components/pages/production/uniformity/detail/UniformityDetail';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const UniformityDetailPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const uniformityId = searchParams.get('uniformityId');
|
||||||
|
|
||||||
|
const { data: uniformity, isLoading: isLoadingUniformity } = useSWR(
|
||||||
|
uniformityId,
|
||||||
|
(id: string) => UniformityApi.getUniformityDetail(parseInt(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uniformityId) {
|
||||||
|
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 (!isLoadingUniformity && (!uniformity || isResponseError(uniformity))) {
|
||||||
|
router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex flex-col justify-center'>
|
||||||
|
{isLoadingUniformity && (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4 min-h-screen'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isResponseSuccess(uniformity) && (
|
||||||
|
<UniformityDetail initialValues={uniformity.data} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityDetailPage;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import UniformityPageWrapper from '@/components/pages/production/uniformity/UniformityPageWrapper';
|
||||||
|
|
||||||
|
export default function UniformityLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return <UniformityPageWrapper>{children}</UniformityPageWrapper>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import UniformityTable from '@/components/pages/production/uniformity/UniformityTable';
|
||||||
|
|
||||||
|
const Uniformity = () => {
|
||||||
|
return <UniformityTable />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Uniformity;
|
||||||
+34
-14
@@ -3,29 +3,25 @@
|
|||||||
import { HTMLAttributes, ReactNode } from 'react';
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import type { Color, Variant, Size } from '@/types/theme';
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends Omit<HTMLAttributes<HTMLSpanElement>, 'className'> {
|
extends Omit<HTMLAttributes<HTMLSpanElement>, 'className'> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: {
|
className?: {
|
||||||
badge?: string;
|
badge?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
variant?: 'default' | 'outline' | 'ghost' | 'soft' | 'dash';
|
statusIndicator?: boolean;
|
||||||
color?:
|
variant?: Variant;
|
||||||
| 'neutral'
|
color?: Color;
|
||||||
| 'primary'
|
size?: Size;
|
||||||
| 'secondary'
|
|
||||||
| 'accent'
|
|
||||||
| 'info'
|
|
||||||
| 'success'
|
|
||||||
| 'warning'
|
|
||||||
| 'error';
|
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Badge = ({
|
const Badge = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
statusIndicator = false,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
color,
|
color,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
@@ -34,7 +30,7 @@ const Badge = ({
|
|||||||
const getBadgeClasses = () => {
|
const getBadgeClasses = () => {
|
||||||
const baseClasses = 'badge';
|
const baseClasses = 'badge';
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses: Record<Variant, string> = {
|
||||||
default: '',
|
default: '',
|
||||||
outline: 'badge-outline',
|
outline: 'badge-outline',
|
||||||
ghost: 'badge-ghost',
|
ghost: 'badge-ghost',
|
||||||
@@ -42,7 +38,7 @@ const Badge = ({
|
|||||||
dash: 'badge-dash',
|
dash: 'badge-dash',
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorClasses = {
|
const colorClasses: Record<Color, string> = {
|
||||||
neutral: 'badge-neutral',
|
neutral: 'badge-neutral',
|
||||||
primary: 'badge-primary',
|
primary: 'badge-primary',
|
||||||
secondary: 'badge-secondary',
|
secondary: 'badge-secondary',
|
||||||
@@ -51,9 +47,10 @@ const Badge = ({
|
|||||||
success: 'badge-success',
|
success: 'badge-success',
|
||||||
warning: 'badge-warning',
|
warning: 'badge-warning',
|
||||||
error: 'badge-error',
|
error: 'badge-error',
|
||||||
|
none: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
xs: 'badge-xs',
|
xs: 'badge-xs',
|
||||||
sm: 'badge-sm',
|
sm: 'badge-sm',
|
||||||
md: 'badge-md',
|
md: 'badge-md',
|
||||||
@@ -70,8 +67,31 @@ const Badge = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStatusClasses = () => {
|
||||||
|
if (!statusIndicator) return '';
|
||||||
|
|
||||||
|
const statusIndicatorClasses: Record<Color, string> = {
|
||||||
|
neutral: 'bg-neutral',
|
||||||
|
primary: 'bg-primary',
|
||||||
|
secondary: 'bg-secondary',
|
||||||
|
accent: 'bg-accent',
|
||||||
|
info: 'bg-info',
|
||||||
|
success: 'bg-success',
|
||||||
|
warning: 'bg-warning',
|
||||||
|
error: 'bg-error',
|
||||||
|
none: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
'w-2.5 h-2.5 rounded-full',
|
||||||
|
color && statusIndicatorClasses[color],
|
||||||
|
className?.status
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={getBadgeClasses()} {...props}>
|
<span className={getBadgeClasses()} {...props}>
|
||||||
|
{statusIndicator && <span className={getStatusClasses()} />}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface DrawerProps {
|
|||||||
className?: DrawerClassName;
|
className?: DrawerClassName;
|
||||||
onBackdropClick?: () => void;
|
onBackdropClick?: () => void;
|
||||||
closeOnBackdropClick?: boolean;
|
closeOnBackdropClick?: boolean;
|
||||||
|
expandedContent?: ReactNode;
|
||||||
|
expandedWidth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DrawerClassName = {
|
type DrawerClassName = {
|
||||||
@@ -36,6 +38,8 @@ const Drawer = ({
|
|||||||
className,
|
className,
|
||||||
onBackdropClick,
|
onBackdropClick,
|
||||||
closeOnBackdropClick = true,
|
closeOnBackdropClick = true,
|
||||||
|
expandedContent,
|
||||||
|
expandedWidth = 'w-[400px]',
|
||||||
}: DrawerProps) => {
|
}: DrawerProps) => {
|
||||||
const getDrawerClassNames = (): DrawerClassName => {
|
const getDrawerClassNames = (): DrawerClassName => {
|
||||||
const baseClassNames = {
|
const baseClassNames = {
|
||||||
@@ -46,12 +50,21 @@ const Drawer = ({
|
|||||||
drawerSidebarContent: 'min-h-full bg-base-100',
|
drawerSidebarContent: 'min-h-full bg-base-100',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSidebarWidth = () => {
|
||||||
|
if (variant === 'sidebar') {
|
||||||
|
return expandedContent
|
||||||
|
? 'w-full lg:min-w-[600px] lg:max-w-[600px]'
|
||||||
|
: 'w-full max-w-[300px] lg:w-[300px]';
|
||||||
|
}
|
||||||
|
return 'w-full sm:min-w-120 sm:w-fit';
|
||||||
|
};
|
||||||
|
|
||||||
if (variant === 'sidebar') {
|
if (variant === 'sidebar') {
|
||||||
return {
|
return {
|
||||||
...baseClassNames,
|
...baseClassNames,
|
||||||
drawerSidebarContent: cn(
|
drawerSidebarContent: cn(
|
||||||
baseClassNames.drawerSidebarContent,
|
baseClassNames.drawerSidebarContent,
|
||||||
'w-full max-w-[300px] lg:w-[300px]'
|
getSidebarWidth()
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (variant === 'right') {
|
} else if (variant === 'right') {
|
||||||
@@ -60,11 +73,11 @@ const Drawer = ({
|
|||||||
drawer: cn(baseClassNames.drawer, 'drawer-end'),
|
drawer: cn(baseClassNames.drawer, 'drawer-end'),
|
||||||
drawerSide: cn(
|
drawerSide: cn(
|
||||||
baseClassNames.drawerSide,
|
baseClassNames.drawerSide,
|
||||||
'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21'
|
'border-l border-solid border-gray-200 sm:drawer-side w-screen top-0 right-0 fixed z-21'
|
||||||
),
|
),
|
||||||
drawerSidebarContent: cn(
|
drawerSidebarContent: cn(
|
||||||
baseClassNames.drawerSidebarContent,
|
baseClassNames.drawerSidebarContent,
|
||||||
'w-full sm:min-w-120 sm:w-fit'
|
getSidebarWidth()
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (variant === 'left') {
|
} else if (variant === 'left') {
|
||||||
@@ -76,7 +89,7 @@ const Drawer = ({
|
|||||||
),
|
),
|
||||||
drawerSidebarContent: cn(
|
drawerSidebarContent: cn(
|
||||||
baseClassNames.drawerSidebarContent,
|
baseClassNames.drawerSidebarContent,
|
||||||
'w-full sm:min-w-120 sm:w-fit'
|
getSidebarWidth()
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -138,14 +151,37 @@ const Drawer = ({
|
|||||||
onClick={closeDrawer}
|
onClick={closeDrawer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Sidebar Content */}
|
{/* Sidebar Content - Full height container */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
varianClassName?.drawerSidebarContent,
|
'flex h-screen bg-base-100 overflow-hidden',
|
||||||
className?.drawerContent
|
variant === 'right' && 'flex-row'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sidebarContent}
|
{/* Primary Sidebar Content */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
varianClassName?.drawerSidebarContent,
|
||||||
|
className?.drawerContent,
|
||||||
|
'overflow-y-auto'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{sidebarContent}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Drawer (Right side, side-by-side) */}
|
||||||
|
{expandedContent && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border-l border-gray-200 bg-white flex flex-col h-full',
|
||||||
|
expandedWidth
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='overflow-y-auto flex-1 h-full'>
|
||||||
|
{expandedContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import Button, { ButtonProps } from '@/components/Button';
|
|||||||
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
export type IconPosition = 'left' | 'center' | 'right';
|
||||||
|
|
||||||
export interface ConfirmationModalProps {
|
export interface ConfirmationModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
type?: 'info' | 'success' | 'error';
|
type?: 'info' | 'success' | 'error';
|
||||||
text?: string;
|
text?: string;
|
||||||
|
subtitleText?: string;
|
||||||
closeOnBackdrop?: boolean;
|
closeOnBackdrop?: boolean;
|
||||||
primaryButton?: ButtonProps & {
|
primaryButton?: ButtonProps & {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -24,17 +27,84 @@ export interface ConfirmationModalProps {
|
|||||||
modalBox?: string;
|
modalBox?: string;
|
||||||
};
|
};
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
iconSize?: number;
|
||||||
|
iconPosition?: IconPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iconConfig = {
|
||||||
|
info: {
|
||||||
|
icon: 'material-symbols:info-outline-rounded',
|
||||||
|
iconClassName: 'text-info-content',
|
||||||
|
bgClassName: 'bg-info',
|
||||||
|
outerRingClassName: 'bg-info/20',
|
||||||
|
borderClassName: 'border-info',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
icon: 'heroicons:check',
|
||||||
|
iconClassName: 'text-white',
|
||||||
|
bgClassName: 'bg-[#00D390]',
|
||||||
|
outerRingClassName: 'bg-[#00D3901F]',
|
||||||
|
borderClassName: 'border-[#CCF7EB]',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
icon: 'solar:danger-triangle-linear',
|
||||||
|
iconClassName: 'text-error-content',
|
||||||
|
bgClassName: 'bg-[#f03338]',
|
||||||
|
outerRingClassName: 'bg-[#f3cdcd]',
|
||||||
|
borderClassName: 'border-[#fff0ef]',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const ConfirmationModalIcon = ({
|
||||||
|
type,
|
||||||
|
size = 24,
|
||||||
|
}: {
|
||||||
|
type: 'info' | 'success' | 'error';
|
||||||
|
size?: number;
|
||||||
|
}) => {
|
||||||
|
const config = iconConfig[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center p-2'>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-full border-4 p-1',
|
||||||
|
config.outerRingClassName,
|
||||||
|
config.borderClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn('rounded-full p-1', config.outerRingClassName)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-full p-3 flex items-center justify-center',
|
||||||
|
config.bgClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={config.icon}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={config.iconClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ConfirmationModal = ({
|
const ConfirmationModal = ({
|
||||||
ref,
|
ref,
|
||||||
type = 'info',
|
type = 'info',
|
||||||
text,
|
text,
|
||||||
|
subtitleText,
|
||||||
closeOnBackdrop,
|
closeOnBackdrop,
|
||||||
primaryButton,
|
primaryButton,
|
||||||
secondaryButton,
|
secondaryButton,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
iconSize = 32,
|
||||||
|
iconPosition = 'center',
|
||||||
}: ConfirmationModalProps) => {
|
}: ConfirmationModalProps) => {
|
||||||
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false);
|
||||||
|
|
||||||
@@ -55,47 +125,44 @@ const ConfirmationModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
<Modal ref={ref} closeOnBackdrop={closeOnBackdrop} className={className}>
|
||||||
<div className='w-full flex flex-col gap-4'>
|
<div className='w-full flex flex-col gap-4'>
|
||||||
<div
|
{iconPosition === 'center' ? (
|
||||||
className={cn(
|
<>
|
||||||
'w-fit p-4 mx-auto flex flex-row justify-center items-center rounded-full',
|
<div className='w-fit mx-auto'>
|
||||||
{
|
<ConfirmationModalIcon type={type} size={iconSize} />
|
||||||
'bg-error': type === 'error',
|
</div>
|
||||||
'bg-info': type === 'info',
|
|
||||||
'bg-success': type === 'success',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{type === 'info' && (
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:info-outline-rounded'
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className='text-info-content'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === 'success' && (
|
<p className='text-center font-medium'>
|
||||||
<Icon
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
icon='qlementine-icons:success-12'
|
</p>
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className='text-success-content'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === 'error' && (
|
{subtitleText && (
|
||||||
<Icon
|
<p className='text-center text-sm text-gray-400'>
|
||||||
icon='solar:danger-triangle-linear'
|
{subtitleText}
|
||||||
width={64}
|
</p>
|
||||||
height={64}
|
)}
|
||||||
className='text-error-content'
|
</>
|
||||||
/>
|
) : (
|
||||||
)}
|
<div
|
||||||
</div>
|
className={cn('flex flex-row items-center gap-4', {
|
||||||
|
'flex-row': iconPosition === 'left',
|
||||||
|
'flex-row-reverse': iconPosition === 'right',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className='w-fit'>
|
||||||
|
<ConfirmationModalIcon type={type} size={iconSize} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className='text-center font-medium'>
|
<div className='flex flex-col gap-1'>
|
||||||
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
<p className='font-medium'>
|
||||||
</p>
|
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{subtitleText && (
|
||||||
|
<p className='text-sm text-gray-400'>{subtitleText}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{children && <div className='w-full'>{children}</div>}
|
{children && <div className='w-full'>{children}</div>}
|
||||||
|
|
||||||
@@ -103,7 +170,7 @@ const ConfirmationModal = ({
|
|||||||
{secondaryButton && secondaryButton.text && (
|
{secondaryButton && secondaryButton.text && (
|
||||||
<Button
|
<Button
|
||||||
{...secondaryButton}
|
{...secondaryButton}
|
||||||
variant='ghost'
|
variant='outline'
|
||||||
color={secondaryButton?.color}
|
color={secondaryButton?.color}
|
||||||
isLoading={secondaryButton?.isLoading}
|
isLoading={secondaryButton?.isLoading}
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart';
|
||||||
|
import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart';
|
||||||
|
import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton';
|
||||||
|
import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton';
|
||||||
|
|
||||||
|
interface BarChartData {
|
||||||
|
name: string;
|
||||||
|
uv: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GaugeChartData {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
kandang?: string;
|
||||||
|
week?: string;
|
||||||
|
currentValue?: number;
|
||||||
|
totalValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityChart = () => {
|
||||||
|
// TODO: Replace with actual API call
|
||||||
|
const barChartData: BarChartData[] = [
|
||||||
|
{
|
||||||
|
name: '48-52',
|
||||||
|
uv: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '52-56',
|
||||||
|
uv: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '56-60',
|
||||||
|
uv: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '60-64',
|
||||||
|
uv: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '64-68',
|
||||||
|
uv: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '68-72',
|
||||||
|
uv: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '72-76',
|
||||||
|
uv: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '76-80',
|
||||||
|
uv: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '84-88',
|
||||||
|
uv: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '88-92',
|
||||||
|
uv: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '92-96',
|
||||||
|
uv: 160,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: Replace with actual API call
|
||||||
|
// const gaugeChartData: GaugeChartData = {
|
||||||
|
// value: 0,
|
||||||
|
// label: '',
|
||||||
|
// kandang: 'Kandang Cirangga',
|
||||||
|
// week: 'Week 2',
|
||||||
|
// currentValue: 512,
|
||||||
|
// totalValue: 1024,
|
||||||
|
// };
|
||||||
|
|
||||||
|
const gaugeChartData: GaugeChartData = {
|
||||||
|
value: 52,
|
||||||
|
label: 'Uniformity',
|
||||||
|
kandang: 'Kandang Cirangga',
|
||||||
|
week: 'Week 2',
|
||||||
|
currentValue: 512,
|
||||||
|
totalValue: 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-4 gap-4'>
|
||||||
|
<Card
|
||||||
|
title='Performance Overview ⓘ'
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'xl:col-span-1 2xl:col-span-3 w-full',
|
||||||
|
body: 'h-96',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='w-full h-full flex items-center justify-center'>
|
||||||
|
{barChartData.length === 0 ? (
|
||||||
|
<UniformityBarChartSkeleton />
|
||||||
|
) : (
|
||||||
|
<UniformityBarChart data={barChartData} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
{gaugeChartData.value === 0 ? (
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
title='Weekly Performance ⓘ'
|
||||||
|
className={{
|
||||||
|
wrapper: 'xl:col-span-1 2xl:col-span-1 w-full',
|
||||||
|
body: 'h-110',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UniformityGaugeChartSkeleton />
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
title='Weekly Performance ⓘ'
|
||||||
|
className={{
|
||||||
|
wrapper: 'xl:col-span-1 2xl:col-span-1 w-full',
|
||||||
|
body: 'p-4',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UniformityGaugeChart
|
||||||
|
value={gaugeChartData.value}
|
||||||
|
label={gaugeChartData.label}
|
||||||
|
kandang={gaugeChartData.kandang}
|
||||||
|
week={gaugeChartData.week}
|
||||||
|
currentValue={gaugeChartData.currentValue}
|
||||||
|
totalValue={gaugeChartData.totalValue}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityChart;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import Drawer from '@/components/Drawer';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import UniformityTable from '@/components/pages/production/uniformity/UniformityTable';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
|
export default function UniformityPageWrapper({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const toggleValidate = useUiStore((s) => s.toggleValidate);
|
||||||
|
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
|
||||||
|
const expandedDrawerOpen = useUiStore((s) => s.expandedDrawerOpen);
|
||||||
|
const expandedDrawerContent = useUiStore((s) => s.expandedDrawerContent);
|
||||||
|
|
||||||
|
const isAdd = pathname.includes('/add');
|
||||||
|
const isEdit = pathname.includes('/detail/edit');
|
||||||
|
const isDetail = pathname.includes('/detail');
|
||||||
|
|
||||||
|
const isOpen = isAdd || isEdit || isDetail;
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
const unsub = useUiStore.getState().subscribeIsValid((isValid) => {
|
||||||
|
if (isValid) {
|
||||||
|
router.push('/production/uniformity');
|
||||||
|
unsub?.();
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
} else {
|
||||||
|
unsub?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleValidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
<UniformityTable
|
||||||
|
refresh={() => !isOpen && router.push('/production/uniformity')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
open={isOpen}
|
||||||
|
setOpen={(v) => {
|
||||||
|
if (!v) {
|
||||||
|
router.push('/production/uniformity');
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
closeOnBackdropClick={isDetail ? true : false}
|
||||||
|
onBackdropClick={handleBackdropClick}
|
||||||
|
variant='right'
|
||||||
|
zIndex='99999'
|
||||||
|
sidebarContent={isOpen ? <div className=''>{children}</div> : null}
|
||||||
|
expandedContent={expandedDrawerOpen ? expandedDrawerContent : null}
|
||||||
|
expandedWidth='w-[500px]'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Rectangle,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
interface Payload {
|
||||||
|
value?: number;
|
||||||
|
name?: string;
|
||||||
|
dataKey?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomTooltipProps {
|
||||||
|
active?: boolean;
|
||||||
|
payload?: readonly Payload[];
|
||||||
|
label?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarChartData {
|
||||||
|
name: string;
|
||||||
|
uv: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UniformityBarChartProps {
|
||||||
|
data: BarChartData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
|
||||||
|
if (active && payload && payload.length && label !== undefined) {
|
||||||
|
const labelStr = String(label);
|
||||||
|
return (
|
||||||
|
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
|
||||||
|
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
|
||||||
|
<div className='flex items-center gap-2 mt-2 justify-between'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
|
||||||
|
{payload[0].value} of Birds
|
||||||
|
</div>
|
||||||
|
<span>{labelStr}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityBarChart: React.FC<UniformityBarChartProps> = ({ data }) => {
|
||||||
|
const margin = {
|
||||||
|
top: 20,
|
||||||
|
right: 30,
|
||||||
|
left: 20,
|
||||||
|
bottom: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
className='min-h-[300px] xl:min-h-[350px]'
|
||||||
|
>
|
||||||
|
<BarChart data={data} margin={margin} barGap={20}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id='activeBarGradient' x1='0' y1='0' x2='0' y2='1'>
|
||||||
|
<stop offset='0%' stopColor='#0069E0' stopOpacity={0.01} />
|
||||||
|
<stop offset='40%' stopColor='#0069E0' stopOpacity={1} />
|
||||||
|
<stop offset='100%' stopColor='#0069E0' stopOpacity={1} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<XAxis
|
||||||
|
dataKey='name'
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
label={{
|
||||||
|
value: 'Body Weight Range',
|
||||||
|
position: 'insideBottom',
|
||||||
|
offset: -5,
|
||||||
|
style: { textAnchor: 'middle', fontSize: 14, fill: '#18181B33' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
label={{
|
||||||
|
value: 'Number of Birds',
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { textAnchor: 'middle', fontSize: 14, fill: '#18181B33' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
cursor={false}
|
||||||
|
content={CustomTooltip}
|
||||||
|
wrapperStyle={{
|
||||||
|
width: '200px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<Bar
|
||||||
|
name='Birds'
|
||||||
|
dataKey='uv'
|
||||||
|
fill='#FFFFFF'
|
||||||
|
stroke='#DDD'
|
||||||
|
strokeWidth={2}
|
||||||
|
radius={[25, 25, 0, 0]}
|
||||||
|
activeBar={
|
||||||
|
<Rectangle
|
||||||
|
fill='url(#activeBarGradient)'
|
||||||
|
stroke='#18181B'
|
||||||
|
strokeWidth={0}
|
||||||
|
radius={[25, 25, 0, 0]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityBarChart;
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface UniformityGaugeChartProps {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
kandang?: string;
|
||||||
|
week?: string;
|
||||||
|
currentValue?: number;
|
||||||
|
totalValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityGaugeChart: React.FC<UniformityGaugeChartProps> = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
kandang,
|
||||||
|
week,
|
||||||
|
currentValue,
|
||||||
|
totalValue,
|
||||||
|
}) => {
|
||||||
|
const numberOfSegments = 50;
|
||||||
|
const filledSegments = Math.round((value / 100) * numberOfSegments);
|
||||||
|
|
||||||
|
const data = Array.from({ length: numberOfSegments }, (_, index) => ({
|
||||||
|
name: index,
|
||||||
|
value: 1,
|
||||||
|
filled: index < filledSegments,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activeColor = '#1890ff';
|
||||||
|
const inactiveColor = '#f0f0f0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col w-full'>
|
||||||
|
<div className='h-64 w-full relative flex justify-center'>
|
||||||
|
<div className='relative w-full h-full flex flex-col items-center justify-end'>
|
||||||
|
<ResponsiveContainer width='100%' height='100%'>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={data}
|
||||||
|
cx='50%'
|
||||||
|
cy='70%'
|
||||||
|
startAngle={180}
|
||||||
|
endAngle={0}
|
||||||
|
innerRadius='75%'
|
||||||
|
outerRadius='100%'
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey='value'
|
||||||
|
stroke='none'
|
||||||
|
isAnimationActive={false}
|
||||||
|
>
|
||||||
|
{data.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={entry.filled ? activeColor : inactiveColor}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<div className='absolute inset-x-0 bottom-8 flex flex-col items-center justify-center'>
|
||||||
|
<span className='2xl:text-3xl text-2xl font-bold text-gray-800 mb-4'>
|
||||||
|
{value}%
|
||||||
|
</span>
|
||||||
|
<div className='mt-2 px-4 py-1 bg-base-100 rounded-full shadow-sm border border-gray-200'>
|
||||||
|
<span className='text-sm font-medium text-gray-700 mb-32'>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card
|
||||||
|
variant='bordered'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section className='flex items-center gap-4'>
|
||||||
|
<div className='w-12 h-12 bg-base-200 rounded-lg flex items-center justify-center border border-gray-200 shrink-0'>
|
||||||
|
<Icon icon='heroicons:calendar-date-range' width={24} height={24} />
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 min-w-0'>
|
||||||
|
<div className='flex items-center space-x-2 text-[#18181B80] text-sm mb-1'>
|
||||||
|
<span className='font-medium truncate'>{kandang}</span>
|
||||||
|
<span className='shrink-0'>•</span>
|
||||||
|
<span className='text-[#0069E0] font-semibold truncate'>
|
||||||
|
{week}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='text-xl font-bold text-[#18181B80]'>
|
||||||
|
<span className='text-[#0069E0] break-all'>
|
||||||
|
{formatNumber(currentValue ?? 0)}
|
||||||
|
</span>
|
||||||
|
<span className='mx-1 text-gray-400 text-base'>From</span>
|
||||||
|
<span className='break-all'>{formatNumber(totalValue ?? 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityGaugeChart;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import Badge from '../../../../Badge';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
|
||||||
|
const UniformityStat = () => {
|
||||||
|
const statisticsData = [
|
||||||
|
{
|
||||||
|
title: 'Total Population',
|
||||||
|
value: 1908978,
|
||||||
|
icon: 'heroicons-outline:inbox-stack',
|
||||||
|
change: '15.5%',
|
||||||
|
changeType: 'increase',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Uniformity',
|
||||||
|
value: 954489,
|
||||||
|
icon: 'heroicons-outline:scale',
|
||||||
|
change: '50%',
|
||||||
|
changeType: 'decrease',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Depletion',
|
||||||
|
value: 954489,
|
||||||
|
icon: 'heroicons-outline:inbox-stack',
|
||||||
|
change: '15.5%',
|
||||||
|
changeType: 'increase',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Production',
|
||||||
|
value: 2534,
|
||||||
|
icon: 'heroicons-outline:inbox-stack',
|
||||||
|
change: '15.5%',
|
||||||
|
changeType: 'increase',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4'>
|
||||||
|
{statisticsData.map((stat, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
variant='bordered'
|
||||||
|
size='sm'
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
footer: 'bg-[#F8F8F8]',
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<section className='flex items-center justify-between'>
|
||||||
|
<span className='font-normal text-gray-500'>
|
||||||
|
From last month
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
color={stat.changeType === 'increase' ? 'success' : 'error'}
|
||||||
|
variant='soft'
|
||||||
|
className={{ badge: 'rounded-2xl' }}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
stat.changeType === 'increase'
|
||||||
|
? 'heroicons-outline:arrow-trending-up'
|
||||||
|
: 'heroicons-outline:arrow-trending-down'
|
||||||
|
}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='inline-block'
|
||||||
|
/>
|
||||||
|
{stat.change}
|
||||||
|
</Badge>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='flex gap-2 items-center'>
|
||||||
|
<div className='p-2 border rounded-xl border-gray-300 shrink-0'>
|
||||||
|
<Icon icon={stat.icon} width={32} height={32} />
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 min-w-0'>
|
||||||
|
<span className='truncate'>{stat.title}</span>
|
||||||
|
<span className='text-xl font-semibold break-all'>
|
||||||
|
{formatNumber(stat.value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityStat;
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useEffect } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { UniformityDetail as UniformityDetailType } from '@/types/api/production/uniformity';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import UniformityDetailsPreview from '@/components/pages/production/uniformity/detail/UniformityDetailsPreview';
|
||||||
|
import {
|
||||||
|
getStatusColor,
|
||||||
|
getStatusIndicatorColor,
|
||||||
|
getStatusText,
|
||||||
|
} from '@/components/pages/production/uniformity/uniformity-utils';
|
||||||
|
import { DetailOptionType } from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
|
interface UniformityDetailProps {
|
||||||
|
initialValues: UniformityDetailType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityDetail: React.FC<UniformityDetailProps> = ({
|
||||||
|
initialValues,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
|
||||||
|
const setExpandedDrawerContent = useUiStore(
|
||||||
|
(s) => s.setExpandedDrawerContent
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleApprove = () => {
|
||||||
|
router.push(`/production/uniformity?action=approve&id=${initialValues.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = () => {
|
||||||
|
router.push(`/production/uniformity?action=reject&id=${initialValues.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewUniformityDetails = () => {
|
||||||
|
setExpandedDrawerContent(
|
||||||
|
<UniformityDetailsPreview
|
||||||
|
info_umum={initialValues.info_umum}
|
||||||
|
uniformity_details={initialValues.uniformity_details}
|
||||||
|
sampling={initialValues.sampling}
|
||||||
|
result={initialValues.result}
|
||||||
|
uniformityId={initialValues.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setExpandedDrawerOpen(true);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
setExpandedDrawerContent(null);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const infoUmumTableData: DetailOptionType[] = useMemo(() => {
|
||||||
|
if (!initialValues) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'tanggal',
|
||||||
|
value: 'tanggal',
|
||||||
|
label: 'Tanggal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lokasi-farm',
|
||||||
|
value: 'lokasi-farm',
|
||||||
|
label: 'Lokasi Farm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'project-flock',
|
||||||
|
value: 'project-flock',
|
||||||
|
label: 'Project Flock',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kandang',
|
||||||
|
value: 'kandang',
|
||||||
|
label: 'Kandang',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'document-name',
|
||||||
|
value: 'document-name',
|
||||||
|
label: 'File Uniformity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'approval-status',
|
||||||
|
value: 'approval-status',
|
||||||
|
label: 'Status',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
const columnsInfoUmum: ColumnDef<DetailOptionType>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'label',
|
||||||
|
header: 'Label',
|
||||||
|
cell: (props) => props.row.original.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'value',
|
||||||
|
header: 'Value',
|
||||||
|
cell: (props) => {
|
||||||
|
const id = props.row.original.id;
|
||||||
|
const { info_umum, latest_approval } = initialValues!;
|
||||||
|
|
||||||
|
const statusValue = latest_approval?.action ?? '-';
|
||||||
|
|
||||||
|
const valueMap: Record<string, string> = {
|
||||||
|
tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'),
|
||||||
|
'lokasi-farm': info_umum.lokasi_farm,
|
||||||
|
'project-flock': info_umum.project_flock,
|
||||||
|
kandang: info_umum.kandang,
|
||||||
|
'document-name': info_umum.file_name,
|
||||||
|
'approval-status': statusValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (id === 'approval-status') {
|
||||||
|
const status = latest_approval?.action;
|
||||||
|
if (status) {
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<Badge
|
||||||
|
statusIndicator={true}
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge: `rounded-xl w-full justify-start border border-gray-200 text-black ${getStatusColor(status)}`,
|
||||||
|
status: getStatusIndicatorColor(status),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getStatusText(status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span>-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id === 'document-name') {
|
||||||
|
return (
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span>{valueMap[id]}</span>
|
||||||
|
<Tooltip content='Lihat Detail'>
|
||||||
|
<button
|
||||||
|
className='p-1 hover:bg-gray-100 rounded cursor-pointer'
|
||||||
|
onClick={handleViewUniformityDetails}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={18} height={18} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{valueMap[id] || '-'}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full h-full bg-white border-l border-gray-200'>
|
||||||
|
{/* Header */}
|
||||||
|
<DrawerHeader
|
||||||
|
leftIconHref='/production/uniformity'
|
||||||
|
subtitle={`Details`}
|
||||||
|
subtitleClassName='text-sm text-neutral'
|
||||||
|
showDivider
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Form Section */}
|
||||||
|
<div className='divider mt-3.5'></div>
|
||||||
|
<section className='w-full px-6'>
|
||||||
|
{initialValues ? (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{/* Info Umum */}
|
||||||
|
<div className=''>
|
||||||
|
<p className='text-sm font-medium mb-5'>Informasi Umum</p>
|
||||||
|
<Table<DetailOptionType>
|
||||||
|
data={infoUmumTableData}
|
||||||
|
columns={columnsInfoUmum}
|
||||||
|
pageSize={6}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Approve/Reject Buttons */}
|
||||||
|
{initialValues.result &&
|
||||||
|
initialValues.latest_approval?.step_name === 'CREATED' ? (
|
||||||
|
<>
|
||||||
|
<div className='divider my-3.5' />
|
||||||
|
<RequirePermission permissions='lti.production.uniformity.approve'>
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 [&_button]:rounded-lg'>
|
||||||
|
<Button variant='outline' onClick={handleReject}>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleApprove}>Approve</Button>
|
||||||
|
</div>
|
||||||
|
</RequirePermission>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col items-center justify-center py-10 text-gray-400'>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:file-document-outline'
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className='mb-4'
|
||||||
|
/>
|
||||||
|
<p className='text-lg'>No data available</p>
|
||||||
|
<p className='text-sm'>Uniformity detail not found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityDetail;
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import {
|
||||||
|
UniformityDetailItem,
|
||||||
|
UniformitySampling,
|
||||||
|
UniformityResult,
|
||||||
|
UniformityInfoUmum,
|
||||||
|
} from '@/types/api/production/uniformity';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
import { DetailOptionType } from '@/types/api/production/uniformity';
|
||||||
|
import {
|
||||||
|
getWeightStatusColor,
|
||||||
|
getWeightStatusIndicatorColor,
|
||||||
|
getWeightStatusText,
|
||||||
|
} from '@/components/pages/production/uniformity/uniformity-utils';
|
||||||
|
import { BodyWeightData } from '@/types/api/production/uniformity';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
|
interface UniformityDetailsPreviewProps {
|
||||||
|
info_umum: UniformityInfoUmum;
|
||||||
|
sampling: UniformitySampling;
|
||||||
|
result: UniformityResult;
|
||||||
|
uniformity_details?: UniformityDetailItem[];
|
||||||
|
uniformityId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityDetailsPreview = ({
|
||||||
|
info_umum,
|
||||||
|
uniformity_details: initialUniformityDetails,
|
||||||
|
sampling,
|
||||||
|
result,
|
||||||
|
uniformityId,
|
||||||
|
}: UniformityDetailsPreviewProps) => {
|
||||||
|
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
|
||||||
|
const [shouldFetchDetails, setShouldFetchDetails] = useState(false);
|
||||||
|
|
||||||
|
const { data: uniformityDetailResponse, isLoading } = useSWR(
|
||||||
|
shouldFetchDetails
|
||||||
|
? `uniformity-detail-${uniformityId}-with-details`
|
||||||
|
: null,
|
||||||
|
() => UniformityApi.getUniformityDetail(uniformityId, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniformity_details = useMemo(() => {
|
||||||
|
if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) {
|
||||||
|
return uniformityDetailResponse.data.uniformity_details;
|
||||||
|
}
|
||||||
|
return initialUniformityDetails;
|
||||||
|
}, [shouldFetchDetails, uniformityDetailResponse, initialUniformityDetails]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchWeightData = () => {
|
||||||
|
setShouldFetchDetails(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const samplingTableData: DetailOptionType[] = useMemo(() => {
|
||||||
|
if (!sampling) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'sampling-size',
|
||||||
|
label: 'Sampling size',
|
||||||
|
value: `${formatNumber(sampling.chick_qty_of_weight)} of Birds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mean-weight',
|
||||||
|
label: 'Mean Weight',
|
||||||
|
value: `${sampling.mean_weight} g`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'min-limit',
|
||||||
|
label: 'Min Limit (-10%)',
|
||||||
|
value: `${sampling.mean_down} g`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'max-limit',
|
||||||
|
label: 'Max Limit (+10%)',
|
||||||
|
value: `${sampling.mean_up} g`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [sampling]);
|
||||||
|
|
||||||
|
const columnsSampling: ColumnDef<DetailOptionType>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'label',
|
||||||
|
header: 'Label',
|
||||||
|
cell: (props) => props.row.original.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'value',
|
||||||
|
header: 'Value',
|
||||||
|
cell: (props) => <span>{props.row.original.value}</span>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultTableData: DetailOptionType[] = useMemo(() => {
|
||||||
|
if (!result) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'ideal-birds',
|
||||||
|
label: 'Ideal Birds',
|
||||||
|
value: `${formatNumber(result.uniform_qty)} of Birds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'outside-range',
|
||||||
|
label: 'Outside Range',
|
||||||
|
value: `${formatNumber(result.outside_qty)} of Birds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uniformity',
|
||||||
|
label: 'Uniformity',
|
||||||
|
value: `${result.uniformity} %`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
|
const resultColumns: ColumnDef<DetailOptionType>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'label',
|
||||||
|
header: 'Label',
|
||||||
|
cell: (props) => props.row.original.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'value',
|
||||||
|
header: 'Value',
|
||||||
|
cell: (props) => <span>{props.row.original.value}</span>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!uniformity_details) return [];
|
||||||
|
|
||||||
|
return uniformity_details.map(
|
||||||
|
(detail: UniformityDetailItem, index: number) => ({
|
||||||
|
id: `body-weight-${index + 1}`,
|
||||||
|
number: index + 1,
|
||||||
|
weight: detail.weight,
|
||||||
|
status: detail.range.toLowerCase() as 'ideal' | 'outside',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [uniformity_details]);
|
||||||
|
|
||||||
|
const columnsUniformity: ColumnDef<BodyWeightData>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'number',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => props.row.original.number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'weight',
|
||||||
|
header: 'Weight (g)',
|
||||||
|
cell: (props) => <span>{props.row.original.weight}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: (props) => {
|
||||||
|
const status = props.row.original.status;
|
||||||
|
return status ? (
|
||||||
|
<div className='w-full'>
|
||||||
|
<Badge
|
||||||
|
statusIndicator={true}
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge: `rounded-xl w-full justify-start border border-gray-200 text-black ${getWeightStatusColor(status)}`,
|
||||||
|
status: getWeightStatusIndicatorColor(status),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getWeightStatusText(status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Badge
|
||||||
|
statusIndicator={true}
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'rounded-xl w-full justify-start border border-gray-200 text-black bg-info/10',
|
||||||
|
status: 'bg-info',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unknown
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full h-full bg-white border-l border-gray-200'>
|
||||||
|
{/* Header */}
|
||||||
|
<DrawerHeader
|
||||||
|
leftIcon=''
|
||||||
|
subtitle={info_umum?.file_name ?? 'Uniformity Details'}
|
||||||
|
subtitleClassName='text-sm text-neutral line-clamp-1'
|
||||||
|
showDivider={false}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className='p-0 text-error hover:bg-transparent hover:text-error/50 cursor-pointer'
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:close' width={24} height={24} />
|
||||||
|
</button>
|
||||||
|
</DrawerHeader>
|
||||||
|
|
||||||
|
{/* Form Section */}
|
||||||
|
<div className='divider mt-3.5'></div>
|
||||||
|
<section className='w-full px-6'>
|
||||||
|
{uniformity_details && uniformity_details.length > 0 ? (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{/* Sampling and Range */}
|
||||||
|
<div className=''>
|
||||||
|
<p className='text-sm font-medium mb-5'>Sampling and Range</p>
|
||||||
|
<Table<DetailOptionType>
|
||||||
|
data={samplingTableData}
|
||||||
|
columns={columnsSampling}
|
||||||
|
pageSize={4}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Result */}
|
||||||
|
<div className=''>
|
||||||
|
<p className='text-sm font-medium mb-5'>Result</p>
|
||||||
|
<Table<DetailOptionType>
|
||||||
|
data={resultTableData}
|
||||||
|
columns={resultColumns}
|
||||||
|
pageSize={4}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body Weight Details Button */}
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={fetchWeightData}
|
||||||
|
disabled={isLoading}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
{isLoading ? 'Loading...' : 'Show Body Weight Details'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/*{!uniformity_details || uniformity_details.length === 0 ? (
|
||||||
|
<></>
|
||||||
|
) : null}*/}
|
||||||
|
|
||||||
|
{/* Body Weight Details */}
|
||||||
|
{uniformity_details && uniformity_details.length > 0 && (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Table<BodyWeightData>
|
||||||
|
data={tableData}
|
||||||
|
columns={columnsUniformity}
|
||||||
|
pageSize={15}
|
||||||
|
className={{ containerClassName: 'mb-5' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col items-center justify-center py-10 text-gray-400'>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:file-document-outline'
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className='mb-4'
|
||||||
|
/>
|
||||||
|
<p className='text-lg'>No data available</p>
|
||||||
|
<p className='text-sm'>Uniformity details not found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityDetailsPreview;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import type { Uniformity } from '@/types/api/production/uniformity';
|
||||||
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface UniformityExportExcelParams {
|
||||||
|
data: Uniformity[];
|
||||||
|
params: {
|
||||||
|
location_name?: string;
|
||||||
|
project_flock_name?: string;
|
||||||
|
kandang_name?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'Disetujui';
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'Ditolak';
|
||||||
|
case 'CREATED':
|
||||||
|
return 'Pengajuan';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateUniformityExcel = (
|
||||||
|
data: UniformityExportExcelParams['data'],
|
||||||
|
params: UniformityExportExcelParams['params']
|
||||||
|
): void => {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const excelData: { [key: string]: string | number }[] = data.map(
|
||||||
|
(item: Uniformity, index: number) => ({
|
||||||
|
No: index + 1,
|
||||||
|
Lokasi: item.location_name || '',
|
||||||
|
'Project Flock': item.flock_name || '',
|
||||||
|
Kandang: item.kandang_name || '',
|
||||||
|
Tanggal: formatDate(item.applied_at, 'DD MMM YYYY'),
|
||||||
|
Minggu: item.week || 0,
|
||||||
|
Status: getStatusText(item.status),
|
||||||
|
'Uniformity (%)': formatNumber(item.uniformity),
|
||||||
|
'CV (%)': formatNumber(item.cv),
|
||||||
|
'Chick Qty': formatNumber(item.chick_qty_of_weight),
|
||||||
|
'Uniform Qty': formatNumber(item.uniform_qty),
|
||||||
|
'Mean Up': formatNumber(item.mean_up),
|
||||||
|
'Mean Down': formatNumber(item.mean_down),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
||||||
|
|
||||||
|
const colWidths = [
|
||||||
|
{ wch: 6 }, // No
|
||||||
|
{ wch: 25 }, // Lokasi
|
||||||
|
{ wch: 20 }, // Project Flock
|
||||||
|
{ wch: 15 }, // Kandang
|
||||||
|
{ wch: 15 }, // Tanggal
|
||||||
|
{ wch: 10 }, // Minggu
|
||||||
|
{ wch: 12 }, // Status
|
||||||
|
{ wch: 15 }, // Uniformity (%)
|
||||||
|
{ wch: 10 }, // CV (%)
|
||||||
|
{ wch: 12 }, // Chick Qty
|
||||||
|
{ wch: 12 }, // Uniform Qty
|
||||||
|
{ wch: 12 }, // Mean Up
|
||||||
|
{ wch: 12 }, // Mean Down
|
||||||
|
];
|
||||||
|
worksheet['!cols'] = colWidths;
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Uniformity');
|
||||||
|
|
||||||
|
const period =
|
||||||
|
params.start_date && params.end_date
|
||||||
|
? `${params.start_date}-${params.end_date}`
|
||||||
|
: formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
const filename = `laporan-uniformity-${period}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(workbook, filename);
|
||||||
|
};
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Document,
|
||||||
|
StyleSheet,
|
||||||
|
Font,
|
||||||
|
pdf,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import type { Uniformity } from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
|
Font.register({
|
||||||
|
family: 'Helvetica',
|
||||||
|
src: 'helvetica',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfStyles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
titleSection: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
mainTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 5,
|
||||||
|
color: '#1f74bf',
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000000',
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
tableRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
tableHeader: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
},
|
||||||
|
tableCell: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
tableCellHeader: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
paddingVertical: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tableCellHeaderRight: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
textAlign: 'right',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
tableCellRight: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
tableCellCenter: {
|
||||||
|
flex: 1,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000000',
|
||||||
|
borderRightStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
tableBorderBottom: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000000',
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
backgroundColor: '#1f74bf',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
fontSize: 7,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
parameterBadge: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
color: '#333333',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UniformityExportPDFParams {
|
||||||
|
data: Uniformity[];
|
||||||
|
params: {
|
||||||
|
location_name?: string;
|
||||||
|
project_flock_name?: string;
|
||||||
|
kandang_name?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParameterText = (params: UniformityExportPDFParams['params']) => {
|
||||||
|
const paramsText = [];
|
||||||
|
|
||||||
|
if (params.location_name && params.location_name !== 'Semua Lokasi') {
|
||||||
|
paramsText.push(`Lokasi: ${params.location_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
params.project_flock_name &&
|
||||||
|
params.project_flock_name !== 'Semua Project Flock'
|
||||||
|
) {
|
||||||
|
paramsText.push(`Project Flock: ${params.project_flock_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.kandang_name && params.kandang_name !== 'Semua Kandang') {
|
||||||
|
paramsText.push(`Kandang: ${params.kandang_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.start_date && params.end_date) {
|
||||||
|
const formattedStartDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||||
|
const formattedEndDate = formatDate(params.end_date, 'DD MMM YYYY');
|
||||||
|
paramsText.push(`Periode: ${formattedStartDate} - ${formattedEndDate}`);
|
||||||
|
} else if (params.start_date) {
|
||||||
|
const formattedStartDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||||
|
paramsText.push(`Tanggal Mulai: ${formattedStartDate}`);
|
||||||
|
} else if (params.end_date) {
|
||||||
|
const formattedEndDate = formatDate(params.end_date, 'DD MMM YYYY');
|
||||||
|
paramsText.push(`Tanggal Akhir: ${formattedEndDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
|
||||||
|
paramsText.push(`Dicetak: ${currentDate}`);
|
||||||
|
|
||||||
|
return paramsText;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'Disetujui';
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'Ditolak';
|
||||||
|
case 'CREATED':
|
||||||
|
return 'Pengajuan';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPDFDocument = (
|
||||||
|
data: UniformityExportPDFParams['data'],
|
||||||
|
params: UniformityExportPDFParams['params']
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page size='A4' orientation='landscape' style={pdfStyles.page}>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<Text style={pdfStyles.mainTitle}>Production > Uniformity</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
{getParameterText(params).map((param, index) => (
|
||||||
|
<View key={index} style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>{param}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<View style={pdfStyles.table}>
|
||||||
|
{/* Table Header */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||||
|
<Text>No</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Lokasi</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Project Flock</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>Kandang</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Tanggal (Week)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
|
<Text>Status</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Uniformity (%)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>CV (%)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Chick Qty</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Uniform Qty</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Mean Up</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Mean Down</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table Body */}
|
||||||
|
{data.map((item: Uniformity, index: number) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableRow,
|
||||||
|
index < data.length - 1 ? pdfStyles.tableBorderBottom : {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||||
|
<Text>{index + 1}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>{item.location_name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>{item.flock_name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
|
<Text>{item.kandang_name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>
|
||||||
|
{formatDate(item.applied_at, 'DD MMM YYYY')} (Week {item.week}
|
||||||
|
)
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
|
<View style={pdfStyles.badge}>
|
||||||
|
<Text>{getStatusText(item.status)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.uniformity)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.cv)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatNumber(item.chick_qty_of_weight)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.uniform_qty)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.mean_up)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.mean_down)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateUniformityPDF = async (
|
||||||
|
data: UniformityExportPDFParams['data'],
|
||||||
|
params: UniformityExportPDFParams['params']
|
||||||
|
): Promise<void> => {
|
||||||
|
const PDFDocument = createPDFDocument(data, params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await pdf(PDFDocument).toBlob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const period =
|
||||||
|
params.start_date && params.end_date
|
||||||
|
? `${params.start_date}-${params.end_date}`
|
||||||
|
: formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
link.download = `laporan-uniformity-${period}.pdf`;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { formatNumber, formatDate } from '@/lib/helper';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
||||||
|
|
||||||
|
export const generateUniformityTemplate = (
|
||||||
|
population: number,
|
||||||
|
projectFlockKandangLookup: ProjectFlockKandangLookup
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const sampleSize = Math.round(population * 0.02);
|
||||||
|
const kandangName = projectFlockKandangLookup.kandang?.name || 'Kandang';
|
||||||
|
const flockName = projectFlockKandangLookup.project_flock?.flock_name || '';
|
||||||
|
const flockPeriod = projectFlockKandangLookup.project_flock?.period || 1;
|
||||||
|
const locationName =
|
||||||
|
projectFlockKandangLookup.project_flock?.location?.name || '';
|
||||||
|
|
||||||
|
const instructions = [
|
||||||
|
['PETUNJUK PENGISIAN DATA UNIFORMITY'],
|
||||||
|
[''],
|
||||||
|
['INFORMASI FLOCK'],
|
||||||
|
['Lokasi', locationName],
|
||||||
|
['Nama Flock', flockName],
|
||||||
|
['Periode', flockPeriod],
|
||||||
|
['Kandang', kandangName],
|
||||||
|
['Total Populasi', formatNumber(population)],
|
||||||
|
['Jumlah Sampel (2%)', formatNumber(sampleSize)],
|
||||||
|
[''],
|
||||||
|
['CARA PENGISIAN:'],
|
||||||
|
['1. Pindah ke sheet ke-2 untuk mengisi data BW (Body Weight).'],
|
||||||
|
[
|
||||||
|
'2. Kolom NO sudah terisi otomatis dari 1 sampai ' +
|
||||||
|
formatNumber(sampleSize),
|
||||||
|
],
|
||||||
|
['3. Isi kolom BW dengan berat badan ayam dalam gram.'],
|
||||||
|
['4. Pastikan baris terisi dengan data yang valid.'],
|
||||||
|
['5. Simpan file dan upload kembali ke sistem.'],
|
||||||
|
[''],
|
||||||
|
['FORMAT DATA:'],
|
||||||
|
['• NO: Nomor urut ayam (1, 2, 3, ...).'],
|
||||||
|
['• BW: Berat badan dalam gram (contoh: 1500, 1650, 1800).'],
|
||||||
|
[''],
|
||||||
|
['CATATAN:'],
|
||||||
|
[
|
||||||
|
'1. File ini dibuat secara otomatis berdasarkan ukuran sampling (2% dari total populasi).',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'2. Jumlah baris sudah ditentukan dan boleh ditambah asal angkanya berurutan.',
|
||||||
|
],
|
||||||
|
['3. Silakan isi berat badan (gram) untuk setiap ayam yang disampling.'],
|
||||||
|
[
|
||||||
|
'4. Biarkan sel kosong jika data tidak tersedia, jangan dihapus nomornya.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const instructionSheet = XLSX.utils.aoa_to_sheet(instructions);
|
||||||
|
instructionSheet['!cols'] = [
|
||||||
|
{ wch: 30 }, // Column A width
|
||||||
|
{ wch: 40 }, // Column B width
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = Array.from({ length: sampleSize }, (_, index) => ({
|
||||||
|
NO: index + 1,
|
||||||
|
BW: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dataWorksheet = XLSX.utils.json_to_sheet(data, {
|
||||||
|
header: ['NO', 'BW'],
|
||||||
|
});
|
||||||
|
|
||||||
|
dataWorksheet['!cols'] = [{ wch: 10 }, { wch: 15 }];
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, instructionSheet, 'Instruksi');
|
||||||
|
|
||||||
|
const dataSheetName =
|
||||||
|
kandangName.length > 31 ? kandangName.substring(0, 31) : kandangName;
|
||||||
|
XLSX.utils.book_append_sheet(workbook, dataWorksheet, dataSheetName);
|
||||||
|
|
||||||
|
const sanitizedFlockName = flockName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
|
const sanitizedKandangName = kandangName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
|
const filename = `${formatDate(
|
||||||
|
new Date(),
|
||||||
|
'YYYY-MM-DD'
|
||||||
|
)}-${sanitizedFlockName}-${sanitizedKandangName}-periode-${flockPeriod}-data-${sampleSize}.xlsx`;
|
||||||
|
|
||||||
|
XLSX.writeFile(workbook, filename);
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Template berhasil dibuat dengan ${formatNumber(sampleSize)} baris data (2% dari ${formatNumber(population)} populasi).`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating uniformity template:', error);
|
||||||
|
toast.error('Gagal membuat template Excel. Silakan coba lagi.');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import { Uniformity } from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
|
type UniformityFormSchemaType = {
|
||||||
|
date: string;
|
||||||
|
week: number;
|
||||||
|
location?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
location_id: number;
|
||||||
|
project_flock?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
project_flock_id: number;
|
||||||
|
project_flock_kandang_id: number | null;
|
||||||
|
kandang?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
kandang_id: number;
|
||||||
|
document: File | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileSchema = Yup.mixed<File>()
|
||||||
|
.test('documentSize', 'Ukuran file maksimal 2 MB', (value): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (value instanceof File) return value.size <= 2 * 1024 * 1024;
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.test('documentType', 'Format file harus Excel', (value): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (value instanceof File) {
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'text/csv',
|
||||||
|
];
|
||||||
|
return allowedTypes.includes(value.type);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UniformityFormSchema: Yup.ObjectSchema<UniformityFormSchemaType> =
|
||||||
|
Yup.object({
|
||||||
|
date: Yup.string().required('Tanggal wajib diisi!'),
|
||||||
|
week: Yup.number()
|
||||||
|
.min(1, 'Minggu ke wajib diisi!')
|
||||||
|
.required('Minggu ke wajib diisi!')
|
||||||
|
.typeError('Minggu ke wajib diisi!'),
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.min(1, 'Location wajib diisi!')
|
||||||
|
.required('Location wajib diisi!')
|
||||||
|
.typeError('Location wajib diisi!'),
|
||||||
|
project_flock: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.number()
|
||||||
|
.min(1, 'Project flock wajib diisi!')
|
||||||
|
.required('Project flock wajib diisi!')
|
||||||
|
.typeError('Project flock wajib diisi!'),
|
||||||
|
project_flock_kandang_id: Yup.number().optional().nullable().default(null),
|
||||||
|
kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.number()
|
||||||
|
.min(1, 'Kandang wajib diisi!')
|
||||||
|
.required('Kandang wajib diisi!')
|
||||||
|
.typeError('Kandang wajib diisi!'),
|
||||||
|
document: FileSchema.required('File wajib diisi!'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UniformityFormValues = Yup.InferType<typeof UniformityFormSchema>;
|
||||||
|
|
||||||
|
export type UniformityFormData = {
|
||||||
|
date: string;
|
||||||
|
week: number;
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
document: File | null;
|
||||||
|
document_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUniformityFormInitialValues = (
|
||||||
|
initialValues?: Partial<Uniformity>
|
||||||
|
): UniformityFormValues => {
|
||||||
|
return {
|
||||||
|
date: initialValues?.week ? '' : '',
|
||||||
|
week: initialValues?.week ?? 0,
|
||||||
|
location: null,
|
||||||
|
location_id: 0,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_id: 0,
|
||||||
|
project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? null,
|
||||||
|
kandang: null,
|
||||||
|
kandang_id: 0,
|
||||||
|
document: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,708 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import moment from 'moment';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
|
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
|
import {
|
||||||
|
UniformityFormSchema,
|
||||||
|
UniformityFormValues,
|
||||||
|
getUniformityFormInitialValues,
|
||||||
|
} from '@/components/pages/production/uniformity/form/UniformityForm.schema';
|
||||||
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
|
import {
|
||||||
|
ProjectFlockApi,
|
||||||
|
ProjectFlockKandangApi,
|
||||||
|
} from '@/services/api/production';
|
||||||
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import {
|
||||||
|
Uniformity,
|
||||||
|
VerifyUniformityPayload,
|
||||||
|
} from '@/types/api/production/uniformity';
|
||||||
|
import { type BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||||
|
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||||
|
import { generateUniformityTemplate } from '@/components/pages/production/uniformity/export/UniformityTemplate';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
|
||||||
|
interface UniformityFormProps {
|
||||||
|
formType?: 'add' | 'edit';
|
||||||
|
initialValues?: Uniformity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityForm = ({
|
||||||
|
formType = 'add',
|
||||||
|
initialValues,
|
||||||
|
}: UniformityFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const subscribeValidate = useUiStore((s) => s.subscribeValidate);
|
||||||
|
const setIsValid = useUiStore((s) => s.setIsValid);
|
||||||
|
const expandedDrawerOpen = useUiStore((s) => s.expandedDrawerOpen);
|
||||||
|
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
|
||||||
|
const setExpandedDrawerContent = useUiStore(
|
||||||
|
(s) => s.setExpandedDrawerContent
|
||||||
|
);
|
||||||
|
const isNextStep = useUiStore((s) => s.isNextStep);
|
||||||
|
const setIsNextStep = useUiStore((s) => s.setIsNextStep);
|
||||||
|
|
||||||
|
const setVerifyUniformityResult = useUniformityStore(
|
||||||
|
(s) => s.setVerifyUniformityResult
|
||||||
|
);
|
||||||
|
const setUniformityFormData = useUniformityStore(
|
||||||
|
(s) => s.setUniformityFormData
|
||||||
|
);
|
||||||
|
const uniformityStep = useUniformityStore((s) => s.uniformityStep);
|
||||||
|
const setUniformityStep = useUniformityStore((s) => s.setUniformityStep);
|
||||||
|
|
||||||
|
const [uniformityFormErrorMessage, setUniformityFormErrorMessage] =
|
||||||
|
useState('');
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// ===== SELECT INPUT DATA =====
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
||||||
|
const [selectedProjectFlock, setSelectedProjectFlock] =
|
||||||
|
useState<OptionType | null>(null);
|
||||||
|
|
||||||
|
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationSelectInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocations,
|
||||||
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
// ===== FETCH PROJECT FLOCKS DATA =====
|
||||||
|
const projectFlocksUrl = useMemo(() => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
search: projectFlockSearchValue || '',
|
||||||
|
limit: '100',
|
||||||
|
});
|
||||||
|
if (selectedLocation) {
|
||||||
|
params.append('location_id', selectedLocation.value.toString());
|
||||||
|
}
|
||||||
|
return `${ProjectFlockApi.basePath}?${params.toString()}`;
|
||||||
|
}, [projectFlockSearchValue, selectedLocation]);
|
||||||
|
|
||||||
|
const { data: projectFlocksData, isLoading: isLoadingProjectFlocks } = useSWR(
|
||||||
|
projectFlocksUrl,
|
||||||
|
ProjectFlockApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectFlocksDataList =
|
||||||
|
projectFlocksData?.status === 'success'
|
||||||
|
? projectFlocksData.data
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// ===== PROJECT FLOCK OPTIONS =====
|
||||||
|
const projectFlockOptions = useMemo(() => {
|
||||||
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
|
if (isResponseSuccess(projectFlocksData)) {
|
||||||
|
const flockOptions =
|
||||||
|
projectFlocksData?.data.map((projectFlock) => ({
|
||||||
|
value: projectFlock.id,
|
||||||
|
label: projectFlock.flock_name || '',
|
||||||
|
})) || [];
|
||||||
|
options = options.concat(flockOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [projectFlocksData]);
|
||||||
|
|
||||||
|
// ===== APPROVED PROJECT FLOCK KANDANGS =====
|
||||||
|
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
step_name: 'Disetujui',
|
||||||
|
limit: '100',
|
||||||
|
});
|
||||||
|
return `${ProjectFlockKandangApi.basePath}?${params.toString()}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { data: approvedProjectFlockKandangsData } = useSWR(
|
||||||
|
approvedProjectFlockKandangsUrl,
|
||||||
|
ProjectFlockKandangApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const approvedProjectFlockKandangs = useMemo(() => {
|
||||||
|
if (!isResponseSuccess(approvedProjectFlockKandangsData)) return [];
|
||||||
|
return approvedProjectFlockKandangsData.data;
|
||||||
|
}, [approvedProjectFlockKandangsData]);
|
||||||
|
|
||||||
|
// ===== KANDANG OPTIONS (FILTERED BY SELECTED PROJECT FLOCK) =====
|
||||||
|
const kandangOptions = useMemo(() => {
|
||||||
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
|
if (selectedProjectFlock && projectFlocksDataList) {
|
||||||
|
const selectedProjectFlockData = projectFlocksDataList.find(
|
||||||
|
(pf) => pf.id === selectedProjectFlock.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedProjectFlockData?.kandangs) {
|
||||||
|
const approvedKandangIds = approvedProjectFlockKandangs
|
||||||
|
.filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value)
|
||||||
|
.map((pfk) => pfk.kandang_id);
|
||||||
|
|
||||||
|
const kandangOpts = selectedProjectFlockData.kandangs
|
||||||
|
.filter((kandang: Kandang) => {
|
||||||
|
if (formType === 'add') {
|
||||||
|
return approvedKandangIds.includes(kandang.id);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((kandang: Kandang) => ({
|
||||||
|
value: kandang.id,
|
||||||
|
label: kandang.name || '',
|
||||||
|
}));
|
||||||
|
options = options.concat(kandangOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [
|
||||||
|
selectedProjectFlock,
|
||||||
|
projectFlocksDataList,
|
||||||
|
approvedProjectFlockKandangs,
|
||||||
|
formType,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
|
if (!selectedProjectFlock || !selectedKandang) return null;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
project_flock_id: selectedProjectFlock.value.toString(),
|
||||||
|
kandang_id: selectedKandang.value.toString(),
|
||||||
|
withpopulation: Boolean(true).toString(),
|
||||||
|
});
|
||||||
|
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||||
|
}, [selectedProjectFlock, selectedKandang]);
|
||||||
|
|
||||||
|
const { data: projectFlockKandangLookupData } = useSWR(
|
||||||
|
projectFlockKandangLookupUrl,
|
||||||
|
projectFlockKandangLookupUrl
|
||||||
|
? () =>
|
||||||
|
ProjectFlockApi.getAllFetcher(
|
||||||
|
projectFlockKandangLookupUrl
|
||||||
|
) as Promise<BaseApiResponse<ProjectFlockKandangLookup>>
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectFlockKandangLookup =
|
||||||
|
projectFlockKandangLookupData?.status === 'success'
|
||||||
|
? projectFlockKandangLookupData.data
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// ===== FORM CONFIGURATION =====
|
||||||
|
const formikInitialValues = useMemo<UniformityFormValues>(
|
||||||
|
() => getUniformityFormInitialValues(initialValues),
|
||||||
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<UniformityFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema: UniformityFormSchema,
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
validateOnMount: false,
|
||||||
|
enableReinitialize: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const projectFlockKandangId = projectFlockKandangLookup?.id;
|
||||||
|
|
||||||
|
if (!projectFlockKandangId) {
|
||||||
|
setUniformityFormErrorMessage(
|
||||||
|
'Project Flock Kandang tidak ditemukan. Silakan pilih Project Flock dan Kandang yang valid.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUniformityFormData({
|
||||||
|
date: values.date,
|
||||||
|
week: values.week,
|
||||||
|
project_flock_kandang_id: projectFlockKandangId,
|
||||||
|
document: values.document as File,
|
||||||
|
document_name: (values.document as File).name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload: VerifyUniformityPayload = {
|
||||||
|
document: values.document as File,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await UniformityApi.verifyUniformity(payload);
|
||||||
|
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
setUniformityFormErrorMessage(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isResponseSuccess(res) && res.data) {
|
||||||
|
setVerifyUniformityResult(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
|
||||||
|
if (formType === 'add') {
|
||||||
|
setIsNextStep(true);
|
||||||
|
setExpandedDrawerOpen(true);
|
||||||
|
setUniformityStep('preview');
|
||||||
|
} else {
|
||||||
|
router.push('/production/uniformity');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== FORM HANDLERS =====
|
||||||
|
const handleLocationChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const location = val as OptionType | null;
|
||||||
|
const locationId = Number(location?.value);
|
||||||
|
|
||||||
|
formik.setFieldTouched('location', true);
|
||||||
|
formik.setFieldValue('location', location);
|
||||||
|
formik.setFieldTouched('location_id', true);
|
||||||
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
|
||||||
|
setSelectedLocation(location);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProjectFlockChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const projectFlock = val as OptionType | null;
|
||||||
|
const projectFlockId = Number(projectFlock?.value);
|
||||||
|
|
||||||
|
formik.setFieldTouched('project_flock', true);
|
||||||
|
formik.setFieldValue('project_flock', projectFlock);
|
||||||
|
formik.setFieldTouched('project_flock_id', true);
|
||||||
|
formik.setFieldValue('project_flock_id', projectFlockId);
|
||||||
|
|
||||||
|
setSelectedProjectFlock(projectFlock);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKandangChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const kandang = val as OptionType | null;
|
||||||
|
const kandangId = Number(kandang?.value);
|
||||||
|
|
||||||
|
formik.setFieldTouched('kandang', true);
|
||||||
|
formik.setFieldValue('kandang', kandang);
|
||||||
|
formik.setFieldTouched('kandang_id', true);
|
||||||
|
formik.setFieldValue('kandang_id', kandangId);
|
||||||
|
|
||||||
|
setSelectedKandang(kandang);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFileChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const document = e.target.files?.[0];
|
||||||
|
|
||||||
|
formik.setFieldTouched('document', true);
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
formik.setFieldValue('document', undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.size > 2 * 1024 * 1024) {
|
||||||
|
toast.error(`Ukuran file ${document.name} maksimal 2 MB!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'text/csv',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(document.type)) {
|
||||||
|
toast.error(`Format file ${document.name} harus Excel atau CSV!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldValue('document', document);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDateChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
formik.setFieldValue('date', e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveFile = useCallback(() => {
|
||||||
|
formik.setFieldValue('document', undefined);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}, [formik]);
|
||||||
|
|
||||||
|
const handleDownloadTemplate = useCallback(() => {
|
||||||
|
const population = projectFlockKandangLookup?.population;
|
||||||
|
|
||||||
|
if (!population || !projectFlockKandangLookup) {
|
||||||
|
toast.error('Silakan pilih Project Flock dan Kandang terlebih dahulu.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateUniformityTemplate(population, projectFlockKandangLookup);
|
||||||
|
}, [projectFlockKandangLookup]);
|
||||||
|
|
||||||
|
// ===== SIDE EFFECTS =====
|
||||||
|
useEffect(() => {
|
||||||
|
if (formik.values.date) {
|
||||||
|
const date = moment(formik.values.date);
|
||||||
|
const weekNumber = date.week() - moment(date).startOf('month').week() + 1;
|
||||||
|
const adjustedWeekNumber = weekNumber <= 0 ? weekNumber + 52 : weekNumber;
|
||||||
|
|
||||||
|
formik.setFieldValue('week', adjustedWeekNumber);
|
||||||
|
}
|
||||||
|
}, [formik.values.date]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = subscribeValidate(() => {
|
||||||
|
setIsValid(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub?.();
|
||||||
|
useUiStore.getState().setExpandedDrawerOpen(false);
|
||||||
|
useUiStore.getState().setExpandedDrawerContent(null);
|
||||||
|
useUiStore.getState().setIsNextStep(false);
|
||||||
|
useUniformityStore.getState().setUniformityStep('preview');
|
||||||
|
useUniformityStore.getState().setVerifyUniformityResult(null);
|
||||||
|
};
|
||||||
|
}, [subscribeValidate, setIsValid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expandedDrawerOpen) {
|
||||||
|
if (uniformityStep === 'preview') {
|
||||||
|
setExpandedDrawerContent(<UniformityPreviewForm />);
|
||||||
|
} else if (uniformityStep === 'result') {
|
||||||
|
setExpandedDrawerContent(<UniformityResultForm />);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setExpandedDrawerContent(null);
|
||||||
|
setIsNextStep(false);
|
||||||
|
setUniformityStep('preview');
|
||||||
|
setVerifyUniformityResult(null);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
expandedDrawerOpen,
|
||||||
|
uniformityStep,
|
||||||
|
setExpandedDrawerContent,
|
||||||
|
setIsNextStep,
|
||||||
|
setUniformityStep,
|
||||||
|
setVerifyUniformityResult,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full'>
|
||||||
|
<DrawerHeader
|
||||||
|
leftIcon={formType == 'add' ? 'mdi:close' : 'mdi:arrow-left'}
|
||||||
|
leftIconSize={24}
|
||||||
|
leftIconHref={
|
||||||
|
formType == 'add'
|
||||||
|
? '/production/uniformity'
|
||||||
|
: `/production/uniformity/detail`
|
||||||
|
}
|
||||||
|
leftIconClassName='hover:text-gray-400'
|
||||||
|
subtitle={formType == 'add' ? 'Add Uniformity' : 'Update Uniformity'}
|
||||||
|
subtitleClassName='text-sm text-neutral'
|
||||||
|
showDivider
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='divider mt-3'></div>
|
||||||
|
<section className='w-full px-6 mb-6'>
|
||||||
|
<h2 className='text-2xl font-semibold mb-6'>Informasi Umum</h2>
|
||||||
|
|
||||||
|
<form onSubmit={formik.handleSubmit} className='flex flex-col gap-6'>
|
||||||
|
{uniformityFormErrorMessage && (
|
||||||
|
<div className='alert alert-error' role='alert'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{uniformityFormErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
required
|
||||||
|
label='Tanggal'
|
||||||
|
name='date'
|
||||||
|
value={formik.values.date}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={formik.touched.date && Boolean(formik.errors.date)}
|
||||||
|
errorMessage={formik.errors.date as string}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi...'
|
||||||
|
value={formik.values.location}
|
||||||
|
onChange={handleLocationChange}
|
||||||
|
options={locationOptions}
|
||||||
|
onInputChange={setLocationSelectInputValue}
|
||||||
|
isLoading={isLoadingLocations}
|
||||||
|
isError={
|
||||||
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.location_id as string}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock...'
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={handleProjectFlockChange}
|
||||||
|
options={projectFlockOptions}
|
||||||
|
onInputChange={setProjectFlockSearchValue}
|
||||||
|
isLoading={isLoadingProjectFlocks}
|
||||||
|
isDisabled={!formik.values.location_id}
|
||||||
|
isError={
|
||||||
|
formik.touched.project_flock_id &&
|
||||||
|
Boolean(formik.errors.project_flock_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.project_flock_id as string}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
required
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang...'
|
||||||
|
value={formik.values.kandang}
|
||||||
|
onChange={handleKandangChange}
|
||||||
|
options={kandangOptions}
|
||||||
|
isDisabled={!formik.values.project_flock_id}
|
||||||
|
isError={
|
||||||
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.kandang_id as string}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<label
|
||||||
|
htmlFor='file-upload-input'
|
||||||
|
className={cn(
|
||||||
|
"w-full text-sm font-normal leading-5 after:content-['*'] after:ml-0.5 after:text-red-500",
|
||||||
|
formik.touched.document &&
|
||||||
|
formik.errors.document &&
|
||||||
|
'text-red-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Upload File
|
||||||
|
</label>
|
||||||
|
{formik.values.document && !isNextStep ? (
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveFile}
|
||||||
|
className='cursor-pointer'
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons-solid:trash'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-gray-400 hover:text-gray-600'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : !formik.values.document && !isNextStep ? (
|
||||||
|
<button className='cursor-pointer' type='button'>
|
||||||
|
<Tooltip
|
||||||
|
position='left'
|
||||||
|
content='Pastikan file yang diunggah sesuai dengan template yang disediakan, template akan menyesuaikan dengan jumlah populasi pada kandang yang dipilih.'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:information-circle'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-gray-400 hover:text-gray-600'
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
'h-full w-full border rounded-2xl border-dashed cursor-pointer mt-2',
|
||||||
|
formik.touched.document && formik.errors.document
|
||||||
|
? 'border-red-500'
|
||||||
|
: 'border-gray-300'
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
document.getElementById('file-upload-input')?.click()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formik.values.document ? (
|
||||||
|
<div className='flex flex-col items-center justify-center gap-2 my-10'>
|
||||||
|
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
document.getElementById('file-upload-input')?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={'heroicons:document-text'}
|
||||||
|
className='text-2xl text-white'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<span className='text-md font-semibold text-black line-clamp-2 text-center max-w-xs break-all'>
|
||||||
|
{formik.values.document.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-center justify-center text-center gap-2 ${projectFlockKandangLookup?.available_quantity ? 'my-10 mb-0' : 'my-10'}`}
|
||||||
|
>
|
||||||
|
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
document
|
||||||
|
.getElementById('file-upload-input')
|
||||||
|
?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={'heroicons-solid:arrow-up-tray'}
|
||||||
|
className='text-2xl text-white'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<span className='text-md font-semibold text-[#18181B80]'>
|
||||||
|
Choose file to upload
|
||||||
|
</span>
|
||||||
|
<span className='text-xs font-light text-[#18181B80] text-center max-w-xs px-4'>
|
||||||
|
{projectFlockKandangLookup?.population
|
||||||
|
? `Jumlah data yang dibutuhkan: ${formatNumber(Math.round(projectFlockKandangLookup.population * 0.02))} (2% dari ${formatNumber(projectFlockKandangLookup.population)} populasi).`
|
||||||
|
: 'Upload data file (*.xlsx)'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{projectFlockKandangLookup?.population && (
|
||||||
|
<>
|
||||||
|
<div className='flex items-center justify-center gap-2 py-4'>
|
||||||
|
<div className='h-px bg-[#18181B33] w-8'></div>
|
||||||
|
<span className='text-[#18181B33] text-xs'>
|
||||||
|
Templates
|
||||||
|
</span>
|
||||||
|
<div className='h-px bg-[#18181B33] w-8'></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center justify-center mb-10'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='outline'
|
||||||
|
className='btn-sm rounded-2xl shadow-md border border-base-300'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDownloadTemplate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:arrow-down-tray'
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
Template XLSX
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type='file'
|
||||||
|
id='file-upload-input'
|
||||||
|
name='document'
|
||||||
|
accept='application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv'
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className='hidden'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.touched.document && formik.errors.document && (
|
||||||
|
<p className='w-full text-sm text-red-500 mt-2'>
|
||||||
|
{formik.errors.document as string}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isNextStep && (
|
||||||
|
<RequirePermission permissions='lti.production.uniformity.create'>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
color='primary'
|
||||||
|
className='w-full'
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
{formik.isSubmitting ? (
|
||||||
|
<span className='loading loading-spinner'></span>
|
||||||
|
) : (
|
||||||
|
'Next'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityForm;
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import {
|
||||||
|
BodyWeightData,
|
||||||
|
UniformityDetailItem,
|
||||||
|
} from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
|
const UniformityPreviewForm = () => {
|
||||||
|
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
|
||||||
|
const setIsNextStep = useUiStore((s) => s.setIsNextStep);
|
||||||
|
const setUniformityStep = useUniformityStore((s) => s.setUniformityStep);
|
||||||
|
const verifyUniformityResult = useUniformityStore(
|
||||||
|
(s) => s.verifyUniformityResult
|
||||||
|
);
|
||||||
|
const uniformityFormData = useUniformityStore((s) => s.uniformityFormData);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
setIsNextStep(false);
|
||||||
|
setUniformityStep('preview');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
setUniformityStep('result');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!verifyUniformityResult) return [];
|
||||||
|
|
||||||
|
return verifyUniformityResult.uniformity_details.map(
|
||||||
|
(detail: UniformityDetailItem, index: number) => ({
|
||||||
|
id: `weight-${index}`,
|
||||||
|
number: index + 1,
|
||||||
|
weight: detail.weight,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [verifyUniformityResult]);
|
||||||
|
|
||||||
|
const columns: ColumnDef<BodyWeightData>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'number',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => props.row.original.number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'weight',
|
||||||
|
header: 'Weight (g)',
|
||||||
|
cell: (props) => <span>{props.row.original.weight}</span>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full h-full bg-white border-l border-gray-200'>
|
||||||
|
{/* Header */}
|
||||||
|
<DrawerHeader
|
||||||
|
leftIcon=''
|
||||||
|
subtitle={uniformityFormData?.file_name || 'Add Body Weight'}
|
||||||
|
subtitleClassName='text-sm text-neutral line-clamp-1'
|
||||||
|
showDivider={false}
|
||||||
|
>
|
||||||
|
<Button variant='link' className='p-0 text-error' onClick={handleClose}>
|
||||||
|
<Tooltip content='Hapus' position='left'>
|
||||||
|
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DrawerHeader>
|
||||||
|
|
||||||
|
{/* Form Section */}
|
||||||
|
<div className='divider mt-3.5'></div>
|
||||||
|
<section className='w-full px-6'>
|
||||||
|
{verifyUniformityResult ? (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<Table<BodyWeightData>
|
||||||
|
data={tableData}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={15}
|
||||||
|
className={{ containerClassName: 'mb-5' }}
|
||||||
|
/>
|
||||||
|
<RequirePermission permissions='lti.production.uniformity.create'>
|
||||||
|
<Button color='primary' onClick={handleNext} className='mb-10'>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col items-center justify-center py-10 text-gray-400'>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:file-document-outline'
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className='mb-4'
|
||||||
|
/>
|
||||||
|
<p className='text-lg'>No data available</p>
|
||||||
|
<p className='text-sm'>Upload a file to verify uniformity</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityPreviewForm;
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { UniformityApi } from '@/services/api/uniformity';
|
||||||
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
import {
|
||||||
|
getWeightStatusColor,
|
||||||
|
getWeightStatusIndicatorColor,
|
||||||
|
getWeightStatusText,
|
||||||
|
} from '@/components/pages/production/uniformity/uniformity-utils';
|
||||||
|
import { DetailOptionType } from '@/types/api/production/uniformity';
|
||||||
|
import {
|
||||||
|
BodyWeightData,
|
||||||
|
UniformityDetailItem,
|
||||||
|
} from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
|
const UniformityResultForm = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
|
||||||
|
const setIsNextStep = useUiStore((s) => s.setIsNextStep);
|
||||||
|
const setUniformityStep = useUniformityStore((s) => s.setUniformityStep);
|
||||||
|
const verifyUniformityResult = useUniformityStore(
|
||||||
|
(s) => s.verifyUniformityResult
|
||||||
|
);
|
||||||
|
const setVerifyUniformityResult = useUniformityStore(
|
||||||
|
(s) => s.setVerifyUniformityResult
|
||||||
|
);
|
||||||
|
const uniformityFormData = useUniformityStore((s) => s.uniformityFormData);
|
||||||
|
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
|
||||||
|
const setCreatedUniformity = useUniformityStore(
|
||||||
|
(s) => s.setCreatedUniformity
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
setIsNextStep(false);
|
||||||
|
setUniformityStep('preview');
|
||||||
|
setVerifyUniformityResult(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!uniformityFormData || !uniformityFormData.document) {
|
||||||
|
toast.error('Form data is missing. Please try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
date: uniformityFormData.date,
|
||||||
|
week: uniformityFormData.week,
|
||||||
|
project_flock_kandang_id: uniformityFormData.project_flock_kandang_id,
|
||||||
|
document: uniformityFormData.document,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await UniformityApi.createUniformity(payload);
|
||||||
|
|
||||||
|
if (!res || isResponseError(res)) {
|
||||||
|
toast.error(res?.message || 'Failed to create uniformity');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreatedUniformity(res.data);
|
||||||
|
setIsSuccess(true);
|
||||||
|
setExpandedDrawerOpen(false);
|
||||||
|
setIsNextStep(false);
|
||||||
|
setUniformityStep('preview');
|
||||||
|
setVerifyUniformityResult(null);
|
||||||
|
router.push('/production/uniformity');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const samplingTableData: DetailOptionType[] = useMemo(() => {
|
||||||
|
if (!verifyUniformityResult) return [];
|
||||||
|
|
||||||
|
const { sampling } = verifyUniformityResult;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'sampling-size',
|
||||||
|
label: 'Sampling size',
|
||||||
|
value: `${formatNumber(sampling.chick_qty_of_weight)} of Birds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mean-weight',
|
||||||
|
label: 'Mean Weight',
|
||||||
|
value: `${sampling.mean_weight} g`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'min-limit',
|
||||||
|
label: 'Min Limit (-10%)',
|
||||||
|
value: `${sampling.mean_down} g`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'max-limit',
|
||||||
|
label: 'Max Limit (+10%)',
|
||||||
|
value: `${sampling.mean_up} g`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [verifyUniformityResult]);
|
||||||
|
|
||||||
|
const columnsSampling: ColumnDef<DetailOptionType>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'label',
|
||||||
|
header: 'Label',
|
||||||
|
cell: (props) => props.row.original.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'value',
|
||||||
|
header: 'Value',
|
||||||
|
cell: (props) => <span>{props.row.original.value}</span>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultTableData: DetailOptionType[] = useMemo(() => {
|
||||||
|
if (!verifyUniformityResult) return [];
|
||||||
|
|
||||||
|
const { result } = verifyUniformityResult;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'ideal-birds',
|
||||||
|
label: 'Ideal Birds',
|
||||||
|
value: `${formatNumber(result.uniform_qty)} of Birds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'outside-range',
|
||||||
|
label: 'Outside Range',
|
||||||
|
value: `${formatNumber(result.outside_qty)} of Birds`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uniformity',
|
||||||
|
label: 'Uniformity',
|
||||||
|
value: `${result.uniformity} %`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [verifyUniformityResult]);
|
||||||
|
|
||||||
|
const resultColumns: ColumnDef<DetailOptionType>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'label',
|
||||||
|
header: 'Label',
|
||||||
|
cell: (props) => props.row.original.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'value',
|
||||||
|
header: 'Value',
|
||||||
|
cell: (props) => <span>{props.row.original.value}</span>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!verifyUniformityResult) return [];
|
||||||
|
|
||||||
|
return verifyUniformityResult.uniformity_details.map(
|
||||||
|
(detail: UniformityDetailItem, index: number) => ({
|
||||||
|
id: `body-weight-${index + 1}`,
|
||||||
|
number: index + 1,
|
||||||
|
weight: detail.weight,
|
||||||
|
status: detail.range.toLowerCase() as 'ideal' | 'outside',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [verifyUniformityResult]);
|
||||||
|
|
||||||
|
const columnsUniformity: ColumnDef<BodyWeightData>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: 'number',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => props.row.original.number,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'weight',
|
||||||
|
header: 'Weight (g)',
|
||||||
|
cell: (props) => <span>{props.row.original.weight}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: (props) => {
|
||||||
|
const status = props.row.original.status;
|
||||||
|
return status ? (
|
||||||
|
<div className='w-full'>
|
||||||
|
<Badge
|
||||||
|
statusIndicator={true}
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge: `rounded-xl w-full justify-start border border-gray-200 text-black ${getWeightStatusColor(status)}`,
|
||||||
|
status: getWeightStatusIndicatorColor(status),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getWeightStatusText(status)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Badge
|
||||||
|
statusIndicator={true}
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'rounded-xl w-full justify-start border border-gray-200 text-black bg-info/10',
|
||||||
|
status: 'bg-info',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unknown
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full h-full bg-white border-l border-gray-200'>
|
||||||
|
{/* Header */}
|
||||||
|
<DrawerHeader
|
||||||
|
leftIcon=''
|
||||||
|
subtitle={uniformityFormData?.document_name || 'Uniformity Result'}
|
||||||
|
subtitleClassName='text-sm text-neutral line-clamp-1'
|
||||||
|
showDivider={false}
|
||||||
|
>
|
||||||
|
<Button variant='link' className='p-0 text-error' onClick={handleClose}>
|
||||||
|
<Tooltip content='Hapus' position='left'>
|
||||||
|
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DrawerHeader>
|
||||||
|
|
||||||
|
{/* Form Section */}
|
||||||
|
<div className='divider mt-3.5'></div>
|
||||||
|
<section className='w-full px-6'>
|
||||||
|
{verifyUniformityResult ? (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<div className=''>
|
||||||
|
<p className='text-sm font-medium mb-5'>Sampling and Range</p>
|
||||||
|
<Table<DetailOptionType>
|
||||||
|
data={samplingTableData}
|
||||||
|
columns={columnsSampling}
|
||||||
|
pageSize={4}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className=''>
|
||||||
|
<p className='text-sm font-medium mb-5'>Result</p>
|
||||||
|
<Table<DetailOptionType>
|
||||||
|
data={resultTableData}
|
||||||
|
columns={resultColumns}
|
||||||
|
pageSize={3}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mb-0',
|
||||||
|
paginationClassName: 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Table<BodyWeightData>
|
||||||
|
data={tableData}
|
||||||
|
columns={columnsUniformity}
|
||||||
|
pageSize={15}
|
||||||
|
className={{ containerClassName: 'mb-5' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<RequirePermission permissions='lti.production.uniformity.create'>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={handleSubmit}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
disabled={!uniformityFormData}
|
||||||
|
className='mb-10'
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='flex flex-col items-center justify-center py-10 text-gray-400'>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:file-document-outline'
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className='mb-4'
|
||||||
|
/>
|
||||||
|
<p className='text-lg'>No data available</p>
|
||||||
|
<p className='text-sm'>Upload a file to verify uniformity</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityResultForm;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
const LeftLegend = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='skeleton h-30 w-4 flex items-center justify-center self-center mb-10' />
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 justify-center items-center'>
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`grid-${index}`}
|
||||||
|
className='shrink-0 flex flex-col justify-center mb-10'
|
||||||
|
>
|
||||||
|
<div className='skeleton h-4 w-8' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartArea = () => {
|
||||||
|
const ranges = [
|
||||||
|
'48-52',
|
||||||
|
'52-56',
|
||||||
|
'56-60',
|
||||||
|
'60-64',
|
||||||
|
'64-68',
|
||||||
|
'68-72',
|
||||||
|
'72-76',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex-1 min-w-0 flex flex-col relative'>
|
||||||
|
<div className='flex-1 ml-6 min-w-0 flex flex-col'>
|
||||||
|
<div className='flex-1 relative flex flex-col justify-between py-4'>
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`grid-${index}`}
|
||||||
|
className='w-full border-b border-gray-200 absolute'
|
||||||
|
style={{ top: `${(index / 4) * 100}%` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-between gap-2 sm:gap-4 md:gap-8 lg:gap-12 px-2 sm:px-4 py-2'>
|
||||||
|
{ranges.map((range) => (
|
||||||
|
<div
|
||||||
|
key={range}
|
||||||
|
className='skeleton h-3 w-8 sm:w-12 md:w-16 shrink-0'
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-center pb-1'>
|
||||||
|
<div className='skeleton h-3 w-20 sm:w-28' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyState = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='absolute inset-0 flex flex-col items-center justify-center z-10 gap-2'>
|
||||||
|
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center my-2'>
|
||||||
|
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
|
||||||
|
<Icon icon={'heroicons:funnel'} className='text-4xl text-whitd' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<span className='text-xl font-semibold text-[#18181B80] leading-5'>
|
||||||
|
No Filters Selected
|
||||||
|
</span>
|
||||||
|
<span className='text-xs font-light text-[#18181B80] leading-4 text-center max-w-xs px-4'>
|
||||||
|
Please choose filters to narrow down your results and make your search
|
||||||
|
easier.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UniformityBarChartSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className='relative w-full h-full min-h-[300px] xl:min-h-[350px]'>
|
||||||
|
<div className='sm:flex hidden h-full gap-4'>
|
||||||
|
<LeftLegend />
|
||||||
|
<ChartArea />
|
||||||
|
</div>
|
||||||
|
<EmptyState />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityBarChartSkeleton;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';
|
||||||
|
|
||||||
|
interface UniformityGaugeChartSkeletonProps {
|
||||||
|
label?: string;
|
||||||
|
kandang?: string;
|
||||||
|
week?: string;
|
||||||
|
currentValue?: number;
|
||||||
|
totalValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UniformityGaugeChartSkeleton: React.FC<
|
||||||
|
UniformityGaugeChartSkeletonProps
|
||||||
|
> = ({}) => {
|
||||||
|
const numberOfSegments = 50;
|
||||||
|
const value = 0;
|
||||||
|
const filledSegments = Math.round((value / 100) * numberOfSegments);
|
||||||
|
|
||||||
|
const data = Array.from({ length: numberOfSegments }, (_, index) => ({
|
||||||
|
name: index,
|
||||||
|
value: 1,
|
||||||
|
filled: index < filledSegments,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activeColor = '#1890ff';
|
||||||
|
const inactiveColor = '#f0f0f0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col w-full'>
|
||||||
|
<div className='h-64 w-full relative flex justify-center min-h-[256px]'>
|
||||||
|
<div className='relative w-full h-full flex flex-col items-center justify-end min-w-0'>
|
||||||
|
<ResponsiveContainer width='100%' height={256}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={data}
|
||||||
|
cx='50%'
|
||||||
|
cy='70%'
|
||||||
|
startAngle={180}
|
||||||
|
endAngle={0}
|
||||||
|
innerRadius='75%'
|
||||||
|
outerRadius='100%'
|
||||||
|
paddingAngle={2}
|
||||||
|
dataKey='value'
|
||||||
|
stroke='none'
|
||||||
|
isAnimationActive={false}
|
||||||
|
>
|
||||||
|
{data.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={entry.filled ? activeColor : inactiveColor}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<div className='absolute inset-x-0 top-24 flex flex-col items-center justify-center'>
|
||||||
|
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center mt-5'>
|
||||||
|
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
|
||||||
|
<Icon
|
||||||
|
icon={'heroicons:funnel'}
|
||||||
|
className='text-4xl text-whitd'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<span className='text-lg font-semibold text-[#18181B80] leading-5 my-3'>
|
||||||
|
No Filters Selected
|
||||||
|
</span>
|
||||||
|
<span className='text-xs font-light text-[#18181B80] leading-4 text-center max-w-xs px-4'>
|
||||||
|
Please choose filters to narrow down your results and make your
|
||||||
|
search easier.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityGaugeChartSkeleton;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Button from '@/components/Button';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
const UniformityTableSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center justify-center gap-2 my-20'>
|
||||||
|
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
|
||||||
|
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
|
||||||
|
<Icon
|
||||||
|
icon={'heroicons-outline:chart-bar'}
|
||||||
|
className='text-4xl text-whitd'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<span className='text-xl font-semibold text-[#18181B80] leading-5'>
|
||||||
|
No Data Available
|
||||||
|
</span>
|
||||||
|
<span className='text-xs font-light text-[#18181B80] leading-4 text-center max-w-xs px-4'>
|
||||||
|
There is no uniformity data displayed. Enter uniformity check data to
|
||||||
|
get started.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UniformityTableSkeleton;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
export const weightStatusColorMap: Record<string, string> = {
|
||||||
|
ideal: 'bg-[#00D39033]',
|
||||||
|
outside: 'bg-error/10',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const weightStatusIndicatorColorMap: Record<string, string> = {
|
||||||
|
ideal: 'bg-[#008000]',
|
||||||
|
outside: 'bg-error',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const weightStatusTextMap: Record<string, string> = {
|
||||||
|
ideal: 'Ideal',
|
||||||
|
outside: 'Outside',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWeightStatusColor = (status: string): string => {
|
||||||
|
return weightStatusColorMap[status] || 'bg-info';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWeightStatusIndicatorColor = (status: string): string => {
|
||||||
|
return weightStatusIndicatorColorMap[status] || 'bg-info';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWeightStatusText = (status: string): string => {
|
||||||
|
return weightStatusTextMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusColorMap: Record<string, string> = {
|
||||||
|
APPROVED: 'bg-[#00D39033]',
|
||||||
|
Disetujui: 'bg-[#00D39033]',
|
||||||
|
REJECTED: 'bg-error/10',
|
||||||
|
Ditolak: 'bg-error/10',
|
||||||
|
CREATED: 'bg-[#f3f3f4]',
|
||||||
|
Pengajuan: 'bg-[#f3f3f4]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusIndicatorColorMap: Record<string, string> = {
|
||||||
|
APPROVED: 'bg-[#008000]',
|
||||||
|
Disetujui: 'bg-[#008000]',
|
||||||
|
REJECTED: 'bg-error',
|
||||||
|
Ditolak: 'bg-error',
|
||||||
|
CREATED: 'bg-[#D9D9D9]',
|
||||||
|
Pengajuan: 'bg-[#D9D9D9]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statusTextMap: Record<string, string> = {
|
||||||
|
APPROVED: 'Disetujui',
|
||||||
|
Disetujui: 'Disetujui',
|
||||||
|
REJECTED: 'Ditolak',
|
||||||
|
Ditolak: 'Ditolak',
|
||||||
|
CREATED: 'Pengajuan',
|
||||||
|
Pengajuan: 'Pengajuan',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStatusColor = (status: string): string => {
|
||||||
|
return statusColorMap[status] || 'bg-info';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStatusIndicatorColor = (status: string): string => {
|
||||||
|
return statusIndicatorColorMap[status] || 'bg-info';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStatusText = (status: string): string => {
|
||||||
|
return statusTextMap[status] || status;
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { useSearchParams } from 'next/navigation';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import FileInput from '@/components/input/FileInput';
|
||||||
import SelectInput, {
|
import SelectInput, {
|
||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
@@ -66,6 +67,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
| 'expedition_vendor_id'
|
| 'expedition_vendor_id'
|
||||||
| 'received_qty'
|
| 'received_qty'
|
||||||
| 'transport_per_item'
|
| 'transport_per_item'
|
||||||
|
| 'travel_documents'
|
||||||
): { isError: boolean; errorMessage: string } => {
|
): { isError: boolean; errorMessage: string } => {
|
||||||
const touchedItem = formik.touched.items?.[idx];
|
const touchedItem = formik.touched.items?.[idx];
|
||||||
const errorItem = formik.errors.items?.[idx] as
|
const errorItem = formik.errors.items?.[idx] as
|
||||||
@@ -185,6 +187,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
: formItem.transport_per_item || 0,
|
: formItem.transport_per_item || 0,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
|
travel_documents: values.travel_documents || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -236,15 +239,29 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
travel_document_path: item.travel_document_path || '',
|
travel_document_path: item.travel_document_path || '',
|
||||||
vehicle_number: item.vehicle_number || '',
|
vehicle_number: item.vehicle_number || '',
|
||||||
expedition_vendor: null,
|
expedition_vendor: null,
|
||||||
expedition_vendor_id: 0,
|
expedition_vendor_id: item.expedition_vendor_id || 0,
|
||||||
received_qty: item.total_qty || '',
|
received_qty: item.total_qty || '',
|
||||||
transport_per_item: '',
|
transport_per_item: item.transport_per_item || '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
formik.setFieldValue('items', updatedItems);
|
formik.setFieldValue('items', updatedItems);
|
||||||
}
|
}
|
||||||
}, [purchaseItems, initialValues]);
|
}, [purchaseItems, initialValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
formik.values.travel_documents &&
|
||||||
|
formik.values.travel_documents.length > 0
|
||||||
|
) {
|
||||||
|
const fileNames = formik.values.travel_documents
|
||||||
|
.map((file) => file.name)
|
||||||
|
.join(', ');
|
||||||
|
formik.values.items?.forEach((item, idx) => {
|
||||||
|
formik.setFieldValue(`items.${idx}.travel_document_path`, fileNames);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [formik.values.travel_documents]);
|
||||||
|
|
||||||
// ===== HELPER FUNCTIONS =====
|
// ===== HELPER FUNCTIONS =====
|
||||||
const getQuantityExceededError = useCallback(
|
const getQuantityExceededError = useCallback(
|
||||||
(idx: number, receivedQty: number) => {
|
(idx: number, receivedQty: number) => {
|
||||||
@@ -343,7 +360,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
No. Surat Jalan
|
No. Surat Jalan
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th className='hidden'>
|
||||||
Dokumen Surat Jalan
|
Dokumen Surat Jalan
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -478,7 +495,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className='hidden'>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
name={`items.${idx}.travel_document_path`}
|
name={`items.${idx}.travel_document_path`}
|
||||||
@@ -636,7 +653,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className={'col-span-2'}>
|
<div className={'col-span-2 my-2'}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label='Notes'
|
label='Notes'
|
||||||
name='notes'
|
name='notes'
|
||||||
@@ -649,6 +666,31 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={'col-span-2 my-2'}>
|
||||||
|
<FileInput
|
||||||
|
required
|
||||||
|
name='travel_documents'
|
||||||
|
label='Dokumen Surat Jalan'
|
||||||
|
accept='.pdf,.jpg,.jpeg,.png'
|
||||||
|
onChange={(e) => {
|
||||||
|
const files = Array.from(e.target.files || []);
|
||||||
|
formik.setFieldValue('travel_documents', files);
|
||||||
|
}}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
bottomLabel={
|
||||||
|
formik.values.travel_documents &&
|
||||||
|
formik.values.travel_documents.length > 0
|
||||||
|
? `${formik.values.travel_documents.length} file(s) dipilih`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
isError={
|
||||||
|
formik.touched.travel_documents &&
|
||||||
|
Boolean(formik.errors.travel_documents)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.travel_documents as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap mt-5'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap mt-5'>
|
||||||
<div className='flex flex-row justify-end gap-2 w-full'>
|
<div className='flex flex-row justify-end gap-2 w-full'>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
|||||||
received_qty: number | string;
|
received_qty: number | string;
|
||||||
transport_per_item: number | string;
|
transport_per_item: number | string;
|
||||||
}[];
|
}[];
|
||||||
|
travel_documents: File[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseStaffApprovalItemSchema = {
|
export type PurchaseStaffApprovalItemSchema = {
|
||||||
@@ -379,6 +380,11 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
|
|||||||
.min(1, 'Minimal harus ada 1 item pembelian!')
|
.min(1, 'Minimal harus ada 1 item pembelian!')
|
||||||
.required('Item pembelian wajib diisi!')
|
.required('Item pembelian wajib diisi!')
|
||||||
.typeError('Item pembelian wajib diisi!'),
|
.typeError('Item pembelian wajib diisi!'),
|
||||||
|
travel_documents: Yup.array()
|
||||||
|
.of(Yup.mixed<File>().required())
|
||||||
|
.required('Dokumen surat jalan wajib diupload!')
|
||||||
|
.min(1, 'Minimal upload 1 dokumen surat jalan!')
|
||||||
|
.typeError('Dokumen surat jalan wajib diupload!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
||||||
@@ -397,6 +403,7 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce
|
|||||||
transport_per_item: '',
|
transport_per_item: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
travel_documents: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
||||||
@@ -428,6 +435,7 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
|||||||
transport_per_item: '',
|
transport_per_item: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
travel_documents: [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ const PurchaseOrderDetail = ({
|
|||||||
{
|
{
|
||||||
header: 'Ekspedisi',
|
header: 'Ekspedisi',
|
||||||
accessorKey: 'expedition_name',
|
accessorKey: 'expedition_name',
|
||||||
cell: (props) => '-',
|
cell: (props) => props.row.original.expedition_vendor.name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Transport /Item',
|
header: 'Transport /Item',
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
text: 'Transfer to Laying',
|
text: 'Transfer to Laying',
|
||||||
link: '/production/transfer-to-laying',
|
link: '/production/transfer-to-laying',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Uniformity',
|
||||||
|
link: '/production/uniformity',
|
||||||
|
permission: ['lti.production.uniformity.list'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'lti.production.transfer_to_laying.update',
|
'lti.production.transfer_to_laying.update',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Production - Uniformity
|
||||||
|
'/production/uniformity/': ['lti.production.uniformity.list'],
|
||||||
|
'/production/uniformity/add/': ['lti.production.uniformity.create'],
|
||||||
|
'/production/uniformity/detail/': ['lti.production.uniformity.detail'],
|
||||||
|
'/production/uniformity/detail/edit/': ['lti.production.uniformity.update'],
|
||||||
|
|
||||||
// Purchase
|
// Purchase
|
||||||
'/purchase/': ['lti.purchase.list'],
|
'/purchase/': ['lti.purchase.list'],
|
||||||
'/purchase/add/': ['lti.purchase.create'],
|
'/purchase/add/': ['lti.purchase.create'],
|
||||||
|
|||||||
@@ -72,11 +72,29 @@ export const PurchaseApi = {
|
|||||||
purchaseRequestId: number,
|
purchaseRequestId: number,
|
||||||
payload: CreateAcceptApprovalRequestPayload
|
payload: CreateAcceptApprovalRequestPayload
|
||||||
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('action', payload.action);
|
||||||
|
|
||||||
|
if (payload.notes) {
|
||||||
|
formData.append('notes', payload.notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.items) {
|
||||||
|
formData.append('items', JSON.stringify(payload.items));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.travel_documents) {
|
||||||
|
payload.travel_documents.forEach((file) => {
|
||||||
|
formData.append('travel_documents', file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await basePurchaseApi.customRequest<
|
return await basePurchaseApi.customRequest<
|
||||||
BaseApiResponse<{ message: string }>
|
BaseApiResponse<{ message: string }>
|
||||||
>(`${purchaseRequestId}/receipts`, {
|
>(`${purchaseRequestId}/receipts`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
payload,
|
payload: formData as unknown as Record<string, unknown>,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { BaseApiService } from '@/services/api/base';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import {
|
||||||
|
Uniformity,
|
||||||
|
UniformityDetail,
|
||||||
|
VerifyUniformityPayload,
|
||||||
|
VerifyUniformityResponse,
|
||||||
|
CreateUniformityPayload,
|
||||||
|
} from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
|
export class UniformityApiService extends BaseApiService<
|
||||||
|
Uniformity,
|
||||||
|
CreateUniformityPayload,
|
||||||
|
VerifyUniformityPayload
|
||||||
|
> {
|
||||||
|
constructor(basePath: string) {
|
||||||
|
super(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUniformity(
|
||||||
|
project_flock_kandang_id?: number,
|
||||||
|
start_date?: string,
|
||||||
|
end_date?: string,
|
||||||
|
page?: number,
|
||||||
|
limit?: number
|
||||||
|
): Promise<BaseApiResponse<Uniformity> | undefined> {
|
||||||
|
return await this.customRequest<BaseApiResponse<Uniformity>>('', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
project_flock_kandang_id: project_flock_kandang_id,
|
||||||
|
start_date: start_date,
|
||||||
|
end_date: end_date,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUniformityDetail(
|
||||||
|
id: number,
|
||||||
|
with_details = false
|
||||||
|
): Promise<BaseApiResponse<UniformityDetail> | undefined> {
|
||||||
|
return await this.customRequest<BaseApiResponse<UniformityDetail>>(
|
||||||
|
`/${id}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
with_details: with_details,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUniformity(
|
||||||
|
payload: CreateUniformityPayload
|
||||||
|
): Promise<BaseApiResponse<UniformityDetail> | undefined> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('date', payload.date);
|
||||||
|
formData.append('week', payload.week.toString());
|
||||||
|
formData.append(
|
||||||
|
'project_flock_kandang_id',
|
||||||
|
payload.project_flock_kandang_id.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (payload.document) {
|
||||||
|
formData.append('document', payload.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.customRequest<BaseApiResponse<UniformityDetail>>('', {
|
||||||
|
method: 'POST',
|
||||||
|
payload: formData as unknown as Record<string, unknown>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyUniformity(
|
||||||
|
payload: VerifyUniformityPayload
|
||||||
|
): Promise<BaseApiResponse<VerifyUniformityResponse> | undefined> {
|
||||||
|
const formData = new FormData();
|
||||||
|
if (payload.document) {
|
||||||
|
formData.append('document', payload.document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.customRequest<BaseApiResponse<VerifyUniformityResponse>>(
|
||||||
|
'/verify',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
payload: formData as unknown as Record<string, unknown>,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async approve(
|
||||||
|
idOrIds: number | number[],
|
||||||
|
notes?: string
|
||||||
|
): Promise<BaseApiResponse<Uniformity[]> | undefined> {
|
||||||
|
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
||||||
|
return await this.customRequest<BaseApiResponse<Uniformity[]>>(
|
||||||
|
'approvals',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
action: 'APPROVED',
|
||||||
|
approvable_ids,
|
||||||
|
notes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reject(
|
||||||
|
idOrIds: number | number[],
|
||||||
|
notes: string = ''
|
||||||
|
): Promise<BaseApiResponse<Uniformity[]> | undefined> {
|
||||||
|
const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
||||||
|
return await this.customRequest<BaseApiResponse<Uniformity[]>>(
|
||||||
|
'approvals',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
action: 'REJECTED',
|
||||||
|
approvable_ids,
|
||||||
|
notes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: number): Promise<BaseApiResponse<Uniformity> | undefined> {
|
||||||
|
return await this.customRequest<BaseApiResponse<Uniformity>>(`/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UniformityApi = new UniformityApiService(
|
||||||
|
'production/uniformities'
|
||||||
|
// 'http://localhost:4010/api/production/uniformities'
|
||||||
|
);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DrawerUISlice } from '@/types/stores';
|
import { DrawerUISlice } from '@/types/stores';
|
||||||
import { StateCreator } from 'zustand';
|
import { StateCreator } from 'zustand';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export const createDrawerUISlice: StateCreator<
|
export const createDrawerUISlice: StateCreator<
|
||||||
DrawerUISlice,
|
DrawerUISlice,
|
||||||
@@ -37,4 +38,14 @@ export const createDrawerUISlice: StateCreator<
|
|||||||
callback(Boolean(state.isValid));
|
callback(Boolean(state.isValid));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
expandedDrawerOpen: false,
|
||||||
|
setExpandedDrawerOpen: (open: boolean) => set({ expandedDrawerOpen: open }),
|
||||||
|
|
||||||
|
expandedDrawerContent: null as ReactNode | null,
|
||||||
|
setExpandedDrawerContent: (content: ReactNode) =>
|
||||||
|
set({ expandedDrawerContent: content }),
|
||||||
|
|
||||||
|
isNextStep: false,
|
||||||
|
setIsNextStep: (isNextStep: boolean) => set({ isNextStep }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { UniformitySlice } from '@/types/stores';
|
||||||
|
import { StateCreator } from 'zustand';
|
||||||
|
|
||||||
|
export const createUniformitySlice: StateCreator<
|
||||||
|
UniformitySlice,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
UniformitySlice
|
||||||
|
> = (set) => ({
|
||||||
|
// Initial state
|
||||||
|
uniformityStep: 'preview',
|
||||||
|
verifyUniformityResult: null,
|
||||||
|
uniformityFormData: null,
|
||||||
|
isSuccess: false,
|
||||||
|
createdUniformity: null,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setUniformityStep: (step) => set({ uniformityStep: step }),
|
||||||
|
|
||||||
|
setVerifyUniformityResult: (result) =>
|
||||||
|
set({ verifyUniformityResult: result }),
|
||||||
|
|
||||||
|
setUniformityFormData: (data) => set({ uniformityFormData: data }),
|
||||||
|
|
||||||
|
setIsSuccess: (success) => set({ isSuccess: success }),
|
||||||
|
|
||||||
|
setCreatedUniformity: (data) => set({ createdUniformity: data }),
|
||||||
|
|
||||||
|
resetUniformity: () =>
|
||||||
|
set({
|
||||||
|
uniformityStep: 'preview',
|
||||||
|
verifyUniformityResult: null,
|
||||||
|
uniformityFormData: null,
|
||||||
|
isSuccess: false,
|
||||||
|
createdUniformity: null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
import { createUniformitySlice } from '@/stores/uniformity/slices/uniformity.slice';
|
||||||
|
import { UniformitySlice } from '@/types/stores';
|
||||||
|
|
||||||
|
export type UniformityStore = UniformitySlice;
|
||||||
|
|
||||||
|
export const useUniformityStore = create<UniformityStore>()(
|
||||||
|
devtools(
|
||||||
|
(...args) => ({
|
||||||
|
...createUniformitySlice(...args),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'UniformityStore',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
+2
@@ -71,6 +71,8 @@ export type ProjectFlockKandangLookup = {
|
|||||||
kandang: Kandang;
|
kandang: Kandang;
|
||||||
project_flock: ProjectFlock;
|
project_flock: ProjectFlock;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
available_quantity?: number;
|
||||||
|
population: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectFlockAvailableQuantity = {
|
export type ProjectFlockAvailableQuantity = {
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
|
import { Location } from '@/types/api/location/location';
|
||||||
|
import { ProjectFlock } from '@/types/api/project-flock/project-flock';
|
||||||
|
import { Kandang } from '@/types/api/kandang/kandang';
|
||||||
|
import { BaseApproval } from '@/types/api/approval/approval';
|
||||||
|
|
||||||
|
// ==================== GET ALL RESPONSE ====================
|
||||||
|
export type Uniformity = BaseMetadata & {
|
||||||
|
id: number;
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
location_name: string;
|
||||||
|
flock_name: string;
|
||||||
|
kandang_name: string;
|
||||||
|
applied_at: string;
|
||||||
|
week: number;
|
||||||
|
status: string;
|
||||||
|
uniformity: number;
|
||||||
|
cv: number;
|
||||||
|
chick_qty_of_weight: number;
|
||||||
|
uniform_qty: number;
|
||||||
|
mean_up: number;
|
||||||
|
mean_down: number;
|
||||||
|
standard_mean_weight: number | null;
|
||||||
|
standard_uniformity: number | null;
|
||||||
|
created_at: string;
|
||||||
|
created_by: number;
|
||||||
|
latest_approval?: BaseApproval;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== GET ONE RESPONSE ====================
|
||||||
|
export type UniformityInfoUmum = {
|
||||||
|
tanggal: string;
|
||||||
|
lokasi_farm: string;
|
||||||
|
project_flock: string;
|
||||||
|
kandang: string;
|
||||||
|
file_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UniformitySampling = {
|
||||||
|
chick_qty_of_weight: number;
|
||||||
|
mean_weight: number;
|
||||||
|
mean_down: number;
|
||||||
|
mean_up: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UniformityResult = {
|
||||||
|
uniform_qty: number;
|
||||||
|
outside_qty: number;
|
||||||
|
uniformity: number;
|
||||||
|
cv: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UniformityDetailItem = {
|
||||||
|
id: number;
|
||||||
|
weight: number;
|
||||||
|
range: 'Ideal' | 'Outside';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UniformityStandard = {
|
||||||
|
mean_weight: number;
|
||||||
|
uniformity: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export type UniformityDetail = BaseMetadata & {
|
||||||
|
id: number;
|
||||||
|
info_umum: UniformityInfoUmum;
|
||||||
|
sampling: UniformitySampling;
|
||||||
|
result: UniformityResult;
|
||||||
|
standard: UniformityStandard;
|
||||||
|
uniformity_details?: UniformityDetailItem[];
|
||||||
|
latest_approval?: BaseApproval;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== VERIFY RESPONSE ====================
|
||||||
|
export type VerifyUniformityResponse = {
|
||||||
|
sampling: UniformitySampling;
|
||||||
|
result: UniformityResult;
|
||||||
|
uniformity_details: UniformityDetailItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== PAYLOADS ====================
|
||||||
|
export type CreateUniformityPayload = {
|
||||||
|
date: string;
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
document: File;
|
||||||
|
week: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VerifyUniformityPayload = {
|
||||||
|
document: File;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== OTHER TYPES ====================
|
||||||
|
export type BodyWeightData = {
|
||||||
|
id: string;
|
||||||
|
number: number;
|
||||||
|
weight: number;
|
||||||
|
status?: 'ideal' | 'outside';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DetailOptionType = OptionType & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
Vendored
+7
@@ -42,6 +42,12 @@ export type PurchaseItem = {
|
|||||||
expedition_vendor_name?: string | null;
|
expedition_vendor_name?: string | null;
|
||||||
received_qty?: number | null;
|
received_qty?: number | null;
|
||||||
transport_per_item?: number | null;
|
transport_per_item?: number | null;
|
||||||
|
expedition_vendor: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
alias: string;
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BasePurchase = {
|
export type BasePurchase = {
|
||||||
@@ -118,6 +124,7 @@ export type CreateAcceptApprovalRequestPayload = {
|
|||||||
received_qty: number;
|
received_qty: number;
|
||||||
transport_per_item: number;
|
transport_per_item: number;
|
||||||
}[];
|
}[];
|
||||||
|
travel_documents?: File[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeletePurchaseRequestItemPayload = {
|
export type DeletePurchaseRequestItemPayload = {
|
||||||
|
|||||||
Vendored
+31
-1
@@ -1,4 +1,9 @@
|
|||||||
import type { ProductionStandardRepeaterFormSchemaValues } from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema';
|
import type { ProductionStandardRepeaterFormSchemaValues } from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema';
|
||||||
|
import type {
|
||||||
|
UniformityFormData,
|
||||||
|
UniformityDetail,
|
||||||
|
VerifyUniformityResponse,
|
||||||
|
} from '@/types/api/production/uniformity';
|
||||||
|
|
||||||
type MainUiSlice = {
|
type MainUiSlice = {
|
||||||
mainDrawerOpen: boolean;
|
mainDrawerOpen: boolean;
|
||||||
@@ -8,10 +13,16 @@ type MainUiSlice = {
|
|||||||
type DrawerUISlice = {
|
type DrawerUISlice = {
|
||||||
triggerValidate: boolean;
|
triggerValidate: boolean;
|
||||||
toggleValidate: () => void;
|
toggleValidate: () => void;
|
||||||
subscribeValidate: (callback: () => void) => void;
|
subscribeValidate: (callback: () => void) => () => void;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
setIsValid: (v: boolean) => void;
|
setIsValid: (v: boolean) => void;
|
||||||
subscribeIsValid: (callback: (isValid: boolean) => void) => () => void;
|
subscribeIsValid: (callback: (isValid: boolean) => void) => () => void;
|
||||||
|
expandedDrawerOpen: boolean;
|
||||||
|
setExpandedDrawerOpen: (open: boolean) => void;
|
||||||
|
expandedDrawerContent: ReactNode | null;
|
||||||
|
setExpandedDrawerContent: (content: ReactNode) => void;
|
||||||
|
isNextStep: boolean;
|
||||||
|
setIsNextStep: (v: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIStore = MainUiSlice & DrawerUISlice;
|
export type UIStore = MainUiSlice & DrawerUISlice;
|
||||||
@@ -40,3 +51,22 @@ type ProductionStandardFormSlice = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type FormStore = ProductionStandardFormSlice;
|
export type FormStore = ProductionStandardFormSlice;
|
||||||
|
|
||||||
|
export type UniformityStep = 'preview' | 'result';
|
||||||
|
|
||||||
|
export type UniformitySlice = {
|
||||||
|
// State
|
||||||
|
uniformityStep: UniformityStep;
|
||||||
|
verifyUniformityResult: VerifyUniformityResponse | null;
|
||||||
|
uniformityFormData: UniformityFormData | null;
|
||||||
|
isSuccess: boolean;
|
||||||
|
createdUniformity: UniformityDetail | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setUniformityStep: (step: UniformityStep) => void;
|
||||||
|
setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void;
|
||||||
|
setUniformityFormData: (data: UniformityFormData | null) => void;
|
||||||
|
setIsSuccess: (success: boolean) => void;
|
||||||
|
setCreatedUniformity: (data: UniformityDetail | null) => void;
|
||||||
|
resetUniformity: () => void;
|
||||||
|
};
|
||||||
|
|||||||
Vendored
+2
@@ -9,4 +9,6 @@ export type Color =
|
|||||||
| 'error'
|
| 'error'
|
||||||
| 'none';
|
| 'none';
|
||||||
|
|
||||||
|
export type Variant = 'default' | 'outline' | 'ghost' | 'soft' | 'dash';
|
||||||
|
|
||||||
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|||||||
Reference in New Issue
Block a user