refactor(FE): Refactor production result components and improve UI

This commit is contained in:
rstubryan
2026-02-13 09:24:42 +07:00
parent 211622c7b0
commit 5c00893ea3
6 changed files with 682 additions and 473 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
import ProductionResultContent from '@/components/pages/report/production-result/tab/ProductionResultTab';
import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs';
const ProductionResultReportPage = () => {
return (
<section className='w-full max-w-full'>
<ProductionResultContent />
<ProductionResultTabs />
</section>
);
};
@@ -4,12 +4,10 @@ import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import { cn, formatNumber } from '@/lib/helper';
import { formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { ProductionResult } from '@/types/api/report/production-result';
import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -52,8 +50,6 @@ const ProductionResultProjectFlockKandangTable = ({
}
);
const [open, setOpen] = useState(false);
const [sorting, setSorting] = useState<SortingState>([]);
const productionResultColumns: ColumnDef<ProductionResult>[] = [
@@ -270,93 +266,60 @@ const ProductionResultProjectFlockKandangTable = ({
}
}, [sorting]);
useEffect(() => {
if (!open) {
setOpen(
return (
<Card
title={kandangName}
className={{
wrapper: 'w-full rounded-lg border-none',
body: 'p-0',
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
collapsible: 'rounded-lg',
}}
variant='bordered'
collapsible={true}
defaultCollapsed={
isResponseSuccess(productionResults)
? productionResults.data.length > 0
: false
);
}
}, [productionResults, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>{kandangName}</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
<Table<ProductionResult>
data={
isResponseSuccess(productionResults) ? productionResults?.data : []
}
className='w-full!'
titleClassName='w-full p-0!'
>
<div className='w-full p-0'>
{/* <div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Record'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
</div> */}
<Table<ProductionResult>
data={
isResponseSuccess(productionResults)
? productionResults?.data
: []
}
columns={productionResultColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(productionResults)
? productionResults?.meta?.page
: 0
}
totalItems={
isResponseSuccess(productionResults)
? productionResults?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingProductionResults}
sorting={sorting}
setSorting={setSorting}
renderFooter={false}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(productionResults) &&
productionResults?.data?.length === 0,
}),
headerColumnClassName:
'px-4 py-3 border-x border-base-content/10 text-base-content/50',
}}
/>
</div>
</Collapse>
columns={productionResultColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(productionResults)
? productionResults?.meta?.page
: 0
}
totalItems={
isResponseSuccess(productionResults)
? productionResults?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingProductionResults}
sorting={sorting}
setSorting={setSorting}
renderFooter={false}
className={{
containerClassName: 'w-full mb-0!',
tableWrapperClassName:
'overflow-x-auto rounded-tr-none rounded-tl-none',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</Card>
);
};
@@ -0,0 +1,39 @@
'use client';
import { useState } from 'react';
import Tabs from '@/components/Tabs';
import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultTab';
import { useReportTabStore } from '@/stores/report/report-tab.store';
const ProductionResultTabs = () => {
const [activeTabId, setActiveTabId] = useState<string>('1');
const tabActions = useReportTabStore((state) => state.tabActions);
const tabs = [
{
id: '1',
label: 'Hasil Produksi',
content: <ProductionResultTab tabId={'1'} />,
},
];
return (
<section className='w-full'>
<Tabs
tabs={tabs}
variant='boxed'
activeTabId={activeTabId}
onTabChange={setActiveTabId}
className={{
tabHeaderWrapper:
'justify-between items-center p-3 border-b border-base-content/10',
tab: 'w-fit',
content: 'p-0 m-0',
}}
sideContent={tabActions[activeTabId] || null}
/>
</section>
);
};
export default ProductionResultTabs;
@@ -1,48 +1,17 @@
import * as yup from 'yup';
export type ProductionResultFilterType = {
export type ProductionResultFilterProps = {
area_id: string | null;
location_id: string | null;
project_flock_id: string | null;
kandang_id: string | null;
date_start: string | null;
date_end: string | null;
sort_by: string | null;
show_unrecorded: boolean | null;
};
export const ProductionResultFilterSchema = yup.object({
area_id: yup.string().nullable(),
location_id: yup.string().nullable(),
project_flock_id: yup.string().nullable(),
kandang_id: yup.string().nullable(),
date_start: yup
.string()
.nullable()
.test(
'is-valid-date',
'Tanggal mulai tidak valid',
(value) => !value || !isNaN(Date.parse(value))
),
date_end: yup
.string()
.nullable()
.test(
'is-valid-date',
'Tanggal akhir tidak valid',
(value) => !value || !isNaN(Date.parse(value))
)
.test(
'is-after-start',
'Tanggal akhir tidak boleh lebih awal dari tanggal mulai',
function (value) {
const { date_start } = this.parent;
if (!date_start || !value) return true;
return new Date(value) >= new Date(date_start);
}
),
sort_by: yup.string().nullable(),
show_unrecorded: yup.boolean().nullable(),
area_id: yup.string().required('Area wajib dipilih'),
location_id: yup.string().required('Lokasi wajib dipilih'),
project_flock_id: yup.string().required('Project Flock wajib dipilih'),
kandang_id: yup.string().required('Kandang wajib dipilih'),
});
export type ProductionResultFilterValues = yup.InferType<
@@ -4,13 +4,26 @@ import Table from '@/components/Table';
import { ProductionResult } from '@/types/api/report/production-result';
import { ColumnDef } from '@tanstack/react-table';
type ProductionResultColumn =
| ColumnDef<ProductionResult>
| {
header: string;
columns: Array<{
header: string;
accessorKey?: string;
cell?: (props: {
row: { original: ProductionResult };
}) => React.ReactNode;
}>;
};
const ProductionResultSkeleton = ({
columns,
icon,
title,
subtitle,
}: {
columns: ColumnDef<ProductionResult>[];
columns: ProductionResultColumn[];
icon: React.ReactNode;
title: string;
subtitle: string;
File diff suppressed because it is too large Load Diff