mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
400 lines
14 KiB
TypeScript
400 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import Button from '@/components/Button';
|
|
import Card from '@/components/Card';
|
|
import { Icon } from '@iconify/react';
|
|
import ProductionLineChart from '@/components/pages/dashboard/chart/ProductionLineChart';
|
|
import StandardLineChart from '@/components/pages/dashboard/chart/StandardLineChart';
|
|
import EggWeightBarChart from '@/components/pages/dashboard/chart/EggWeightBarChart';
|
|
import FCRBarChart from '@/components/pages/dashboard/chart/FCRBarChart';
|
|
import ProductionStat from '@/components/pages/dashboard/chart/ProductionStat';
|
|
import Modal, { useModal } from '@/components/Modal';
|
|
import DateInput from '@/components/input/DateInput';
|
|
import SelectInput, {
|
|
OptionType,
|
|
useSelect,
|
|
} from '@/components/input/SelectInput';
|
|
import { RadioGroup } from '@/components/input/RadioInput';
|
|
import { useState } from 'react';
|
|
import useSWR from 'swr';
|
|
import { DashboardApi } from '@/services/api/dashboard';
|
|
import { useFormik } from 'formik';
|
|
import dashboardProductionFilterSchema from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
|
import { ProjectFlockApi } from '@/services/api/production';
|
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
|
|
|
const DashboardProduction = () => {
|
|
const filterModal = useModal();
|
|
const [selectedPeriod, setSelectedPeriod] = useState('daily');
|
|
const [selectedStandards, setSelectedStandards] = useState<string[]>([
|
|
'hen_day',
|
|
'hen_house',
|
|
]);
|
|
const [endpointUrl, setEndpointUrl] = useState('/dashboard');
|
|
|
|
// ===== FETCH DATA =====
|
|
const {
|
|
data: dashboardProductionResponse,
|
|
isLoading: isLoadingDashboardProductionData,
|
|
error: dashboardProductionError,
|
|
} = useSWR(endpointUrl, () =>
|
|
DashboardApi.getDashboardProductionFetcher(endpointUrl)
|
|
);
|
|
|
|
const dashboardProductionData =
|
|
dashboardProductionResponse?.status === 'success'
|
|
? dashboardProductionResponse.data
|
|
: undefined;
|
|
|
|
// ===== SELECT =====
|
|
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
|
|
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
|
limit: 'limit',
|
|
category: 'LAYING',
|
|
});
|
|
const {
|
|
options: standardProductionOptions,
|
|
isLoadingOptions: isLoadingStandardProductionOptions,
|
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
|
limit: 'limit',
|
|
});
|
|
|
|
// ===== FORMIK =====
|
|
const formik = useFormik({
|
|
initialValues: {
|
|
startDate: '',
|
|
endDate: '',
|
|
flock: [] as OptionType[],
|
|
standard_production_id: [] as OptionType[],
|
|
standard_productions: [] as OptionType[],
|
|
period: selectedPeriod,
|
|
},
|
|
validationSchema: dashboardProductionFilterSchema,
|
|
onSubmit: (values) => {
|
|
console.log(values);
|
|
// Build URL with query parameters
|
|
const params = new URLSearchParams();
|
|
|
|
if (values.startDate) params.set('startDate', values.startDate);
|
|
if (values.endDate) params.set('endDate', values.endDate);
|
|
|
|
if (values.flock && values.flock.length > 0) {
|
|
const flockIds = values.flock
|
|
.map((f: OptionType) => f.value || f)
|
|
.join(',');
|
|
params.set('flock', flockIds);
|
|
}
|
|
|
|
if (
|
|
values.standard_production_id &&
|
|
values.standard_production_id.length > 0
|
|
) {
|
|
const standardIds = values.standard_production_id
|
|
.map((s: OptionType) => s.value || s)
|
|
.join(',');
|
|
params.set('standard_production_id', standardIds);
|
|
}
|
|
|
|
if (selectedStandards.length > 0) {
|
|
params.set('standards', selectedStandards.join(','));
|
|
}
|
|
|
|
params.set('period', selectedPeriod);
|
|
|
|
const newUrl = `/dashboard?${params.toString()}`;
|
|
setEndpointUrl(newUrl);
|
|
|
|
// Close modal after applying filter
|
|
filterModal.closeModal();
|
|
},
|
|
});
|
|
|
|
const handleResetFilter = () => {
|
|
formik.resetForm();
|
|
setSelectedPeriod('daily');
|
|
setSelectedStandards(['hen_day', 'hen_house']);
|
|
setEndpointUrl('/dashboard');
|
|
};
|
|
|
|
if (isLoadingDashboardProductionData) {
|
|
return (
|
|
<div className='w-full min-h-screen flex items-center justify-center'>
|
|
<span className='loading loading-spinner loading-xl'></span>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<>
|
|
<section className='w-full p-4 space-y-6'>
|
|
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
|
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
|
|
<div className='flex flex-row justify-end gap-2'>
|
|
<Button
|
|
variant='outline'
|
|
className='min-w-28 rounded-lg'
|
|
onClick={() => filterModal.openModal()}
|
|
>
|
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
Filter
|
|
</Button>
|
|
<Button
|
|
variant='outline'
|
|
color='neutral'
|
|
className='min-w-28 rounded-lg'
|
|
>
|
|
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} />
|
|
Export
|
|
<Icon icon='heroicons:chevron-down' width={20} height={20} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dashboard Statistics */}
|
|
<ProductionStat data={dashboardProductionData?.statistics_data} />
|
|
|
|
{/* Charts Grid */}
|
|
<div className='grid grid-cols-1 gap-4'>
|
|
{/* Production Line Chart */}
|
|
<Card
|
|
variant='bordered'
|
|
className={{ wrapper: 'w-full', body: 'p-6' }}
|
|
>
|
|
<ProductionLineChart
|
|
period={
|
|
selectedPeriod as 'daily' | 'weekly' | 'monthly' | 'yearly'
|
|
}
|
|
data={dashboardProductionData?.production_charts}
|
|
/>
|
|
</Card>
|
|
|
|
{/* Standard Line Chart */}
|
|
<Card
|
|
variant='bordered'
|
|
className={{ wrapper: 'w-full', body: 'p-6' }}
|
|
>
|
|
<StandardLineChart
|
|
selectedStandards={selectedStandards}
|
|
data={dashboardProductionData?.standard_productions}
|
|
/>
|
|
</Card>
|
|
|
|
{/* Bar Charts Grid - 2 columns */}
|
|
<div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
|
|
{/* FCR Bar Chart */}
|
|
<Card
|
|
variant='bordered'
|
|
className={{ wrapper: 'w-full', body: 'p-6' }}
|
|
>
|
|
<FCRBarChart data={dashboardProductionData?.fcr_data} />
|
|
</Card>
|
|
|
|
{/* Egg Weight Bar Chart */}
|
|
<Card
|
|
variant='bordered'
|
|
className={{ wrapper: 'w-full', body: 'p-6' }}
|
|
>
|
|
<EggWeightBarChart data={dashboardProductionData?.egg_weights} />
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<Modal
|
|
ref={filterModal.ref}
|
|
className={{
|
|
modal: 'p-0',
|
|
modalBox: 'p-0 rounded-xl',
|
|
}}
|
|
>
|
|
<div className='space-y-6'>
|
|
{/* Modal Header */}
|
|
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300'>
|
|
<div className='flex items-center gap-2 ms-4'>
|
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
|
<h3 className='font-semibold'>Filter Data</h3>
|
|
</div>
|
|
<Button
|
|
variant='link'
|
|
onClick={() => filterModal.closeModal()}
|
|
className='text-gray-500 hover:text-gray-700 me-4 '
|
|
>
|
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
|
</Button>
|
|
</div>
|
|
|
|
<form className='space-y-4' onSubmit={formik.handleSubmit}>
|
|
{/* Rentang Waktu */}
|
|
<div className='px-4'>
|
|
<label className='flex items-center gap-2 mb-3'>
|
|
<Icon icon='heroicons:calendar' width={20} height={20} />
|
|
Rentang Waktu
|
|
</label>
|
|
<div className='flex items-center gap-2'>
|
|
<DateInput
|
|
name='startDate'
|
|
placeholder='Tanggal Mulai'
|
|
value={formik.values.startDate}
|
|
errorMessage={formik.errors.startDate}
|
|
onChange={formik.handleChange}
|
|
className={{
|
|
inputWrapper: 'rounded-lg',
|
|
}}
|
|
isError={
|
|
Boolean(formik.errors.startDate) &&
|
|
Boolean(formik.touched.startDate)
|
|
}
|
|
/>
|
|
<span className='hidden md:block text-center'>—</span>
|
|
<DateInput
|
|
name='endDate'
|
|
placeholder='Tanggal Akhir'
|
|
value={formik.values.endDate}
|
|
errorMessage={formik.errors.endDate}
|
|
onChange={formik.handleChange}
|
|
className={{
|
|
inputWrapper: 'rounded-lg',
|
|
}}
|
|
isError={
|
|
Boolean(formik.errors.endDate) &&
|
|
Boolean(formik.touched.endDate)
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Flock */}
|
|
<div className='px-4'>
|
|
<SelectInput
|
|
label='Flock'
|
|
value={formik.values.flock}
|
|
onChange={(selected) => formik.setFieldValue('flock', selected)}
|
|
errorMessage={formik.errors.flock as string}
|
|
options={flockOptions}
|
|
isLoading={isLoadingFlockOptions}
|
|
isMulti
|
|
isError={
|
|
Boolean(formik.errors.flock) && Boolean(formik.touched.flock)
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Production */}
|
|
<div className='px-4'>
|
|
<SelectInput
|
|
label='Standard Produksi'
|
|
value={formik.values.standard_production_id}
|
|
onChange={(selected) =>
|
|
formik.setFieldValue('standard_production_id', selected)
|
|
}
|
|
errorMessage={formik.errors.standard_production_id as string}
|
|
options={standardProductionOptions}
|
|
isLoading={isLoadingStandardProductionOptions}
|
|
isMulti
|
|
isError={
|
|
Boolean(formik.errors.standard_production_id) &&
|
|
Boolean(formik.touched.standard_production_id)
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Standard */}
|
|
<div className='px-4'>
|
|
<SelectInput
|
|
label='Standard'
|
|
value={selectedStandards.map((s) => ({
|
|
value: s,
|
|
label:
|
|
s === 'hen_day'
|
|
? 'Hen Day'
|
|
: s === 'hen_house'
|
|
? 'Hen House'
|
|
: s === 'uniformity'
|
|
? 'Uniformity'
|
|
: s === 'egg_weight'
|
|
? 'Egg Weight'
|
|
: 'Egg Mass',
|
|
}))}
|
|
options={[
|
|
{ value: 'hen_day', label: 'Hen Day' },
|
|
{ value: 'hen_house', label: 'Hen House' },
|
|
{ value: 'uniformity', label: 'Uniformity' },
|
|
{ value: 'egg_weight', label: 'Egg Weight' },
|
|
{ value: 'egg_mass', label: 'Egg Mass' },
|
|
]}
|
|
isMulti
|
|
onChange={(selected: OptionType | OptionType[] | null) => {
|
|
const values = Array.isArray(selected)
|
|
? selected.map((item) => String(item.value))
|
|
: [];
|
|
setSelectedStandards(
|
|
values.length > 0 ? values : ['hen_day']
|
|
);
|
|
}}
|
|
isError={
|
|
Boolean(formik.errors.standard_productions) &&
|
|
Boolean(formik.touched.standard_productions)
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* Periode Perbandingan */}
|
|
<div className='px-4'>
|
|
<label className='block mb-3'>Periode Perbandingan</label>
|
|
<div className='grid grid-cols-4 gap-2'>
|
|
<Button
|
|
variant={selectedPeriod === 'daily' ? 'active' : 'soft'}
|
|
type='button'
|
|
className='rounded-lg'
|
|
onClick={() => setSelectedPeriod('daily')}
|
|
>
|
|
Harian
|
|
</Button>
|
|
<Button
|
|
variant={selectedPeriod === 'weekly' ? 'active' : 'soft'}
|
|
type='button'
|
|
className='rounded-lg'
|
|
onClick={() => setSelectedPeriod('weekly')}
|
|
>
|
|
Mingguan
|
|
</Button>
|
|
<Button
|
|
variant={selectedPeriod === 'monthly' ? 'active' : 'soft'}
|
|
type='button'
|
|
className='rounded-lg'
|
|
onClick={() => setSelectedPeriod('monthly')}
|
|
>
|
|
Bulanan
|
|
</Button>
|
|
<Button
|
|
variant={selectedPeriod === 'yearly' ? 'active' : 'soft'}
|
|
type='button'
|
|
className='rounded-lg'
|
|
onClick={() => setSelectedPeriod('yearly')}
|
|
>
|
|
Tahunan
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
|
<Button
|
|
type='reset'
|
|
variant='soft'
|
|
className='ms-4 min-w-36 rounded-lg'
|
|
onClick={handleResetFilter}
|
|
>
|
|
Reset Filter
|
|
</Button>
|
|
<Button type='submit' className='me-4 min-w-36 rounded-lg'>
|
|
Terapkan Filter
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default DashboardProduction;
|