mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
feat(FE-438): Add Uniformity detail view and navigation
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import UniformityForm from '@/components/pages/uniformity/form/UniformityForm';
|
import UniformityForm from '@/components/pages/uniformity/form/UniformityForm';
|
||||||
|
|
||||||
const AddUniformity = () => {
|
const AddUniformity = () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import UniformityTable from '@/components/pages/uniformity/UniformityTable';
|
||||||
|
|
||||||
const Uniformity = () => {
|
const Uniformity = () => {
|
||||||
return <></>;
|
return <UniformityTable />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Uniformity;
|
export default Uniformity;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
@@ -59,6 +60,7 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
||||||
|
const router = useRouter();
|
||||||
const isSuccess = useUniformityStore((s) => s.isSuccess);
|
const isSuccess = useUniformityStore((s) => s.isSuccess);
|
||||||
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
|
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
|
||||||
|
|
||||||
@@ -200,6 +202,14 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleViewDetail = useCallback(
|
||||||
|
(uniformity: Uniformity) => {
|
||||||
|
router.push(`/uniformity/detail?uniformityId=${uniformity.id}`);
|
||||||
|
setRowSelection({});
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
const handleBulkApprove = useCallback(() => {
|
const handleBulkApprove = useCallback(() => {
|
||||||
bulkApproveModal.openModal();
|
bulkApproveModal.openModal();
|
||||||
}, [bulkApproveModal]);
|
}, [bulkApproveModal]);
|
||||||
@@ -980,6 +990,19 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
{/* Floating Actions Button */}
|
{/* Floating Actions Button */}
|
||||||
<FloatingActionsButton
|
<FloatingActionsButton
|
||||||
actions={[
|
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',
|
action: 'DELETE',
|
||||||
icon: 'mdi:delete-outline',
|
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;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { BaseApiService } from '@/services/api/base';
|
import { BaseApiService } from '@/services/api/base';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
|
Uniformity,
|
||||||
|
UniformityDetail,
|
||||||
VerifyUniformityPayload,
|
VerifyUniformityPayload,
|
||||||
VerifyUniformityResponse,
|
VerifyUniformityResponse,
|
||||||
Uniformity,
|
|
||||||
CreateUniformityPayload,
|
CreateUniformityPayload,
|
||||||
} from '@/types/api/uniformity/uniformity';
|
} from '@/types/api/uniformity/uniformity';
|
||||||
|
|
||||||
@@ -20,6 +21,14 @@ export class UniformityApiService extends BaseApiService<
|
|||||||
return await this.customRequest<BaseApiResponse<Uniformity>>('');
|
return await this.customRequest<BaseApiResponse<Uniformity>>('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUniformityDetail(
|
||||||
|
id: number
|
||||||
|
): Promise<BaseApiResponse<UniformityDetail> | undefined> {
|
||||||
|
return await this.customRequest<BaseApiResponse<UniformityDetail>>(
|
||||||
|
`/${id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async createUniformity(
|
async createUniformity(
|
||||||
payload: CreateUniformityPayload
|
payload: CreateUniformityPayload
|
||||||
): Promise<BaseApiResponse<Uniformity> | undefined> {
|
): Promise<BaseApiResponse<Uniformity> | undefined> {
|
||||||
|
|||||||
Reference in New Issue
Block a user