mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into fix/project-flock
This commit is contained in:
@@ -22,6 +22,7 @@ export interface CardProps
|
|||||||
onCollapsedChange?: (collapsed: boolean) => void;
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
className?: {
|
className?: {
|
||||||
wrapper?: string;
|
wrapper?: string;
|
||||||
|
wrapperContent?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -144,6 +145,10 @@ const Card = ({
|
|||||||
return cn('border-t border-base-300 mt-4 pt-4', className?.footer);
|
return cn('border-t border-base-300 mt-4 pt-4', className?.footer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getWrapperContentClasses = () => {
|
||||||
|
return cn('space-y-4', className?.wrapperContent);
|
||||||
|
};
|
||||||
|
|
||||||
const renderCardContent = () => {
|
const renderCardContent = () => {
|
||||||
const hasContent = children || actions || footer;
|
const hasContent = children || actions || footer;
|
||||||
|
|
||||||
@@ -177,7 +182,7 @@ const Card = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const cardContent = (
|
const cardContent = (
|
||||||
<div className='space-y-4'>
|
<div className={getWrapperContentClasses()}>
|
||||||
{children}
|
{children}
|
||||||
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||||
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import PopoverContent from '@/components/popover/PopoverContent';
|
|||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { AuthApi } from '@/services/api/auth';
|
import { AuthApi } from '@/services/api/auth';
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
toggleSidebar?: () => void;
|
toggleSidebar?: () => void;
|
||||||
@@ -21,6 +22,7 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => {
|
|||||||
const { setUser } = useAuth();
|
const { setUser } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const navbarActions = useUiStore((state) => state.navbarActions);
|
||||||
|
|
||||||
const logoutClickHandler = async () => {
|
const logoutClickHandler = async () => {
|
||||||
const logoutRes = await AuthApi.logout();
|
const logoutRes = await AuthApi.logout();
|
||||||
@@ -53,7 +55,9 @@ const Navbar = ({ toggleSidebar }: NavbarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2 items-center'>
|
||||||
|
{/* Page-specific actions */}
|
||||||
|
{navbarActions && <div className='mr-2'>{navbarActions}</div>}
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
|
|||||||
@@ -9,15 +9,21 @@ export type ButtonFilterProps = ButtonProps & {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200
|
||||||
|
|
||||||
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'padding-[12px] rounded-[8px] max-h-[40px] font-semibold text-[14px] gap-[6px]',
|
||||||
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft',
|
||||||
getFilledFormikValuesCount(values) > 0
|
getFilledFormikValuesCount(values) > 0
|
||||||
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
|
? 'border-primary-gradient !rounded-[8px]'
|
||||||
: '',
|
: '!rounded-[8px]',
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -31,7 +37,7 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
|||||||
/>
|
/>
|
||||||
Filter
|
Filter
|
||||||
{getFilledFormikValuesCount(values) > 0 && (
|
{getFilledFormikValuesCount(values) > 0 && (
|
||||||
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
|
<span className='w-[20px] h-[20px] text-white bg-[#FF3535] rounded-[8px] border-[1px] border-base-300 flex items-center justify-center text-xs'>
|
||||||
{getFilledFormikValuesCount(values)}
|
{getFilledFormikValuesCount(values)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -72,8 +72,10 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
|
|||||||
await AuthApi.refresh();
|
await AuthApi.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (user) {
|
||||||
refreshUserSession();
|
refreshUserSession();
|
||||||
}, []);
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
|
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import Button from '@/components/Button';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import SelectInput, {
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
OptionType,
|
|
||||||
useSelect,
|
|
||||||
} from '@/components/input/SelectInput';
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DashboardApi } from '@/services/api/dashboard';
|
import { DashboardApi } from '@/services/api/dashboard';
|
||||||
@@ -21,9 +18,9 @@ import {
|
|||||||
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
||||||
import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton';
|
import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton';
|
||||||
import DashboardAllCharts, {
|
import DashboardExportCharts, {
|
||||||
DashboardAllChartsRef,
|
DashboardExportChartsRef,
|
||||||
} from '@/components/pages/dashboard/chart/DashboardAllCharts';
|
} from '@/components/pages/dashboard/export/DashboardExportCharts';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
||||||
import {
|
import {
|
||||||
DashboardFilter,
|
DashboardFilter,
|
||||||
@@ -40,6 +37,11 @@ import MenuItem from '@/components/menu/MenuItem';
|
|||||||
import { useDashboardStore } from '@/stores/dashboard';
|
import { useDashboardStore } from '@/stores/dashboard';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import DashboardExportStats, {
|
||||||
|
DashboardExportStatsRef,
|
||||||
|
} from '@/components/pages/dashboard/export/DashboardExportStats';
|
||||||
|
|
||||||
// Helper function to normalize values to array
|
// Helper function to normalize values to array
|
||||||
const normalizeToArray = (
|
const normalizeToArray = (
|
||||||
@@ -59,6 +61,10 @@ const DashboardProduction = () => {
|
|||||||
const { filterValues, setFilterValues, resetFilterValues } =
|
const { filterValues, setFilterValues, resetFilterValues } =
|
||||||
useDashboardStore();
|
useDashboardStore();
|
||||||
|
|
||||||
|
// ===== UI STORE (for navbar actions) =====
|
||||||
|
const setNavbarActions = useUiStore((state) => state.setNavbarActions);
|
||||||
|
const clearNavbarActions = useUiStore((state) => state.clearNavbarActions);
|
||||||
|
|
||||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||||
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
||||||
);
|
);
|
||||||
@@ -67,9 +73,8 @@ const DashboardProduction = () => {
|
|||||||
normalizeToArray(filterValues.location)
|
normalizeToArray(filterValues.location)
|
||||||
);
|
);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const statsRef = useRef<HTMLDivElement>(null);
|
const allChartsRef = useRef<DashboardExportChartsRef>(null);
|
||||||
const chartRef = useRef<HTMLDivElement>(null);
|
const allStatsRef = useRef<DashboardExportStatsRef>(null);
|
||||||
const allChartsRef = useRef<DashboardAllChartsRef>(null);
|
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -194,12 +199,69 @@ const DashboardProduction = () => {
|
|||||||
const handleExportPDF = async () => {
|
const handleExportPDF = async () => {
|
||||||
await generateDashboardPDF({
|
await generateDashboardPDF({
|
||||||
filterValues: formik.values,
|
filterValues: formik.values,
|
||||||
statsRef,
|
allStatsRef,
|
||||||
allChartsRef,
|
allChartsRef,
|
||||||
setExporting,
|
setExporting,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== Register Navbar Actions =====
|
||||||
|
const openFilterModalRef = useRef(filterModal.openModal);
|
||||||
|
openFilterModalRef.current = filterModal.openModal;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNavbarActions(
|
||||||
|
<div className='hidden sm:flex flex-row justify-end gap-3 '>
|
||||||
|
<ButtonFilter
|
||||||
|
values={{
|
||||||
|
...formik.values,
|
||||||
|
analysisMode: undefined,
|
||||||
|
}}
|
||||||
|
variant='outline'
|
||||||
|
onClick={() => openFilterModalRef.current()}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-lg font-semibold text-sm gap-1.5',
|
||||||
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon width={20} height={20} icon='heroicons:cloud-arrow-down' />
|
||||||
|
Export
|
||||||
|
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
|
||||||
|
<Icon width={14} height={14} icon='heroicons:chevron-down' />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
content: 'w-full mt-1 p-0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
className={`p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg ${exporting ? 'hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
className='text-sm p-3'
|
||||||
|
title='PDF'
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [formik.values, exporting, setNavbarActions]);
|
||||||
|
|
||||||
|
// Cleanup only on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearNavbarActions();
|
||||||
|
};
|
||||||
|
}, [clearNavbarActions]);
|
||||||
|
|
||||||
if (isLoadingDashboardProductionData) {
|
if (isLoadingDashboardProductionData) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||||
@@ -210,48 +272,62 @@ const DashboardProduction = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-4 space-y-6'>
|
<section className='w-full p-3 space-y-3'>
|
||||||
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
<div className='flex sm:hidden flex-row justify-end gap-3 '>
|
||||||
<div></div>
|
|
||||||
|
|
||||||
<div className='flex flex-row justify-end gap-2'>
|
|
||||||
<ButtonFilter
|
<ButtonFilter
|
||||||
values={{
|
values={{
|
||||||
...formik.values,
|
...formik.values,
|
||||||
analysisMode: undefined,
|
analysisMode: undefined,
|
||||||
}}
|
}}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='min-w-28 rounded-lg'
|
onClick={() => openFilterModalRef.current()}
|
||||||
onClick={() => filterModal.openModal()}
|
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant='outline' className='min-w-28 rounded-lg z-50'>
|
<Button
|
||||||
<Icon icon='heroicons:arrow-down-tray' />
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-lg font-semibold text-sm gap-1.5',
|
||||||
|
'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
icon='heroicons:cloud-arrow-down'
|
||||||
|
/>
|
||||||
Export
|
Export
|
||||||
<Icon icon='heroicons:chevron-down' />
|
<div className='w-6.5 h-5 flex items-center justify-center border-l border-base-content/10'>
|
||||||
|
<Icon width={14} height={14} icon='heroicons:chevron-down' />
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
className={{
|
className={{
|
||||||
content: 'w-full',
|
content:
|
||||||
|
'w-full mt-1 p-0 shadow-button-soft border border-base-content/10 rounded-lg',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu className={exporting ? 'hidden' : ''}>
|
<Menu
|
||||||
<MenuItem title='PDF' onClick={handleExportPDF} />
|
className={`p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg ${exporting ? 'hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
className='text-sm p-3'
|
||||||
|
title='PDF'
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dashboard Stats */}
|
{/* Dashboard Stats */}
|
||||||
<div ref={statsRef}>
|
<div>
|
||||||
<DashboardStats
|
<DashboardStats
|
||||||
data={dashboardProductionData?.statistics_data ?? []}
|
data={dashboardProductionData?.statistics_data ?? []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Use DashboardLineChart component or skeleton */}
|
{/* Use DashboardLineChart component or skeleton */}
|
||||||
<div ref={chartRef}>
|
<div>
|
||||||
{isLoadingDashboardProductionData ? (
|
{isLoadingDashboardProductionData ? (
|
||||||
<DashboardLineChartSkeleton />
|
<DashboardLineChartSkeleton />
|
||||||
) : dashboardProductionData &&
|
) : dashboardProductionData &&
|
||||||
@@ -287,6 +363,8 @@ const DashboardProduction = () => {
|
|||||||
|
|
||||||
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
||||||
{dashboardProductionData && (
|
{dashboardProductionData && (
|
||||||
|
<>
|
||||||
|
{/* Export Stats Charts */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -295,7 +373,22 @@ const DashboardProduction = () => {
|
|||||||
width: '1200px', // Fixed width for consistent PDF rendering
|
width: '1200px', // Fixed width for consistent PDF rendering
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DashboardAllCharts
|
<DashboardExportStats
|
||||||
|
ref={allStatsRef}
|
||||||
|
data={dashboardProductionData?.statistics_data ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Export ALL Charts */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-9999px',
|
||||||
|
top: 0,
|
||||||
|
width: '1200px', // Fixed width for consistent PDF rendering
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardExportCharts
|
||||||
ref={allChartsRef}
|
ref={allChartsRef}
|
||||||
data={dashboardProductionData}
|
data={dashboardProductionData}
|
||||||
analysisMode={
|
analysisMode={
|
||||||
@@ -309,6 +402,7 @@ const DashboardProduction = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -316,58 +410,55 @@ const DashboardProduction = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
className={{
|
className={{
|
||||||
modal: 'p-0',
|
modal: 'p-0',
|
||||||
modalBox: 'p-0 rounded-xl',
|
modalBox: 'p-0 rounded-[0.875rem]',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='space-y-6'>
|
<div className='flex flex-col'>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300'>
|
<div className='flex items-center justify-between p-4 border-b border-base-content/10'>
|
||||||
<div className='flex items-center gap-2 ms-4'>
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
<h3 className='font-semibold'>Filter Data</h3>
|
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant='link'
|
||||||
onClick={() => filterModal.closeModal()}
|
onClick={() => filterModal.closeModal()}
|
||||||
className='text-gray-500 hover:text-gray-700 me-4 '
|
className='text-gray-500 hover:text-gray-700'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className='space-y-4'
|
className='flex flex-col'
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
onReset={handleResetFilter}
|
onReset={handleResetFilter}
|
||||||
>
|
>
|
||||||
|
<div className='flex flex-col p-4 gap-1.5'>
|
||||||
{/* Rentang Waktu */}
|
{/* Rentang Waktu */}
|
||||||
<div className='px-4'>
|
<div>
|
||||||
<label className='flex items-center gap-2 mb-3'>Tanggal</label>
|
<label className='flex text-xs items-center gap-2 py-2 font-semibold'>
|
||||||
<div className='flex items-start gap-2'>
|
Tanggal
|
||||||
|
</label>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='startDate'
|
name='startDate'
|
||||||
placeholder='Tanggal Mulai'
|
placeholder='Tanggal Mulai'
|
||||||
value={formik.values.startDate}
|
value={formik.values.startDate}
|
||||||
errorMessage={formik.errors.startDate}
|
errorMessage={formik.errors.startDate}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
className={{
|
|
||||||
inputWrapper: 'rounded-lg',
|
|
||||||
}}
|
|
||||||
isError={
|
isError={
|
||||||
Boolean(formik.errors.startDate) &&
|
Boolean(formik.errors.startDate) &&
|
||||||
Boolean(formik.touched.startDate)
|
Boolean(formik.touched.startDate)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className='hidden md:block mt-3 text-center'>—</div>
|
<hr className='w-full max-w-3 h-px border-base-content/10'></hr>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='endDate'
|
name='endDate'
|
||||||
placeholder='Tanggal Akhir'
|
placeholder='Tanggal Akhir'
|
||||||
value={formik.values.endDate}
|
value={formik.values.endDate}
|
||||||
errorMessage={formik.errors.endDate}
|
errorMessage={formik.errors.endDate}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
className={{
|
|
||||||
inputWrapper: 'rounded-lg',
|
|
||||||
}}
|
|
||||||
isError={
|
isError={
|
||||||
Boolean(formik.errors.endDate) &&
|
Boolean(formik.errors.endDate) &&
|
||||||
Boolean(formik.touched.endDate)
|
Boolean(formik.touched.endDate)
|
||||||
@@ -377,14 +468,18 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Analysis Mode */}
|
{/* Analysis Mode */}
|
||||||
<div className='px-4'>
|
<div>
|
||||||
<label className='block mb-3'>Analysis Mode</label>
|
<label className='block py-2 text-xs font-semibold'>
|
||||||
|
Analysis Mode
|
||||||
|
</label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
name='analysisMode'
|
name='analysisMode'
|
||||||
value={formik.values.analysisMode}
|
value={formik.values.analysisMode}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
formik.handleChange(e);
|
formik.handleChange(e);
|
||||||
setAnalysisMode(e.target.value as 'OVERVIEW' | 'COMPARISON');
|
setAnalysisMode(
|
||||||
|
e.target.value as 'OVERVIEW' | 'COMPARISON'
|
||||||
|
);
|
||||||
// Reset all dependent fields when analysis mode changes
|
// Reset all dependent fields when analysis mode changes
|
||||||
formik.setFieldValue('location', []);
|
formik.setFieldValue('location', []);
|
||||||
formik.setFieldValue('flock', []);
|
formik.setFieldValue('flock', []);
|
||||||
@@ -394,24 +489,27 @@ const DashboardProduction = () => {
|
|||||||
}}
|
}}
|
||||||
color='primary'
|
color='primary'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full my-6 font-semibold text-neutral-500',
|
wrapper:
|
||||||
|
'w-full flex flex-row items-center font-medium text-base-content/50',
|
||||||
|
radioWrapper: 'w-full grid grid-cols-2 gap-0 p-0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
color='primary'
|
color='primary'
|
||||||
value='OVERVIEW'
|
value='OVERVIEW'
|
||||||
label='Performance Overview'
|
label='Performance Overview'
|
||||||
|
className='w-full p-3'
|
||||||
/>
|
/>
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
color='primary'
|
color='primary'
|
||||||
value='COMPARISON'
|
value='COMPARISON'
|
||||||
label='Performance Comparison'
|
label='Performance Comparison'
|
||||||
|
className='w-full p-3'
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formik.values.analysisMode === 'COMPARISON' && (
|
{formik.values.analysisMode === 'COMPARISON' && (
|
||||||
<div className='px-4'>
|
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
label='Compared By'
|
label='Compared By'
|
||||||
value={comparisonTypeOptions.find(
|
value={comparisonTypeOptions.find(
|
||||||
@@ -430,12 +528,13 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.comparisonType) &&
|
Boolean(formik.errors.comparisonType) &&
|
||||||
Boolean(formik.touched.comparisonType)
|
Boolean(formik.touched.comparisonType)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Location */}
|
{/* Location */}
|
||||||
<div className='px-4'>
|
|
||||||
{comparisonTypeOptions.find(
|
{comparisonTypeOptions.find(
|
||||||
(option) => option.value === formik.values.comparisonType
|
(option) => option.value === formik.values.comparisonType
|
||||||
)?.value === 'FARM' ? (
|
)?.value === 'FARM' ? (
|
||||||
@@ -465,6 +564,9 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.location) &&
|
Boolean(formik.errors.location) &&
|
||||||
Boolean(formik.touched.location)
|
Boolean(formik.touched.location)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -493,9 +595,11 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.location) &&
|
Boolean(formik.errors.location) &&
|
||||||
Boolean(formik.touched.location)
|
Boolean(formik.touched.location)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Flock */}
|
{/* Flock */}
|
||||||
{!(
|
{!(
|
||||||
@@ -505,7 +609,7 @@ const DashboardProduction = () => {
|
|||||||
formik.values.comparisonType === 'KANDANG'
|
formik.values.comparisonType === 'KANDANG'
|
||||||
)
|
)
|
||||||
) && (
|
) && (
|
||||||
<div className='px-4'>
|
<>
|
||||||
{comparisonTypeOptions.find(
|
{comparisonTypeOptions.find(
|
||||||
(option) => option.value === formik.values.comparisonType
|
(option) => option.value === formik.values.comparisonType
|
||||||
)?.value === 'FLOCK' ? (
|
)?.value === 'FLOCK' ? (
|
||||||
@@ -530,6 +634,9 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.flock) &&
|
Boolean(formik.errors.flock) &&
|
||||||
Boolean(formik.touched.flock)
|
Boolean(formik.touched.flock)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -553,9 +660,12 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.flock) &&
|
Boolean(formik.errors.flock) &&
|
||||||
Boolean(formik.touched.flock)
|
Boolean(formik.touched.flock)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Kandang */}
|
{/* Kandang */}
|
||||||
@@ -563,7 +673,7 @@ const DashboardProduction = () => {
|
|||||||
formik.values.analysisMode === 'COMPARISON' &&
|
formik.values.analysisMode === 'COMPARISON' &&
|
||||||
!(formik.values.comparisonType === 'KANDANG')
|
!(formik.values.comparisonType === 'KANDANG')
|
||||||
) && (
|
) && (
|
||||||
<div className='px-4'>
|
<>
|
||||||
{comparisonTypeOptions.find(
|
{comparisonTypeOptions.find(
|
||||||
(option) => option.value === formik.values.comparisonType
|
(option) => option.value === formik.values.comparisonType
|
||||||
)?.value === 'KANDANG' ? (
|
)?.value === 'KANDANG' ? (
|
||||||
@@ -588,6 +698,9 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.kandang) &&
|
Boolean(formik.errors.kandang) &&
|
||||||
Boolean(formik.touched.kandang)
|
Boolean(formik.touched.kandang)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectInputRadio
|
<SelectInputRadio
|
||||||
@@ -611,26 +724,38 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.errors.kandang) &&
|
Boolean(formik.errors.kandang) &&
|
||||||
Boolean(formik.touched.kandang)
|
Boolean(formik.touched.kandang)
|
||||||
}
|
}
|
||||||
|
className={{
|
||||||
|
select: 'rounded-lg text-sm border-base-content/10',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='w-full p-4'>
|
{formErrorList.length > 0 && (
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<div className='w-full'>
|
||||||
|
<AlertErrorList
|
||||||
|
formErrorList={formErrorList}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
<div className='flex justify-between gap-4 p-4 border-t border-base-content/10 bg-gray-100'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='ms-4 min-w-36 rounded-lg'
|
className='rounded-lg p-3 bg-gray-100 border-gray-100 text-base-content/65 hover:bg-base-content/10'
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button type='submit' className='me-4 min-w-36 rounded-lg'>
|
<Button
|
||||||
Terapkan Filter
|
type='submit'
|
||||||
|
className='min-w-40 text-sm p-3 text-white rounded-lg'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -147,11 +147,12 @@ const DashboardLineChart = ({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full rounded-lg',
|
wrapper: 'w-full rounded-lg p-0',
|
||||||
|
body: 'p-4',
|
||||||
}}
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
>
|
>
|
||||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6'>
|
<div className='flex flex-col sm:flex-row justify-between items-start gap-4 mb-3'>
|
||||||
<div className='text-lg font-semibold'>
|
<div className='text-lg font-semibold'>
|
||||||
Performance{' '}
|
Performance{' '}
|
||||||
<Icon
|
<Icon
|
||||||
@@ -165,26 +166,28 @@ const DashboardLineChart = ({
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
align='end'
|
align='end'
|
||||||
direction='bottom'
|
direction='bottom'
|
||||||
|
className={{
|
||||||
|
content: 'mt-1 min-w-full',
|
||||||
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='none'
|
color='none'
|
||||||
className='text-neutral-500 hover:text-neutral-700 rounded-lg px-4 py-2 border-neutral-300'
|
className='py-2.5 pl-3 pr-2 text-base-content/50 rounded-lg text-sm border-base-content/10 shadow-button-soft'
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
>
|
>
|
||||||
{chartTypeLabels[chartData]}{' '}
|
{chartTypeLabels[chartData]}{' '}
|
||||||
<div className='divider divider-horizontal p-0 m-0 before:bg-neutral-300 after:bg-neutral-300'></div>
|
<div className='w-6 h-5 flex items-center justify-center border-l border-base-content/10'>
|
||||||
<Icon icon='heroicons:chevron-down' width={20} height={20} />
|
<Icon icon='heroicons:chevron-down' width={14} height={14} />
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
className={{
|
|
||||||
content: 'w-52 mt-3',
|
|
||||||
}}
|
|
||||||
controlled={open}
|
controlled={open}
|
||||||
>
|
>
|
||||||
<Menu>
|
<Menu className='p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg'>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title='Body weight'
|
title='Body weight'
|
||||||
|
className='text-sm padding-3 whitespace-nowrap'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChartData('body_weight');
|
setChartData('body_weight');
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
@@ -192,6 +195,7 @@ const DashboardLineChart = ({
|
|||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title='Performance'
|
title='Performance'
|
||||||
|
className='text-sm padding-3 whitespace-nowrap'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChartData('performance');
|
setChartData('performance');
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
@@ -199,6 +203,7 @@ const DashboardLineChart = ({
|
|||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title='FCR'
|
title='FCR'
|
||||||
|
className='text-sm padding-3 whitespace-nowrap'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChartData('fcr');
|
setChartData('fcr');
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
@@ -206,6 +211,7 @@ const DashboardLineChart = ({
|
|||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title='Quality Control'
|
title='Quality Control'
|
||||||
|
className='text-sm padding-3 whitespace-nowrap'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChartData('quality_control');
|
setChartData('quality_control');
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
@@ -213,6 +219,7 @@ const DashboardLineChart = ({
|
|||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title='Deplesi'
|
title='Deplesi'
|
||||||
|
className='text-sm padding-3 whitespace-nowrap'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChartData('deplesi');
|
setChartData('deplesi');
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
@@ -248,8 +255,8 @@ const DashboardLineChart = ({
|
|||||||
.includes('std');
|
.includes('std');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
key={series.id}
|
key={`${series.id}-${index}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newVisible = new Set(visibleSeries);
|
const newVisible = new Set(visibleSeries);
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
@@ -259,14 +266,16 @@ const DashboardLineChart = ({
|
|||||||
}
|
}
|
||||||
setVisibleSeries(newVisible);
|
setVisibleSeries(newVisible);
|
||||||
}}
|
}}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors ${
|
variant='outline'
|
||||||
|
color='none'
|
||||||
|
className={`flex items-center gap-2 p-3 rounded-lg border transition-colors ${
|
||||||
isVisible
|
isVisible
|
||||||
? 'border-neutral-400 bg-neutral-50'
|
? 'border-base-content/10 hover:bg-base-content/4'
|
||||||
: 'border-neutral-300 hover:bg-neutral-50'
|
: 'border-base-content/10 bg-base-content/4'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-6 h-0.5 ${
|
className={`w-5 h-0.5 ${
|
||||||
isStandard ? 'border-t-2 border-dashed' : ''
|
isStandard ? 'border-t-2 border-dashed' : ''
|
||||||
} ${!isVisible ? 'opacity-30' : ''}`}
|
} ${!isVisible ? 'opacity-30' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -279,17 +288,17 @@ const DashboardLineChart = ({
|
|||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${isVisible ? 'text-neutral-900 font-medium' : 'text-neutral-700'}`}
|
className={`font-semibold text-sm ${isVisible ? 'text-base-content/50' : 'text-base-content/50'}`}
|
||||||
>
|
>
|
||||||
{series.label}
|
{series.label}
|
||||||
</span>
|
</span>
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:information-circle'
|
icon='heroicons:eye'
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
className='text-neutral-400'
|
className='text-base-content/40'
|
||||||
/>
|
/>
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
})()}
|
})()}
|
||||||
@@ -335,20 +344,68 @@ const DashboardLineChart = ({
|
|||||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey='week'
|
dataKey='week'
|
||||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
tick={{
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#18181B',
|
||||||
|
opacity: 0.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={{ stroke: '#e5e7eb' }}
|
axisLine={{ stroke: '#C1C1C180', opacity: 0.5 }}
|
||||||
label={{
|
label={{
|
||||||
value: 'Weeks',
|
value: 'Weeks',
|
||||||
position: 'insideBottom',
|
position: 'insideBottom',
|
||||||
offset: -5,
|
offset: -5,
|
||||||
style: { fontSize: 12, fill: '#9ca3af' },
|
style: {
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#18181B',
|
||||||
|
opacity: 0.2,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
tick={{
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#18181B',
|
||||||
|
opacity: 0.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
label={
|
||||||
|
(chartData === 'body_weight' || chartData === 'performance') &&
|
||||||
|
analysisMode === 'OVERVIEW'
|
||||||
|
? {
|
||||||
|
value:
|
||||||
|
chartData === 'body_weight'
|
||||||
|
? 'Body Weight'
|
||||||
|
: 'Percentage',
|
||||||
|
position: 'insideLeft',
|
||||||
|
angle: -90,
|
||||||
|
offset: 5,
|
||||||
|
style: {
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#18181B',
|
||||||
|
opacity: 0.2,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: analysisMode === 'COMPARISON'
|
||||||
|
? {
|
||||||
|
value: 'Percentage',
|
||||||
|
position: 'insideLeft',
|
||||||
|
angle: -90,
|
||||||
|
offset: 5,
|
||||||
|
style: {
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#18181B',
|
||||||
|
opacity: 0.2,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={{ stroke: '#e5e7eb' }}
|
axisLine={{ stroke: '#C1C1C180', opacity: 0.5 }}
|
||||||
domain={(() => {
|
domain={(() => {
|
||||||
// Calculate dynamic domain based on visible data
|
// Calculate dynamic domain based on visible data
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
@@ -399,14 +456,12 @@ const DashboardLineChart = ({
|
|||||||
})()}
|
})()}
|
||||||
ticks={(() => {
|
ticks={(() => {
|
||||||
// Calculate dynamic ticks based on domain
|
// Calculate dynamic ticks based on domain
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
|
||||||
let dataset: DashboardChartsDataset[] = [];
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
analysisMode === 'OVERVIEW' &&
|
analysisMode === 'OVERVIEW' &&
|
||||||
isOverviewCharts(data.charts)
|
isOverviewCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
|
||||||
dataset = data.charts[chartData]?.dataset || [];
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
} else if (
|
} else if (
|
||||||
analysisMode === 'COMPARISON' &&
|
analysisMode === 'COMPARISON' &&
|
||||||
@@ -416,7 +471,6 @@ const DashboardLineChart = ({
|
|||||||
data.charts.farm ||
|
data.charts.farm ||
|
||||||
data.charts.flock ||
|
data.charts.flock ||
|
||||||
data.charts.kandang;
|
data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
dataset = comparisonChart?.dataset || [];
|
dataset = comparisonChart?.dataset || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +490,20 @@ const DashboardLineChart = ({
|
|||||||
|
|
||||||
const minValue = Math.min(...allValues);
|
const minValue = Math.min(...allValues);
|
||||||
const maxValue = Math.max(...allValues);
|
const maxValue = Math.max(...allValues);
|
||||||
|
|
||||||
|
// Handle edge case where min equals max
|
||||||
|
if (minValue === maxValue) {
|
||||||
|
const value = Math.round(minValue);
|
||||||
|
const padding = Math.max(10, Math.abs(value) * 0.2);
|
||||||
|
return [
|
||||||
|
Math.floor(value - padding),
|
||||||
|
Math.floor(value - padding / 2),
|
||||||
|
value,
|
||||||
|
Math.ceil(value + padding / 2),
|
||||||
|
Math.ceil(value + padding),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const padding = (maxValue - minValue) * 0.1;
|
const padding = (maxValue - minValue) * 0.1;
|
||||||
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
||||||
const domainMax = Math.ceil(maxValue + padding);
|
const domainMax = Math.ceil(maxValue + padding);
|
||||||
@@ -444,21 +512,25 @@ const DashboardLineChart = ({
|
|||||||
const range = domainMax - domainMin;
|
const range = domainMax - domainMin;
|
||||||
const step = range / 4;
|
const step = range / 4;
|
||||||
|
|
||||||
return [
|
// Use Set to ensure unique values
|
||||||
|
const tickSet = new Set([
|
||||||
domainMin,
|
domainMin,
|
||||||
Math.round(domainMin + step),
|
Math.round(domainMin + step),
|
||||||
Math.round(domainMin + step * 2),
|
Math.round(domainMin + step * 2),
|
||||||
Math.round(domainMin + step * 3),
|
Math.round(domainMin + step * 3),
|
||||||
domainMax,
|
domainMax,
|
||||||
];
|
]);
|
||||||
|
|
||||||
|
return Array.from(tickSet).sort((a, b) => a - b);
|
||||||
})()}
|
})()}
|
||||||
|
tickFormatter={(value) => formatNumber(Number(value))}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
backgroundColor: '#1f2937',
|
backgroundColor: '#1f2937',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '8px 12px',
|
padding: '12px 12px',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
}}
|
}}
|
||||||
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
||||||
@@ -466,8 +538,8 @@ const DashboardLineChart = ({
|
|||||||
labelFormatter={(value) => `Week ${value}`}
|
labelFormatter={(value) => `Week ${value}`}
|
||||||
content={(props) => {
|
content={(props) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-2 rounded-lg bg-neutral-950 p-4 text-white'>
|
<div className='flex flex-col gap-1.5 rounded-lg bg-neutral-950 p-4 text-white'>
|
||||||
<p className='text-neutral-300 text-xs font-semibold text-start'>
|
<p className='text-white/50 text-xs font-semibold text-start'>
|
||||||
{analysisMode === 'OVERVIEW'
|
{analysisMode === 'OVERVIEW'
|
||||||
? selectedKandang
|
? selectedKandang
|
||||||
? selectedKandang.label || 'Overview Performance'
|
? selectedKandang.label || 'Overview Performance'
|
||||||
@@ -506,12 +578,12 @@ const DashboardLineChart = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={item.name}
|
key={`${item.name}-${index}`}
|
||||||
className='flex w-full justify-between items-center flex-row gap-6 p-0'
|
className='flex w-full justify-between items-center flex-row gap-y-1.5 gap-x-3 p-0'
|
||||||
>
|
>
|
||||||
<span className='flex flex-row gap-1 items-center'>
|
<span className='flex flex-row gap-1.5 items-center'>
|
||||||
<div
|
<div
|
||||||
className='h-4 w-4 m-0 rounded-md'
|
className='h-5 w-5 m-0 rounded'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
}}
|
}}
|
||||||
@@ -526,7 +598,7 @@ const DashboardLineChart = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
<p className='text-neutral-300 text-xs text-start'>
|
<p className='text-white/50 text-xs text-start'>
|
||||||
Week {props.label}
|
Week {props.label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -598,7 +670,7 @@ const DashboardLineChart = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Line
|
<Line
|
||||||
key={series.id}
|
key={`${series.id}--${index}`}
|
||||||
type='monotone'
|
type='monotone'
|
||||||
dataKey={dataKey}
|
dataKey={dataKey}
|
||||||
name={series.label}
|
name={series.label}
|
||||||
@@ -649,20 +721,22 @@ const DashboardLineChart = ({
|
|||||||
return (
|
return (
|
||||||
<div className='absolute inset-x-0 inset-y-15 z-10 flex flex-col items-center justify-center rounded-lg'>
|
<div className='absolute inset-x-0 inset-y-15 z-10 flex flex-col items-center justify-center rounded-lg'>
|
||||||
{/* Chart icon */}
|
{/* Chart icon */}
|
||||||
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
<div className='w-12.5 h-12.5 bg-[var(--main-color-base-100,#FFFFFF)] border border-base-content/10 rounded-[0.875rem] border border-base-content shadow-[0px_25px_50px_-12px_#00000040] flex items-center justify-center mb-2'>
|
||||||
|
<div className='w-9.5 h-9.5 bg-primary rounded-lg border border-primary flex items-center justify-center shadow-[inset_0px_4px_4px_0px_#FFFFFF80,inset_0px_2px_0px_0px_#FFFFFF80]'>
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:chart-bar'
|
icon='heroicons:chart-bar'
|
||||||
className='text-white'
|
className='text-white'
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Empty state text */}
|
{/* Empty state text */}
|
||||||
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
<h3 className='text-base-content/50 font-semibold text-sm mb-1'>
|
||||||
Data Not Yet Available
|
Data Not Yet Available
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
<p className='text-base-content/50 text-xs text-center max-w-xs'>
|
||||||
Please change your filters to get the data.
|
Please change your filters to get the data.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const CARD_CONFIG = [
|
|||||||
key: 'Avg. Selling Price',
|
key: 'Avg. Selling Price',
|
||||||
icon: 'heroicons:document-currency-dollar',
|
icon: 'heroicons:document-currency-dollar',
|
||||||
alertColor: 'success' as const,
|
alertColor: 'success' as const,
|
||||||
suffix: ' /Kg',
|
suffix: ' /Kg Telur',
|
||||||
prefix: '',
|
prefix: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ const DashboardStats = ({ data }: DashboardStatsProps) => {
|
|||||||
icon: isPositive
|
icon: isPositive
|
||||||
? 'heroicons:arrow-trending-up'
|
? 'heroicons:arrow-trending-up'
|
||||||
: 'heroicons:arrow-trending-down',
|
: 'heroicons:arrow-trending-down',
|
||||||
color: isPositive ? 'text-success' : 'text-error',
|
color: isPositive ? 'text-[#008000]' : 'text-[#FF3A3A]',
|
||||||
value: Math.abs(percent),
|
value: Math.abs(percent),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -60,14 +60,16 @@ const DashboardStats = ({ data }: DashboardStatsProps) => {
|
|||||||
{prefix}
|
{prefix}
|
||||||
{formatNumber(value)}
|
{formatNumber(value)}
|
||||||
{suffix && (
|
{suffix && (
|
||||||
<span className='text-sm font-normal text-neutral-500'>{suffix}</span>
|
<span className='text-sm font-normal text-base-content/50'>
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid sm:grid-cols-2 xl:grid-cols-4 gap-6'>
|
<div className='grid sm:grid-cols-2 xl:grid-cols-4 gap-3'>
|
||||||
{CARD_CONFIG.map((config) => {
|
{CARD_CONFIG.map((config) => {
|
||||||
// Find matching data from API
|
// Find matching data from API
|
||||||
const cardData = data.find((item) => item.label === config.key);
|
const cardData = data.find((item) => item.label === config.key);
|
||||||
@@ -78,35 +80,41 @@ const DashboardStats = ({ data }: DashboardStatsProps) => {
|
|||||||
<Card
|
<Card
|
||||||
key={config.key}
|
key={config.key}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full rounded-lg',
|
wrapper: 'w-full rounded-xl border border-base-content/10',
|
||||||
body: 'p-0',
|
body: 'p-0',
|
||||||
|
wrapperContent:
|
||||||
|
'h-full flex flex-col items-between justify-between',
|
||||||
|
footer: 'mt-0!',
|
||||||
}}
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
footer={
|
footer={
|
||||||
<div className='flex flex-row justify-between px-4 pb-4'>
|
<div className='flex flex-row justify-between px-4 pb-4 max-h-12'>
|
||||||
<div className='text-neutral-400 font-semibold text-sm'>
|
<div className='text-base-content/50 font-semibold text-xs'>
|
||||||
From last month
|
From last month
|
||||||
</div>
|
</div>
|
||||||
<div className='text-neutral-400 font-semibold text-sm'>
|
<div className='text-base-content/50 font-semibold text-xs'>
|
||||||
Filter Required
|
Filter Required
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center gap-4 px-4 pt-4'>
|
<div className='flex flex-row items-center gap-3 px-4 py-4'>
|
||||||
<Alert variant='soft' className='rounded-lg p-3 bg-neutral-100'>
|
<Alert
|
||||||
|
variant='soft'
|
||||||
|
className={`rounded-lg p-0 w-12.5 h-12.5 bg-[${config.alertColor}]/12 flex items-center justify-center`}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={config.icon}
|
icon={config.icon}
|
||||||
width={32}
|
width={24}
|
||||||
height={32}
|
height={24}
|
||||||
className='text-neutral-400'
|
className='text-base-content/50'
|
||||||
/>
|
/>
|
||||||
</Alert>
|
</Alert>
|
||||||
<div>
|
<div>
|
||||||
<h3 className='text-neutral-400 font-semibold text-sm'>
|
<h3 className='text-base-content/50 font-semibold text-sm'>
|
||||||
{config.key}
|
{config.key}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-2xl font-semibold text-neutral-400'>
|
<p className='text-xl font-semibold text-base-content/50'>
|
||||||
********
|
********
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,17 +129,20 @@ const DashboardStats = ({ data }: DashboardStatsProps) => {
|
|||||||
<Card
|
<Card
|
||||||
key={config.key}
|
key={config.key}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full rounded-lg',
|
wrapper: 'w-full rounded-xl border border-base-content/10',
|
||||||
body: 'p-0',
|
body: 'p-0',
|
||||||
|
wrapperContent:
|
||||||
|
'h-full flex flex-col items-between justify-between',
|
||||||
|
footer: 'mt-0!',
|
||||||
}}
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
footer={
|
footer={
|
||||||
<div className='flex flex-row justify-between px-4 pb-4'>
|
<div className='flex flex-row justify-between px-4 pb-4'>
|
||||||
<div className='text-neutral-500 font-semibold text-sm'>
|
<div className='text-base-content/50 font-semibold text-xs'>
|
||||||
From last month
|
From last month
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`${trend.color} font-semibold flex flex-row items-center gap-1 text-sm`}
|
className={`${trend.color} font-semibold flex flex-row items-center gap-2 text-xs`}
|
||||||
>
|
>
|
||||||
<Icon icon={trend.icon} width={16} height={16} />
|
<Icon icon={trend.icon} width={16} height={16} />
|
||||||
{trend.value}%
|
{trend.value}%
|
||||||
@@ -143,15 +154,15 @@ const DashboardStats = ({ data }: DashboardStatsProps) => {
|
|||||||
<Alert
|
<Alert
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={config.alertColor}
|
color={config.alertColor}
|
||||||
className='rounded-lg p-3'
|
className={`rounded-lg p-3 bg-[${config.alertColor}]/12 flex items-center justify-center`}
|
||||||
>
|
>
|
||||||
<Icon icon={config.icon} width={32} height={32} />
|
<Icon icon={config.icon} width={24} height={24} />
|
||||||
</Alert>
|
</Alert>
|
||||||
<div>
|
<div className='space-y-1'>
|
||||||
<h3 className='text-neutral-500 font-semibold text-sm'>
|
<h3 className='text-base-content/50 font-semibold text-sm'>
|
||||||
{cardData.label}
|
{cardData.label}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-2xl font-semibold'>
|
<p className='text-xl font-semibold'>
|
||||||
{formatValue(cardData.value, config.prefix, config.suffix)}
|
{formatValue(cardData.value, config.prefix, config.suffix)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+9
-8
@@ -17,12 +17,12 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
type DashboardAllChartsProps = {
|
type DashboardExportChartsProps = {
|
||||||
data: Dashboard;
|
data: Dashboard;
|
||||||
analysisMode: string;
|
analysisMode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardAllChartsRef = {
|
export type DashboardExportChartsRef = {
|
||||||
getChartRefs: () => {
|
getChartRefs: () => {
|
||||||
key: string;
|
key: string;
|
||||||
ref: HTMLDivElement | null;
|
ref: HTMLDivElement | null;
|
||||||
@@ -99,9 +99,9 @@ const chartTypeLabels: Record<keyof DashboardOverviewCharts, string> = {
|
|||||||
deplesi: 'Deplesi',
|
deplesi: 'Deplesi',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DashboardAllCharts = forwardRef<
|
const DashboardExportCharts = forwardRef<
|
||||||
DashboardAllChartsRef,
|
DashboardExportChartsRef,
|
||||||
DashboardAllChartsProps
|
DashboardExportChartsProps
|
||||||
>(({ data, analysisMode }, ref) => {
|
>(({ data, analysisMode }, ref) => {
|
||||||
// Create refs for charts - use string keys for flexibility
|
// Create refs for charts - use string keys for flexibility
|
||||||
const chartRefs = useRef<{
|
const chartRefs = useRef<{
|
||||||
@@ -189,7 +189,8 @@ const DashboardAllCharts = forwardRef<
|
|||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full rounded-lg',
|
wrapper: 'w-full rounded-lg p-0',
|
||||||
|
body: 'p-4',
|
||||||
}}
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
>
|
>
|
||||||
@@ -338,6 +339,6 @@ const DashboardAllCharts = forwardRef<
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
DashboardAllCharts.displayName = 'DashboardAllCharts';
|
DashboardExportCharts.displayName = 'DashboardExportCharts';
|
||||||
|
|
||||||
export default DashboardAllCharts;
|
export default DashboardExportCharts;
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import Alert from '@/components/Alert';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
import { DashboardStatisticsData } from '@/types/api/dashboard/dashboard';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
interface DashboardStatsProps {
|
||||||
|
data: DashboardStatisticsData[];
|
||||||
|
}
|
||||||
|
export type DashboardExportStatsRef = {
|
||||||
|
getStatsRefs: () => {
|
||||||
|
key: string;
|
||||||
|
ref: HTMLDivElement | null;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
getContainerRef: () => HTMLDivElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Konfigurasi untuk setiap kartu
|
||||||
|
const CARD_CONFIG = [
|
||||||
|
{
|
||||||
|
key: 'HPP Global',
|
||||||
|
icon: 'heroicons:banknotes',
|
||||||
|
alertColor: 'warning' as const,
|
||||||
|
suffix: ' /Kg',
|
||||||
|
prefix: 'RP ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Avg. Selling Price',
|
||||||
|
icon: 'heroicons:document-currency-dollar',
|
||||||
|
alertColor: 'success' as const,
|
||||||
|
suffix: ' /Kg',
|
||||||
|
prefix: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'FCR',
|
||||||
|
icon: 'heroicons:clipboard-document-list',
|
||||||
|
alertColor: 'info' as const,
|
||||||
|
suffix: '',
|
||||||
|
prefix: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Mortality',
|
||||||
|
icon: 'heroicons:exclamation-triangle',
|
||||||
|
alertColor: 'error' as const,
|
||||||
|
suffix: ' %',
|
||||||
|
prefix: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DashboardExportStats = forwardRef<
|
||||||
|
DashboardExportStatsRef,
|
||||||
|
DashboardStatsProps
|
||||||
|
>(({ data }, ref) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// Helper to get trend icon and color
|
||||||
|
const getTrendDisplay = (percent: number) => {
|
||||||
|
const isPositive = percent >= 0;
|
||||||
|
return {
|
||||||
|
icon: isPositive
|
||||||
|
? 'heroicons:arrow-trending-up'
|
||||||
|
: 'heroicons:arrow-trending-down',
|
||||||
|
color: isPositive ? 'text-[#008000]' : 'text-[#FF3A3A]',
|
||||||
|
value: Math.abs(percent),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to format value
|
||||||
|
const formatValue = (value: number, prefix: string, suffix: string) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{prefix}
|
||||||
|
{formatNumber(value)}
|
||||||
|
{suffix && (
|
||||||
|
<span className='text-sm font-normal text-base-content/50'>
|
||||||
|
{suffix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose container ref through imperative handle
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getStatsRefs: () => [],
|
||||||
|
getContainerRef: () => containerRef.current,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className='grid grid-cols-4 gap-3'>
|
||||||
|
{CARD_CONFIG.map((config) => {
|
||||||
|
// Find matching data from API
|
||||||
|
const cardData = data.find((item) => item.label === config.key);
|
||||||
|
|
||||||
|
if (!cardData) {
|
||||||
|
// Show placeholder card for missing data (FCR & Mortality)
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={config.key}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full rounded-lg',
|
||||||
|
body: 'p-0',
|
||||||
|
wrapperContent:
|
||||||
|
'h-full flex flex-col items-between justify-between',
|
||||||
|
footer: 'mt-0!',
|
||||||
|
}}
|
||||||
|
variant='bordered'
|
||||||
|
footer={
|
||||||
|
<div className='flex flex-row justify-between px-4 pb-4 max-h-12'>
|
||||||
|
<div className='text-base-content/50 font-semibold text-xs'>
|
||||||
|
From last month
|
||||||
|
</div>
|
||||||
|
<div className='text-base-content/50 font-semibold text-xs'>
|
||||||
|
Filter Required
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-3 px-4 pt-4'>
|
||||||
|
<Alert
|
||||||
|
variant='soft'
|
||||||
|
className={`rounded-lg p-0 w-12.5 h-12.5 bg-[${config.alertColor}]/12 flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={config.icon}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='text-base-content/50'
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
<div>
|
||||||
|
<h3 className='text-base-content/50 font-semibold text-sm'>
|
||||||
|
{config.key}
|
||||||
|
</h3>
|
||||||
|
<p className='text-xl font-semibold text-base-content/50'>
|
||||||
|
********
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trend = getTrendDisplay(cardData.percent_last_month);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={config.key}
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full rounded-lg border border-base-content/10',
|
||||||
|
body: 'p-0',
|
||||||
|
wrapperContent:
|
||||||
|
'h-full flex flex-col items-between justify-between',
|
||||||
|
footer: 'mt-0!',
|
||||||
|
}}
|
||||||
|
variant='bordered'
|
||||||
|
footer={
|
||||||
|
<div className='flex flex-row justify-between px-4 pb-4 max-h-12'>
|
||||||
|
<div className='text-base-content/50 font-semibold text-xs'>
|
||||||
|
From last month
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${trend.color} font-semibold flex flex-row items-center gap-2 text-xs`}
|
||||||
|
>
|
||||||
|
<Icon icon={trend.icon} width={16} height={16} />
|
||||||
|
{trend.value}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='flex flex-row items-center gap-4 px-4 pt-4'>
|
||||||
|
<Alert
|
||||||
|
variant='soft'
|
||||||
|
color={config.alertColor}
|
||||||
|
className={`rounded-lg p-0 w-12.5 h-12.5 bg-[${config.alertColor}]/12 flex items-center justify-center`}
|
||||||
|
>
|
||||||
|
<Icon icon={config.icon} width={24} height={24} />
|
||||||
|
</Alert>
|
||||||
|
<div>
|
||||||
|
<h3 className='text-base-content/50 font-semibold text-sm'>
|
||||||
|
{cardData.label}
|
||||||
|
</h3>
|
||||||
|
<p className='text-xl font-semibold'>
|
||||||
|
{formatValue(cardData.value, config.prefix, config.suffix)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DashboardExportStats.displayName = 'DashboardExportStats';
|
||||||
|
|
||||||
|
export default DashboardExportStats;
|
||||||
@@ -3,18 +3,19 @@ import { toPng } from 'html-to-image';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { formatDate } from '@/lib/helper';
|
import { formatDate } from '@/lib/helper';
|
||||||
import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
import { DashboardAllChartsRef } from '@/components/pages/dashboard/chart/DashboardAllCharts';
|
import { DashboardExportChartsRef } from '@/components/pages/dashboard/export/DashboardExportCharts';
|
||||||
|
import { DashboardExportStatsRef } from '@/components/pages/dashboard/export/DashboardExportStats';
|
||||||
|
|
||||||
interface DashboardPDFExportParams {
|
interface DashboardPDFExportParams {
|
||||||
filterValues: DashboardFilterType;
|
filterValues: DashboardFilterType;
|
||||||
statsRef: React.RefObject<HTMLDivElement | null>;
|
allStatsRef: React.RefObject<DashboardExportStatsRef | null>;
|
||||||
allChartsRef: React.RefObject<DashboardAllChartsRef | null>;
|
allChartsRef: React.RefObject<DashboardExportChartsRef | null>;
|
||||||
setExporting: (value: boolean) => void;
|
setExporting: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateDashboardPDF = async ({
|
export const generateDashboardPDF = async ({
|
||||||
filterValues,
|
filterValues,
|
||||||
statsRef,
|
allStatsRef,
|
||||||
allChartsRef,
|
allChartsRef,
|
||||||
setExporting,
|
setExporting,
|
||||||
}: DashboardPDFExportParams): Promise<void> => {
|
}: DashboardPDFExportParams): Promise<void> => {
|
||||||
@@ -168,8 +169,10 @@ export const generateDashboardPDF = async ({
|
|||||||
yPosition += 10;
|
yPosition += 10;
|
||||||
|
|
||||||
// Capture and add stats if available
|
// Capture and add stats if available
|
||||||
if (statsRef.current) {
|
if (allStatsRef.current) {
|
||||||
const statsImage = await toPng(statsRef.current, {
|
const statsContainer = allStatsRef.current.getContainerRef();
|
||||||
|
if (statsContainer) {
|
||||||
|
const statsImage = await toPng(statsContainer, {
|
||||||
quality: 1,
|
quality: 1,
|
||||||
pixelRatio: 2,
|
pixelRatio: 2,
|
||||||
});
|
});
|
||||||
@@ -194,6 +197,7 @@ export const generateDashboardPDF = async ({
|
|||||||
);
|
);
|
||||||
yPosition += statsHeight + 10;
|
yPosition += statsHeight + 10;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (allChartsRef.current) {
|
if (allChartsRef.current) {
|
||||||
// Get all individual chart refs
|
// Get all individual chart refs
|
||||||
|
|||||||
@@ -3,53 +3,45 @@ import { DashboardMeta } from '@/types/api/dashboard/dashboard';
|
|||||||
|
|
||||||
const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => {
|
const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full bg-white rounded-lg shadow-sm border border-gray-200 p-6 relative'>
|
<div className='w-full bg-white rounded-xl border border-base-content/10 p-4 relative'>
|
||||||
{/* Header with title skeleton */}
|
{/* Header with title skeleton */}
|
||||||
<div className='text-lg font-semibold'>
|
<div className='text-base font-semibold'>
|
||||||
Performance{' '}
|
Performance{' '}
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:information-circle'
|
icon='heroicons:information-circle'
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className='inline text-neutral-500'
|
className='inline text-base-content/50'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart area with axes skeleton */}
|
{/* Chart area with axes skeleton */}
|
||||||
<div className='relative mt-6 '>
|
<div className='relative mt-6 '>
|
||||||
{/* Main chart container */}
|
|
||||||
<div className='flex gap-4'>
|
|
||||||
{/* Y-axis skeleton (left side) */}
|
|
||||||
<div className='flex flex-col justify-between py-4 space-y-4'>
|
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
|
||||||
<div
|
|
||||||
key={item}
|
|
||||||
className='h-4 w-12 bg-gray-100 rounded animate-pulse'
|
|
||||||
></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart content area */}
|
{/* Chart content area */}
|
||||||
<div className='flex-1 relative'>
|
<div className='flex-1 relative'>
|
||||||
{/* Empty state centered in chart area */}
|
{/* Empty state centered in chart area */}
|
||||||
<div className='absolute inset-0 flex flex-col items-center justify-center pb-12'>
|
<div className='absolute inset-0 flex flex-col items-center justify-center pb-10'>
|
||||||
{!meta?.filters && (
|
{!meta?.filters && (
|
||||||
<>
|
<>
|
||||||
{/* Filter icon */}
|
{/* Filter icon */}
|
||||||
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
<div className='mb-2'>
|
||||||
|
<div className='w-12.5 h-12.5 bg-[var(--main-color-base-100,#FFFFFF)] border border-base-content/10 rounded-[0.875rem] border border-base-content shadow-[0px_25px_50px_-12px_#00000040] flex items-center justify-center'>
|
||||||
|
<div className='w-9.5 h-9.5 bg-primary rounded-lg border border-primary flex items-center justify-center shadow-[inset_0px_4px_4px_0px_#FFFFFF80,inset_0px_2px_0px_0px_#FFFFFF80]'>
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:funnel'
|
icon='heroicons:funnel'
|
||||||
className='text-white'
|
className='text-white'
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Empty state text */}
|
{/* Empty state text */}
|
||||||
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
<h3 className='text-base-content/50 font-semibold text-sm mb-1'>
|
||||||
No Filters Selected
|
No Filters Selected
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
<p className='text-base-content/50 text-xs text-center max-w-xs'>
|
||||||
Please choose filters to narrow down your results and make
|
Please choose filters to narrow down your results and make
|
||||||
your search easier.
|
your search easier.
|
||||||
</p>
|
</p>
|
||||||
@@ -58,38 +50,53 @@ const DashboardLineChartSkeleton = ({ meta }: { meta?: DashboardMeta }) => {
|
|||||||
{meta?.filters && (
|
{meta?.filters && (
|
||||||
<>
|
<>
|
||||||
{/* Filter icon */}
|
{/* Filter icon */}
|
||||||
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
<div className='w-12.5 h-12.5 bg-[var(--main-color-base-100,#FFFFFF)] border border-base-content/10 rounded-[0.875rem] border border-base-content shadow-[0px_25px_50px_-12px_#00000040] flex items-center justify-center mb-2'>
|
||||||
|
<div className='w-9.5 h-9.5 bg-primary rounded-lg border border-primary flex items-center justify-center shadow-[inset_0px_4px_4px_0px_#FFFFFF80,inset_0px_2px_0px_0px_#FFFFFF80]'>
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:chart-bar'
|
icon='heroicons:chart-bar'
|
||||||
className='text-white'
|
className='text-white'
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Empty state text */}
|
{/* Empty state text */}
|
||||||
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
<h3 className='text-base-content/50 font-semibold text-sm mb-1'>
|
||||||
Data Not Yet Available
|
Data Not Yet Available
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
<p className='text-base-content/50 text-xs text-center max-w-xs'>
|
||||||
Please change your filters to get the data.
|
Please change your filters to get the data.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Placeholder for chart height */}
|
<div className='flex flex-row w-full items-center gap-4'>
|
||||||
<div className='h-64'></div>
|
<div className='flex-1 h-full min-w-4'>
|
||||||
|
<div className='h-28.5 w-4 bg-base-content/4 rounded'></div>
|
||||||
|
</div>
|
||||||
|
<div className='w-full grid grid-cols-1 gap-y-13.25 mb-2'>
|
||||||
|
{[1, 2, 3, 4].map((item) => (
|
||||||
|
<div key={item} className='flex items-center w-full h-4 gap-4'>
|
||||||
|
<div className='h-4 w-6 bg-base-content/4 rounded'></div>
|
||||||
|
<div className='h-0.25 w-full bg-base-content/4 rounded'></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* X-axis skeleton (bottom) */}
|
{/* X-axis skeleton (bottom) */}
|
||||||
<div className='flex justify-between pt-4 border-t border-gray-100'>
|
<div className='grid grid-cols-10 gap-15 mt-4 ps-13 sm:ps-26 overflow-x-hidden'>
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item}
|
key={item}
|
||||||
className='h-4 w-8 bg-gray-100 rounded animate-pulse'
|
className='h-4 w-9.5 bg-base-content/4 rounded'
|
||||||
></div>
|
></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex justify-center pt-4 ps-13 sm:ps-26'>
|
||||||
|
<div className='h-4 w-28.5 bg-base-content/4 rounded'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -123,23 +123,61 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
.transform((value) =>
|
.transform((value) =>
|
||||||
isNaN(value) || value === '' || value === null ? undefined : value
|
isNaN(value) || value === '' || value === null ? undefined : value
|
||||||
)
|
)
|
||||||
|
.when('supplier_id', {
|
||||||
|
is: (supplier_id: number | null | undefined) =>
|
||||||
|
supplier_id !== null && supplier_id !== undefined && supplier_id > 0,
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.required('Biaya pengiriman wajib diisi!')
|
||||||
|
.min(1, 'Biaya minimal 1!')
|
||||||
|
.typeError('Biaya harus berupa angka!'),
|
||||||
|
otherwise: (schema) =>
|
||||||
|
schema
|
||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.min(1, 'Biaya minimal 1!')
|
.min(1, 'Biaya minimal 1!')
|
||||||
.typeError('Biaya harus berupa angka!'),
|
.typeError('Biaya harus berupa angka!'),
|
||||||
|
}),
|
||||||
delivery_cost_per_item: Yup.number()
|
delivery_cost_per_item: Yup.number()
|
||||||
.transform((value) =>
|
.transform((value) =>
|
||||||
isNaN(value) || value === '' || value === null ? undefined : value
|
isNaN(value) || value === '' || value === null ? undefined : value
|
||||||
)
|
)
|
||||||
|
.when('supplier_id', {
|
||||||
|
is: (supplier_id: number | null | undefined) =>
|
||||||
|
supplier_id !== null && supplier_id !== undefined && supplier_id > 0,
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.required('Biaya per item wajib diisi!')
|
||||||
|
.min(1, 'Biaya per item minimal 1!')
|
||||||
|
.typeError('Biaya per item harus berupa angka!'),
|
||||||
|
otherwise: (schema) =>
|
||||||
|
schema
|
||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.min(1, 'Biaya per item minimal 1!')
|
.min(1, 'Biaya per item minimal 1!')
|
||||||
.typeError('Biaya per item harus berupa angka!'),
|
.typeError('Biaya per item harus berupa angka!'),
|
||||||
|
}),
|
||||||
document_path: Yup.string().nullable().optional(),
|
document_path: Yup.string().nullable().optional(),
|
||||||
document_index: Yup.number().optional(),
|
document_index: Yup.number().optional(),
|
||||||
document: DeliveryDocumentSchema,
|
document: DeliveryDocumentSchema,
|
||||||
driver_name: Yup.string().optional().nullable(),
|
driver_name: Yup.string().when('supplier_id', {
|
||||||
vehicle_plate: Yup.string().optional().nullable(),
|
is: (supplier_id: number | null | undefined) =>
|
||||||
|
supplier_id !== null && supplier_id !== undefined && supplier_id > 0,
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.required('Nama sopir wajib diisi!')
|
||||||
|
.min(1, 'Nama sopir wajib diisi!'),
|
||||||
|
otherwise: (schema) => schema.optional().nullable(),
|
||||||
|
}),
|
||||||
|
vehicle_plate: Yup.string().when('supplier_id', {
|
||||||
|
is: (supplier_id: number | null | undefined) =>
|
||||||
|
supplier_id !== null && supplier_id !== undefined && supplier_id > 0,
|
||||||
|
then: (schema) =>
|
||||||
|
schema
|
||||||
|
.required('Plat nomor wajib diisi!')
|
||||||
|
.min(1, 'Plat nomor wajib diisi!'),
|
||||||
|
otherwise: (schema) => schema.optional().nullable(),
|
||||||
|
}),
|
||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
|
|||||||
@@ -984,6 +984,28 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[formik.values.deliveries, formik.values.products, type]
|
[formik.values.deliveries, formik.values.products, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSupplierSelected = useCallback(
|
||||||
|
(deliveryIdx: number) => {
|
||||||
|
const delivery = formik.values.deliveries?.[deliveryIdx];
|
||||||
|
return (
|
||||||
|
delivery &&
|
||||||
|
delivery.supplier_id !== null &&
|
||||||
|
delivery.supplier_id !== undefined &&
|
||||||
|
delivery.supplier_id > 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[formik.values.deliveries]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasAnySupplierSelected = useMemo(() => {
|
||||||
|
return formik.values.deliveries?.some(
|
||||||
|
(delivery) =>
|
||||||
|
delivery.supplier_id !== null &&
|
||||||
|
delivery.supplier_id !== undefined &&
|
||||||
|
delivery.supplier_id > 0
|
||||||
|
);
|
||||||
|
}, [formik.values.deliveries]);
|
||||||
|
|
||||||
// ===== COMPUTED VALUES =====
|
// ===== COMPUTED VALUES =====
|
||||||
const invalidQtyRows = useMemo(
|
const invalidQtyRows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -1659,12 +1681,62 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
<th>Supplier</th>
|
<th>
|
||||||
<th>Plat Nomor</th>
|
Supplier
|
||||||
|
{hasAnySupplierSelected && (
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-9999'
|
||||||
|
data-tip='required jika supplier dipilih'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Plat Nomor
|
||||||
|
{hasAnySupplierSelected && (
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-9999'
|
||||||
|
data-tip='required jika supplier dipilih'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
<th>Dokumen</th>
|
<th>Dokumen</th>
|
||||||
<th>Biaya Pengiriman (Rp.)</th>
|
<th>
|
||||||
<th>Biaya Per Item (Rp.)</th>
|
Biaya Pengiriman (Rp.)
|
||||||
<th>Nama Sopir</th>
|
{hasAnySupplierSelected && (
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-9999'
|
||||||
|
data-tip='required jika supplier dipilih'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Biaya Per Item (Rp.)
|
||||||
|
{hasAnySupplierSelected && (
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-9999'
|
||||||
|
data-tip='required jika supplier dipilih'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Nama Sopir
|
||||||
|
{hasAnySupplierSelected && (
|
||||||
|
<span
|
||||||
|
className='tooltip tooltip-error tooltip-bottom z-9999'
|
||||||
|
data-tip='required jika supplier dipilih'
|
||||||
|
>
|
||||||
|
<span className='text-error'>*</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
{type !== 'detail' && <th>Aksi</th>}
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1772,6 +1844,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
idx
|
idx
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
required={isSupplierSelected(idx)}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
}}
|
}}
|
||||||
@@ -1867,6 +1940,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
idx
|
idx
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
required={isSupplierSelected(idx)}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
}}
|
}}
|
||||||
@@ -1890,6 +1964,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
idx
|
idx
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
required={isSupplierSelected(idx)}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
}}
|
}}
|
||||||
@@ -1908,6 +1983,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
idx
|
idx
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
|
required={isSupplierSelected(idx)}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { StateCreator } from 'zustand';
|
||||||
|
import { UIStore } from '@/types/stores';
|
||||||
|
|
||||||
|
type NavbarActionsSlice = {
|
||||||
|
navbarActions: ReactNode | null;
|
||||||
|
setNavbarActions: (actions: ReactNode) => void;
|
||||||
|
clearNavbarActions: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNavbarActionsSlice: StateCreator<
|
||||||
|
UIStore,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
NavbarActionsSlice
|
||||||
|
> = (set) => ({
|
||||||
|
navbarActions: null,
|
||||||
|
setNavbarActions: (actions) => set({ navbarActions: actions }),
|
||||||
|
clearNavbarActions: () => set({ navbarActions: null }),
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ import { UIStore } from '@/types/stores';
|
|||||||
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
import { createMainUiSlice } from '@/stores/ui/slices/main.slice';
|
||||||
import { createDrawerUISlice } from '@/stores/ui/slices/drawer.slice';
|
import { createDrawerUISlice } from '@/stores/ui/slices/drawer.slice';
|
||||||
import { createTableUISlice } from '@/stores/ui/slices/table.slice';
|
import { createTableUISlice } from '@/stores/ui/slices/table.slice';
|
||||||
|
import { createNavbarActionsSlice } from '@/stores/ui/slices/navbar.slice';
|
||||||
|
|
||||||
export const useUiStore = create<UIStore>()(
|
export const useUiStore = create<UIStore>()(
|
||||||
devtools(
|
devtools(
|
||||||
@@ -15,6 +16,7 @@ export const useUiStore = create<UIStore>()(
|
|||||||
...createMainUiSlice(...args),
|
...createMainUiSlice(...args),
|
||||||
...createDrawerUISlice(...args),
|
...createDrawerUISlice(...args),
|
||||||
...createTableUISlice(...args),
|
...createTableUISlice(...args),
|
||||||
|
...createNavbarActionsSlice(...args),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'ui-cache',
|
name: 'ui-cache',
|
||||||
|
|||||||
Vendored
+11
-1
@@ -32,7 +32,17 @@ type TableUISlice = {
|
|||||||
resetSearchValue: () => void;
|
resetSearchValue: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIStore = MainUiSlice & DrawerUISlice & TableUISlice;
|
// Navbar Actions Slice
|
||||||
|
type NavbarActionsSlice = {
|
||||||
|
navbarActions: ReactNode | null;
|
||||||
|
setNavbarActions: (actions: ReactNode) => void;
|
||||||
|
clearNavbarActions: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UIStore = MainUiSlice &
|
||||||
|
DrawerUISlice &
|
||||||
|
TableUISlice &
|
||||||
|
NavbarActionsSlice;
|
||||||
|
|
||||||
type ProductionStandardFormSlice = {
|
type ProductionStandardFormSlice = {
|
||||||
formData: {
|
formData: {
|
||||||
|
|||||||
Reference in New Issue
Block a user