feat(FE-438): Add Uniformity detail view and navigation

This commit is contained in:
rstubryan
2025-12-28 20:58:59 +07:00
parent 9f0dc8c644
commit 8ec76af012
6 changed files with 366 additions and 4 deletions
-2
View File
@@ -1,5 +1,3 @@
'use client';
import UniformityForm from '@/components/pages/uniformity/form/UniformityForm';
const AddUniformity = () => {
+47
View File
@@ -0,0 +1,47 @@
'use client';
import UniformityDetail from '@/components/pages/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 && (
<span className='loading loading-spinner loading-xl' />
)}
{isResponseSuccess(uniformity) && (
<UniformityDetail initialValues={uniformity.data} />
)}
</div>
);
};
export default UniformityDetailPage;
+3 -1
View File
@@ -1,5 +1,7 @@
import UniformityTable from '@/components/pages/uniformity/UniformityTable';
const Uniformity = () => {
return <></>;
return <UniformityTable />;
};
export default Uniformity;
@@ -1,6 +1,7 @@
'use client';
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import useSWR from 'swr';
import { Icon } from '@iconify/react';
import { ColumnDef, SortingState } from '@tanstack/react-table';
@@ -59,6 +60,7 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => {
};
const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
const router = useRouter();
const isSuccess = useUniformityStore((s) => s.isSuccess);
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
@@ -200,6 +202,14 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
setRowSelection({});
}, []);
const handleViewDetail = useCallback(
(uniformity: Uniformity) => {
router.push(`/uniformity/detail?uniformityId=${uniformity.id}`);
setRowSelection({});
},
[router]
);
const handleBulkApprove = useCallback(() => {
bulkApproveModal.openModal();
}, [bulkApproveModal]);
@@ -980,6 +990,19 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
{/* Floating Actions Button */}
<FloatingActionsButton
actions={[
{
action: 'DETAIL',
icon: 'mdi:eye-outline',
label: 'Lihat Detail',
hidden: selectedRowIds.length !== 1,
onClick() {
router.push(
`/uniformity/detail?uniformityId=${selectedRowIds[0]}`
);
setRowSelection({});
},
permissions: 'lti.production.uniformity.detail',
},
{
action: 'DELETE',
icon: 'mdi:delete-outline',
@@ -0,0 +1,283 @@
'use client';
import React, { useMemo } from 'react';
import { useRouter } from 'next/navigation';
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 Table from '@/components/Table';
import { formatNumber } from '@/lib/helper';
import { type OptionType } from '@/components/input/SelectInput';
import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity';
type DetailOptionType = OptionType & {
id: string;
};
interface UniformityDetailProps {
initialValues: UniformityDetailType;
}
const UniformityDetail: React.FC<UniformityDetailProps> = ({
initialValues,
}) => {
const router = useRouter();
const handleClose = () => {
router.back();
};
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: 'file-name',
value: 'file-name',
label: 'File Name',
},
];
}, [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 } = initialValues!;
const valueMap: Record<string, string> = {
tanggal: info_umum.tanggal,
'lokasi-farm': info_umum.lokasi_farm,
'project-flock': info_umum.project_flock,
kandang: info_umum.kandang,
'file-name': info_umum.file_name,
};
return <span>{valueMap[id] || '-'}</span>;
},
},
],
[initialValues]
);
const samplingTableData: DetailOptionType[] = useMemo(() => {
if (!initialValues) return [];
return [
{
id: 'sampling-size',
value: 'sampling-size',
label: 'Sampling size',
},
{
id: 'mean-weight',
value: 'mean-weight',
label: 'Mean Weight',
},
{
id: 'min-limit',
value: 'min-limit',
label: 'Min Limit (-10%)',
},
{
id: 'max-limit',
value: 'max-limit',
label: 'Max Limit (+10%)',
},
];
}, [initialValues]);
const columnsSampling: 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 { sampling } = initialValues!;
const valueMap: Record<string, string> = {
'sampling-size': `${formatNumber(sampling.chick_qty_of_weight)} of Birds`,
'mean-weight': `${formatNumber(sampling.mean_weight)} g`,
'min-limit': `${formatNumber(sampling.mean_down)} g`,
'max-limit': `${formatNumber(sampling.mean_up)} g`,
};
return <span>{valueMap[id] || '-'}</span>;
},
},
],
[initialValues]
);
const resultTableData: DetailOptionType[] = useMemo(() => {
if (!initialValues) return [];
return [
{
id: 'ideal-birds',
value: 'ideal-birds',
label: 'Ideal Birds',
},
{
id: 'outside-range',
value: 'outside-range',
label: 'Outside Range',
},
{
id: 'uniformity',
value: 'uniformity',
label: 'Uniformity',
},
{
id: 'cv',
value: 'cv',
label: 'CV',
},
];
}, [initialValues]);
const columnsResult: 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 { result } = initialValues!;
const valueMap: Record<string, string> = {
'ideal-birds': `${formatNumber(result.uniform_qty)} of Birds`,
'outside-range': `${formatNumber(result.outside_qty)} of Birds`,
uniformity: `${result.uniformity} %`,
cv: `${result.cv}`,
};
return <span>{valueMap[id] || '-'}</span>;
},
},
],
[initialValues]
);
return (
<section className='w-full h-full bg-white border-l border-gray-200'>
{/* Header */}
<DrawerHeader
leftIcon=''
subtitle={`${initialValues?.info_umum.file_name || ''}`}
subtitleClassName='text-sm text-neutral'
showDivider={false}
>
<Button variant='link' className='p-0 text-error' onClick={handleClose}>
<Tooltip content='Close' position='left'>
<Icon icon='mdi:close' width={20} height={20} />
</Tooltip>
</Button>
</DrawerHeader>
{/* 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={5}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</div>
{/* Sampling */}
<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={columnsResult}
pageSize={4}
className={{
containerClassName: 'mb-0',
paginationClassName: 'hidden',
}}
/>
</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;
+10 -1
View File
@@ -1,9 +1,10 @@
import { BaseApiService } from '@/services/api/base';
import { BaseApiResponse } from '@/types/api/api-general';
import {
Uniformity,
UniformityDetail,
VerifyUniformityPayload,
VerifyUniformityResponse,
Uniformity,
CreateUniformityPayload,
} from '@/types/api/uniformity/uniformity';
@@ -20,6 +21,14 @@ export class UniformityApiService extends BaseApiService<
return await this.customRequest<BaseApiResponse<Uniformity>>('');
}
async getUniformityDetail(
id: number
): Promise<BaseApiResponse<UniformityDetail> | undefined> {
return await this.customRequest<BaseApiResponse<UniformityDetail>>(
`/${id}`
);
}
async createUniformity(
payload: CreateUniformityPayload
): Promise<BaseApiResponse<Uniformity> | undefined> {