refactor(FE-438): Add uniformity details preview drawer

This commit is contained in:
rstubryan
2025-12-29 12:08:30 +07:00
parent 70d9b4d8ed
commit 9f2fcbf154
2 changed files with 342 additions and 2 deletions
@@ -1,6 +1,6 @@
'use client';
import React, { useMemo } from 'react';
import { useMemo, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
@@ -8,10 +8,13 @@ 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 { type OptionType } from '@/components/input/SelectInput';
import RequirePermission from '@/components/helper/RequirePermission';
import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity';
import { formatDate } from '@/lib/helper';
import { useUiStore } from '@/stores/ui/ui.store';
import UniformityDetailsPreview from './UniformityDetailsPreview';
const statusColorMap: Record<string, string> = {
APPROVED: 'bg-[#00D39033]',
@@ -43,7 +46,7 @@ const getStatusText = (status: string): string => {
return statusTextMap[status] || status;
};
type DetailOptionType = OptionType & {
export type DetailOptionType = OptionType & {
id: string;
};
@@ -55,6 +58,10 @@ 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(`/uniformity?action=approve&id=${initialValues.id}`);
@@ -64,6 +71,28 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
router.push(`/uniformity?action=reject&id=${initialValues.id}`);
};
const handleViewUniformityDetails = () => {
setExpandedDrawerContent(
<UniformityDetailsPreview
info_umum={initialValues.info_umum}
uniformityDetails={initialValues.uniformity_details}
sampling={initialValues.sampling}
result={initialValues.result}
/>
);
setTimeout(() => {
setExpandedDrawerOpen(true);
}, 0);
};
useEffect(() => {
return () => {
setExpandedDrawerOpen(false);
setExpandedDrawerContent(null);
};
}, []);
const infoUmumTableData: DetailOptionType[] = useMemo(() => {
if (!initialValues) return [];
@@ -147,6 +176,22 @@ const UniformityDetail: React.FC<UniformityDetailProps> = ({
return <span>-</span>;
}
if (id === 'file-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'
onClick={handleViewUniformityDetails}
>
<Icon icon='mdi:eye-outline' width={18} height={18} />
</button>
</Tooltip>
</div>
);
}
return <span>{valueMap[id] || '-'}</span>;
},
},
@@ -0,0 +1,295 @@
'use client';
import React, { useMemo } 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/uniformity/uniformity';
import Table from '@/components/Table';
import Badge from '@/components/Badge';
import { formatNumber } from '@/lib/helper';
import { DetailOptionType } from '@/components/pages/uniformity/detail/UniformityDetail';
const weightStatusColorMap: Record<string, string> = {
ideal: 'bg-[#00D39033]',
outside: 'bg-error/10',
};
const weightStatusIndicatorColorMap: Record<string, string> = {
ideal: 'bg-[#008000]',
outside: 'bg-error',
};
const weightStatusTextMap: Record<string, string> = {
ideal: 'Ideal',
outside: 'Outside',
};
const getWeightStatusColor = (status: string): string => {
return weightStatusColorMap[status] || 'bg-info';
};
const getWeightStatusIndicatorColor = (status: string): string => {
return weightStatusIndicatorColorMap[status] || 'bg-info';
};
const getWeightStatusText = (status: string): string => {
return weightStatusTextMap[status] || status;
};
type BodyWeightData = {
id: string;
number: number;
weight: number;
status?: 'ideal' | 'outside';
};
interface UniformityDetailsPreviewProps {
info_umum: UniformityInfoUmum;
uniformityDetails: UniformityDetailItem[];
sampling: UniformitySampling;
result: UniformityResult;
}
const UniformityDetailsPreview = ({
info_umum,
uniformityDetails,
sampling,
result,
}: UniformityDetailsPreviewProps) => {
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
const handleClose = () => {
setExpandedDrawerOpen(false);
};
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} %`,
},
{
id: 'cv',
label: 'CV',
value: `${result.cv} %`,
},
];
}, [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 (!uniformityDetails) return [];
return uniformityDetails.map((detail, index) => ({
id: `body-weight-${index + 1}`,
number: index + 1,
weight: detail.weight,
status: detail.range.toLowerCase() as 'ideal' | 'outside',
}));
}, [uniformityDetails]);
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'
showDivider={false}
>
<button
className='p-0 text-error hover:bg-transparent'
onClick={handleClose}
>
<Icon icon='mdi:close' width={20} height={20} />
</button>
</DrawerHeader>
{/* Form Section */}
<div className='divider mt-3.5'></div>
<section className='w-full px-6'>
{uniformityDetails && uniformityDetails.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 */}
<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;