Enhance farm dashboard components by refactoring to accept data props, improving API integration for dynamic data rendering. Updated FarmDashboardWrapper to fetch and manage card data, ensuring components like EconomicOverview, AnomalyDetectionCard, and others utilize the new data structure. Removed hardcoded values and added error handling for better resilience.

This commit is contained in:
2026-02-19 16:58:30 +03:30
parent 9f1de2166c
commit 51175ffac2
18 changed files with 333 additions and 313 deletions
File diff suppressed because one or more lines are too long
+41 -3
View File
@@ -1,11 +1,13 @@
/** /**
* Farm Dashboard Config Service * Farm Dashboard Service
* Handles API calls for dashboard customization (disabled cards, row order). * Handles API calls for dashboard config and card data.
* Authenticated user required. * - Config: disabled cards, row order, drag reorder
* - Cards: all 15 card payloads from /api/farm-dashboard/
*/ */
import { apiClient } from '../client' import { apiClient } from '../client'
import type { FarmDashboardConfig } from '@/views/dashboards/farm/farmDashboardConfig' import type { FarmDashboardConfig } from '@/views/dashboards/farm/farmDashboardConfig'
import type { CardId } from '@/views/dashboards/farm/farmDashboardConfig'
export interface ApiResponse<T> { export interface ApiResponse<T> {
code: number code: number
@@ -19,6 +21,25 @@ export interface FarmDashboardConfigResponse {
enable_drag_reorder?: boolean enable_drag_reorder?: boolean
} }
/** API response shape for /api/farm-dashboard/ - each key matches CardId */
export interface FarmDashboardCardsResponse {
farmOverviewKpis?: Record<string, unknown>
farmWeatherCard?: Record<string, unknown>
farmAlertsTracker?: Record<string, unknown>
sensorValuesList?: Record<string, unknown>
sensorRadarChart?: Record<string, unknown>
sensorComparisonChart?: Record<string, unknown>
anomalyDetectionCard?: Record<string, unknown>
farmAlertsTimeline?: Record<string, unknown>
waterNeedPrediction?: Record<string, unknown>
harvestPredictionCard?: Record<string, unknown>
yieldPredictionChart?: Record<string, unknown>
soilMoistureHeatmap?: Record<string, unknown>
ndviHealthCard?: Record<string, unknown>
recommendationsList?: Record<string, unknown>
economicOverview?: Record<string, unknown>
}
const STORAGE_KEY = 'farm_dashboard_config' const STORAGE_KEY = 'farm_dashboard_config'
/** /**
@@ -114,5 +135,22 @@ export const farmDashboardService = {
} }
throw err throw err
} }
},
/**
* Get all dashboard card data from API
* Response: { code: 200, msg: "OK", data: { farmOverviewKpis, farmWeatherCard, ... } }
*/
async getAllCards(): Promise<Partial<Record<CardId, Record<string, unknown>>>> {
try {
const response = await apiClient.get<ApiResponse<FarmDashboardCardsResponse>>('/api/farm-dashboard/')
const raw = response?.data ?? response
if (raw && typeof raw === 'object') {
return raw as Partial<Record<CardId, Record<string, unknown>>>
}
return {}
} catch {
return {}
}
} }
} }
@@ -18,12 +18,13 @@ type AnomalyItem = {
severity: 'warning' | 'error' severity: 'warning' | 'error'
} }
const anomalies: AnomalyItem[] = [ interface AnomalyDetectionCardProps {
{ sensor: 'Soil Moisture Z3', value: '38%', expected: '45-65%', deviation: '-12%', severity: 'warning' }, data?: Record<string, unknown>
{ sensor: 'pH Sector 2', value: '5.2', expected: '6.0-7.0', deviation: '-0.8', severity: 'error' } }
]
const AnomalyDetectionCard = ({ data }: AnomalyDetectionCardProps) => {
const anomalies = (data?.anomalies as AnomalyItem[] | undefined) ?? []
const AnomalyDetectionCard = () => {
return ( return (
<Card> <Card>
<CardHeader <CardHeader
+13 -15
View File
@@ -22,12 +22,6 @@ import CustomAvatar from '@core/components/mui/Avatar'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Vars - Cost breakdown (stacked bar style)
const series = [
{ name: 'Water Cost', data: [120, 115, 110, 125, 118, 122] },
{ name: 'Fertilizer', data: [80, 85, 90, 75, 82, 78] }
]
type EconomicItem = { type EconomicItem = {
title: string title: string
value: string value: string
@@ -36,14 +30,14 @@ type EconomicItem = {
avatarColor: 'primary' | 'success' | 'info' | 'warning' avatarColor: 'primary' | 'success' | 'info' | 'warning'
} }
const economicData: EconomicItem[] = [ interface EconomicOverviewProps {
{ title: 'Water Cost', value: '€720', subtitle: 'This month', avatarIcon: 'tabler-droplet', avatarColor: 'primary' }, data?: Record<string, unknown>
{ title: 'AI Water Savings', value: '€156', subtitle: '18% saved', avatarIcon: 'tabler-bulb', avatarColor: 'success' }, }
{ title: 'Platform ROI', value: '127%', subtitle: 'vs last year', avatarIcon: 'tabler-chart-line', avatarColor: 'info' },
{ title: 'Income Forecast', value: '€42k', subtitle: 'This season', avatarIcon: 'tabler-currency-euro', avatarColor: 'success' }
]
const EconomicOverview = () => { const EconomicOverview = ({ data }: EconomicOverviewProps) => {
const economicData = (data?.economicData as EconomicItem[] | undefined) ?? []
const chartSeries = (data?.chartSeries as Array<{ name: string; data: number[] }>) ?? []
const chartCategories = (data?.chartCategories as string[]) ?? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
const theme = useTheme() const theme = useTheme()
const options: ApexOptions = { const options: ApexOptions = {
@@ -69,7 +63,7 @@ const EconomicOverview = () => {
padding: { top: -40, left: -10, right: 0, bottom: -15 } padding: { top: -40, left: -10, right: 0, bottom: -15 }
}, },
xaxis: { xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], categories: chartCategories,
labels: { show: false }, labels: { show: false },
axisTicks: { show: false }, axisTicks: { show: false },
axisBorder: { show: false } axisBorder: { show: false }
@@ -85,6 +79,7 @@ const EconomicOverview = () => {
action={<OptionMenu options={['Export PDF', 'Export Excel', 'Details']} />} action={<OptionMenu options={['Export PDF', 'Export Excel', 'Details']} />}
/> />
<CardContent className='flex flex-col gap-4'> <CardContent className='flex flex-col gap-4'>
{economicData.length > 0 && (
<Grid container spacing={4}> <Grid container spacing={4}>
{economicData.map((item, index) => ( {economicData.map((item, index) => (
<Grid size={{ xs: 12, sm: 6 }} key={index}> <Grid size={{ xs: 12, sm: 6 }} key={index}>
@@ -105,7 +100,10 @@ const EconomicOverview = () => {
</Grid> </Grid>
))} ))}
</Grid> </Grid>
<AppReactApexCharts type='bar' height={180} width='100%' series={series} options={options} /> )}
{chartSeries.length > 0 && (
<AppReactApexCharts type='bar' height={180} width='100%' series={chartSeries} options={options} />
)}
</CardContent> </CardContent>
</Card> </Card>
) )
@@ -34,36 +34,13 @@ type AlertItem = {
color: 'primary' | 'warning' | 'error' | 'info' | 'success' color: 'primary' | 'warning' | 'error' | 'info' | 'success'
} }
const alerts: AlertItem[] = [ interface FarmAlertsTimelineProps {
{ data?: Record<string, unknown>
title: 'Water Shortage Risk',
description:
'Soil moisture at 10cm depth (42%) is below optimal. AI predicts stress in 2-3 days if no irrigation. Recommended: irrigate within 24h.',
time: '15 min ago',
color: 'warning'
},
{
title: 'Fungal Disease Risk',
description:
'High humidity (65%) + temp 24°C creates favorable conditions for fungal growth. Consider preventive fungicide or reduce irrigation.',
time: '1 hour ago',
color: 'error'
},
{
title: 'Irrigation Suggestion',
description: 'Optimal watering window: 6:00-8:00 AM. Suggested amount: 450 m³ for Zone A. Expected efficiency gain: 12%.',
time: '2 hours ago',
color: 'info'
},
{
title: 'Soil Salinity Check',
description: 'EC reading 1.2 dS/m is within range. No action needed. Next check recommended in 5 days.',
time: '4 hours ago',
color: 'success'
} }
]
const FarmAlertsTimeline = () => { const FarmAlertsTimeline = ({ data }: FarmAlertsTimelineProps) => {
const alerts = (data?.alerts as AlertItem[] | undefined) ?? []
return ( return (
<Card> <Card>
<CardHeader <CardHeader
+19 -12
View File
@@ -26,18 +26,19 @@ const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexChart
type AlertStatType = { type AlertStatType = {
title: string title: string
subtitle: string count: string
avatarIcon: string avatarIcon: string
avatarColor?: ThemeColor avatarColor?: ThemeColor
} }
const data: AlertStatType[] = [ interface FarmAlertsTrackerProps {
{ title: 'Water Shortage', subtitle: '2', avatarColor: 'error', avatarIcon: 'tabler-droplet-half-2' }, data?: Record<string, unknown>
{ title: 'Fungal Risk', subtitle: '1', avatarColor: 'warning', avatarIcon: 'tabler-mushroom' }, }
{ title: 'Frost Alert', subtitle: '0', avatarColor: 'info', avatarIcon: 'tabler-snowflake' }
]
const FarmAlertsTracker = () => { const FarmAlertsTracker = ({ data }: FarmAlertsTrackerProps) => {
const alertStats = (data?.alertStats as AlertStatType[] | undefined) ?? []
const totalAlerts = (data?.totalAlerts as number | undefined) ?? 0
const radialBarValue = (data?.radialBarValue as number | undefined) ?? 30
const theme = useTheme() const theme = useTheme()
const disabledText = 'var(--mui-palette-text-disabled)' const disabledText = 'var(--mui-palette-text-disabled)'
@@ -77,7 +78,7 @@ const FarmAlertsTracker = () => {
value: { value: {
offsetY: 8, offsetY: 8,
fontWeight: 500, fontWeight: 500,
formatter: () => '3', formatter: () => String(totalAlerts),
color: 'var(--mui-palette-text-primary)', color: 'var(--mui-palette-text-primary)',
fontFamily: theme.typography.fontFamily, fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.h2.fontSize as string fontSize: theme.typography.h2.fontSize as string
@@ -100,11 +101,11 @@ const FarmAlertsTracker = () => {
<CardContent className='flex flex-col sm:flex-row items-center justify-between gap-7'> <CardContent className='flex flex-col sm:flex-row items-center justify-between gap-7'>
<div className='flex flex-col gap-6 is-full sm:is-[unset]'> <div className='flex flex-col gap-6 is-full sm:is-[unset]'>
<div className='flex flex-col'> <div className='flex flex-col'>
<Typography variant='h2'>3</Typography> <Typography variant='h2'>{totalAlerts}</Typography>
<Typography>Total Alerts</Typography> <Typography>Total Alerts</Typography>
</div> </div>
<div className='flex flex-col gap-4 is-full'> <div className='flex flex-col gap-4 is-full'>
{data.map((item, index) => ( {alertStats.map((item, index) => (
<div key={index} className='flex items-center gap-4'> <div key={index} className='flex items-center gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={34}> <CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={34}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} /> <i className={classnames(item.avatarIcon, 'text-[22px]')} />
@@ -113,13 +114,19 @@ const FarmAlertsTracker = () => {
<Typography className='font-medium' color='text.primary'> <Typography className='font-medium' color='text.primary'>
{item.title} {item.title}
</Typography> </Typography>
<Typography variant='body2'>{item.subtitle}</Typography> <Typography variant='body2'>{item.count}</Typography>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<AppReactApexCharts type='radialBar' height={350} width='100%' series={[30]} options={options} /> <AppReactApexCharts
type='radialBar'
height={350}
width='100%'
series={[radialBarValue]}
options={options}
/>
</CardContent> </CardContent>
</Card> </Card>
) )
@@ -91,13 +91,18 @@ function mergeRowOrderAfterDrag(
const FarmDashboardWrapper = () => { const FarmDashboardWrapper = () => {
const { setSlotContent } = useContext(NavbarSlotContext) const { setSlotContent } = useContext(NavbarSlotContext)
const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG) const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG)
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const disabledSet = new Set(config.disabledCardIds) const disabledSet = new Set(config.disabledCardIds)
const hasVisibleCard = useCallback( const hasVisibleCard = useCallback(
(rowId: RowId) => ROW_CARDS[rowId].some(cardId => !disabledSet.has(cardId)), (rowId: string) => {
const cards = ROW_CARDS[rowId as RowId]
if (!Array.isArray(cards)) return false
return cards.some(cardId => !disabledSet.has(cardId))
},
[config.disabledCardIds] [config.disabledCardIds]
) )
@@ -109,15 +114,18 @@ const FarmDashboardWrapper = () => {
}) })
useEffect(() => { useEffect(() => {
farmDashboardService Promise.all([farmDashboardService.getConfig(), farmDashboardService.getAllCards()])
.getConfig() .then(([configData, cards]) => {
.then(data => { const validRowOrder = (configData.rowOrder ?? []).filter(
(id): id is RowId => id in ROW_CARDS
)
const merged: FarmDashboardConfig = { const merged: FarmDashboardConfig = {
disabledCardIds: data.disabledCardIds ?? [], disabledCardIds: configData.disabledCardIds ?? [],
rowOrder: data.rowOrder?.length ? data.rowOrder : [...ROW_IDS], rowOrder: validRowOrder.length ? validRowOrder : [...ROW_IDS],
enableDragReorder: data.enableDragReorder ?? true enableDragReorder: configData.enableDragReorder ?? true
} }
setConfig(merged) setConfig(merged)
setCardsData(cards ?? {})
}) })
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG)) .catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
@@ -229,7 +237,9 @@ const FarmDashboardWrapper = () => {
</IconButton> </IconButton>
)} )}
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}> <Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
{isOverviewRow && cards.includes('farmOverviewKpis') && <FarmOverviewKPIs />} {isOverviewRow && cards.includes('farmOverviewKpis') && (
<FarmOverviewKPIs data={cardsData?.farmOverviewKpis} />
)}
{!isOverviewRow && {!isOverviewRow &&
cards.map((cardId: CardId) => { cards.map((cardId: CardId) => {
const size = CARD_GRID_SIZE[cardId] const size = CARD_GRID_SIZE[cardId]
@@ -237,7 +247,7 @@ const FarmDashboardWrapper = () => {
if (!Component) return null if (!Component) return null
return ( return (
<Grid key={cardId} size={size} sx={cardRowSx}> <Grid key={cardId} size={size} sx={cardRowSx}>
<Component /> <Component data={cardsData?.[cardId]} />
</Grid> </Grid>
) )
})} })}
+29 -79
View File
@@ -6,93 +6,43 @@ import Grid from '@mui/material/Grid2'
// Component Imports // Component Imports
import CardStatsVertical from '@components/card-statistics/Vertical' import CardStatsVertical from '@components/card-statistics/Vertical'
const FarmOverviewKPIs = () => { type KpiItem = {
id: string
title: string
subtitle: string
stats: string
avatarColor?: string
avatarIcon?: string
chipText?: string
chipColor?: string
}
interface FarmOverviewKPIsProps {
data?: Record<string, unknown>
}
const FarmOverviewKPIs = ({ data }: FarmOverviewKPIsProps) => {
const kpis = (data?.kpis as KpiItem[] | undefined) ?? []
if (kpis.length === 0) return null
return ( return (
<> <>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}> {kpis.map((kpi) => (
<Grid key={kpi.id} size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical <CardStatsVertical
title='Farm Health Score' title={kpi.title}
subtitle='AI Analysis' subtitle={kpi.subtitle}
stats='87%' stats={kpi.stats}
avatarColor='success' avatarColor={(kpi.avatarColor as 'success' | 'info' | 'primary' | 'secondary' | 'warning') ?? 'primary'}
avatarIcon='tabler-heartbeat' avatarIcon={kpi.avatarIcon ?? 'tabler-chart-bar'}
avatarSkin='light' avatarSkin='light'
avatarSize={44} avatarSize={44}
chipText='Good' chipText={kpi.chipText}
chipColor='success' chipColor={(kpi.chipColor as 'success' | 'warning') ?? 'success'}
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Water Stress Index'
subtitle='Current'
stats='12%'
avatarColor='info'
avatarIcon='tabler-droplet'
avatarSkin='light'
avatarSize={44}
chipText='Low'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Disease Risk'
subtitle='Last 7 Days'
stats='Low'
avatarColor='success'
avatarIcon='tabler-bug'
avatarSkin='light'
avatarSize={44}
chipText='5%'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Avg Soil Moisture'
subtitle='Field-wide'
stats='65%'
avatarColor='primary'
avatarIcon='tabler-plant-2'
avatarSkin='light'
avatarSize={44}
chipText='Optimal'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Yield Prediction'
subtitle='This Season'
stats='42 ton'
avatarColor='secondary'
avatarIcon='tabler-chart-bar'
avatarSkin='light'
avatarSize={44}
chipText='+8%'
chipColor='success'
chipVariant='tonal'
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 2 }}>
<CardStatsVertical
title='Pest Risk'
subtitle='AI Forecast'
stats='15%'
avatarColor='warning'
avatarIcon='tabler-bug-off'
avatarSkin='light'
avatarSize={44}
chipText='Monitor'
chipColor='warning'
chipVariant='tonal' chipVariant='tonal'
/> />
</Grid> </Grid>
))}
</> </>
) )
} }
+19 -6
View File
@@ -19,10 +19,20 @@ import OptionMenu from '@core/components/option-menu'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Vars - Mock weather data (temp variation through day) interface FarmWeatherCardProps {
const series = [{ data: [18, 22, 26, 28, 25, 20, 18] }] data?: Record<string, unknown>
}
const FarmWeatherCard = () => { const FarmWeatherCard = ({ data }: FarmWeatherCardProps) => {
const temperature = data?.temperature ?? 24
const condition = (data?.condition as string) ?? ''
const humidity = data?.humidity ?? 45
const windSpeed = data?.windSpeed ?? 12
const windUnit = (data?.windUnit as string) ?? 'km/h'
const unit = (data?.unit as string) ?? '°C'
const chartData = data?.chartData as { labels?: string[]; series?: number[][] } | undefined
const seriesData = chartData?.series?.[0] ?? [18, 22, 26, 28, 25, 20, 18]
const series = [{ data: seriesData }]
const theme = useTheme() const theme = useTheme()
const infoColor = theme.palette.info.main const infoColor = theme.palette.info.main
@@ -77,16 +87,19 @@ const FarmWeatherCard = () => {
<Card className='pbe-6'> <Card className='pbe-6'>
<CardHeader <CardHeader
title='Weather Today' title='Weather Today'
subheader='Clear, 24°C' subheader={condition ? `${condition}, ${temperature}${unit}` : `${temperature}${unit}`}
className='pbe-3' className='pbe-3'
action={<OptionMenu options={['Refresh', '7-day forecast', 'Details']} />} action={<OptionMenu options={['Refresh', '7-day forecast', 'Details']} />}
/> />
<CardContent> <CardContent>
<div className='flex items-center gap-2 mbe-2'> <div className='flex items-center gap-2 mbe-2'>
<i className='tabler-sun text-4xl text-warning' /> <i className='tabler-sun text-4xl text-warning' />
<Typography variant='h4'>24°C</Typography> <Typography variant='h4'>
{temperature}
{unit}
</Typography>
<Typography color='text.disabled' variant='body2'> <Typography color='text.disabled' variant='body2'>
Humid: 45% | Wind: 12 km/h Humid: {humidity}% | Wind: {windSpeed} {windUnit}
</Typography> </Typography>
</div> </div>
</CardContent> </CardContent>
@@ -11,7 +11,16 @@ import Chip from '@mui/material/Chip'
import CustomAvatar from '@core/components/mui/Avatar' import CustomAvatar from '@core/components/mui/Avatar'
import OptionMenu from '@core/components/option-menu' import OptionMenu from '@core/components/option-menu'
const HarvestPredictionCard = () => { interface HarvestPredictionCardProps {
data?: Record<string, unknown>
}
const HarvestPredictionCard = ({ data }: HarvestPredictionCardProps) => {
const harvestDate = (data?.dateFormatted as string) ?? ''
const daysUntil = (data?.daysUntil as number | undefined) ?? 0
const daysLeftFormatted = daysUntil > 0 ? `${daysUntil} days` : ''
const description = (data?.description as string) ?? ''
return ( return (
<Card> <Card>
<CardHeader <CardHeader
@@ -26,12 +35,16 @@ const HarvestPredictionCard = () => {
/> />
<CardContent className='flex flex-col gap-4'> <CardContent className='flex flex-col gap-4'>
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Typography variant='h3'>Oct 15, 2025</Typography> <Typography variant='h3'>{harvestDate}</Typography>
<Chip label='58 days' color='info' size='small' variant='tonal' /> {daysLeftFormatted && (
<Chip label={daysLeftFormatted} color='info' size='small' variant='tonal' />
)}
</div> </div>
{description && (
<Typography variant='body2' color='text.secondary'> <Typography variant='body2' color='text.secondary'>
Based on current GDD accumulation and weather forecast. Optimal harvest window: Oct 12-18. {description}
</Typography> </Typography>
)}
</CardContent> </CardContent>
</Card> </Card>
) )
+19 -7
View File
@@ -21,7 +21,14 @@ import CustomAvatar from '@core/components/mui/Avatar'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
const NDVIHealthCard = () => { interface NDVIHealthCardProps {
data?: Record<string, unknown>
}
const NDVIHealthCard = ({ data }: NDVIHealthCardProps) => {
const ndviIndex = (data?.ndviIndex as number | undefined) ?? 0
const healthData =
(data?.healthData as Array<{ title: string; value: string; color: string; icon: string }>) ?? []
const theme = useTheme() const theme = useTheme()
const successColor = theme.palette.success.main const successColor = theme.palette.success.main
const disabledText = 'var(--mui-palette-text-disabled)' const disabledText = 'var(--mui-palette-text-disabled)'
@@ -72,10 +79,7 @@ const NDVIHealthCard = () => {
} }
} }
const healthData = [ const ndviPercent = (typeof ndviIndex === 'number' ? ndviIndex : parseFloat(String(ndviIndex)) || 0) * 100
{ title: 'Nitrogen Stress', value: 'Low', color: 'success', icon: 'tabler-leaf' },
{ title: 'Crop Health', value: 'Good', color: 'success', icon: 'tabler-plant' }
]
return ( return (
<Card> <Card>
@@ -87,8 +91,9 @@ const NDVIHealthCard = () => {
/> />
<CardContent className='flex flex-col sm:flex-row items-center justify-between gap-6'> <CardContent className='flex flex-col sm:flex-row items-center justify-between gap-6'>
<div className='flex flex-col gap-4 is-full sm:is-[unset]'> <div className='flex flex-col gap-4 is-full sm:is-[unset]'>
<Typography variant='h2'>0.78</Typography> <Typography variant='h2'>{ndviIndex}</Typography>
<Typography variant='body2'>NDVI Index (0-1)</Typography> <Typography variant='body2'>NDVI Index (0-1)</Typography>
{healthData.length > 0 && (
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
{healthData.map((item, index) => ( {healthData.map((item, index) => (
<div key={index} className='flex items-center gap-3'> <div key={index} className='flex items-center gap-3'>
@@ -102,8 +107,15 @@ const NDVIHealthCard = () => {
</div> </div>
))} ))}
</div> </div>
)}
</div> </div>
<AppReactApexCharts type='radialBar' height={200} width='100%' series={[78]} options={options} /> <AppReactApexCharts
type='radialBar'
height={200}
width='100%'
series={[ndviPercent]}
options={options}
/>
</CardContent> </CardContent>
</Card> </Card>
) )
@@ -20,34 +20,14 @@ type RecommendationType = {
avatarColor: 'primary' | 'info' | 'success' | 'warning' | 'error' avatarColor: 'primary' | 'info' | 'success' | 'warning' | 'error'
} }
const data: RecommendationType[] = [ interface RecommendationsListProps {
{ data?: Record<string, unknown>
title: 'Irrigation: 6:00-8:00 AM',
subtitle: '450 m³ for Zone A. Without irrigation, yield may drop ~8%.',
avatarIcon: 'tabler-droplet',
avatarColor: 'primary'
},
{
title: 'Fertilizer: NPK 20-20-20',
subtitle: 'Apply 25 kg/ha in 7 days. Current N deficiency in sector 2.',
avatarIcon: 'tabler-leaf',
avatarColor: 'success'
},
{
title: 'Fungicide: Preventive',
subtitle: 'Humidity + temp favor fungi. Consider copper-based spray.',
avatarIcon: 'tabler-mushroom',
avatarColor: 'warning'
},
{
title: 'Harvest Window: Oct 12-18',
subtitle: 'Peak ripeness expected Oct 15. Plan labor accordingly.',
avatarIcon: 'tabler-calendar-event',
avatarColor: 'info'
} }
]
const RecommendationsList = () => { const RecommendationsList = ({ data }: RecommendationsListProps) => {
const recommendations = (data?.recommendations as RecommendationType[] | undefined) ?? []
if (recommendations.length === 0) return null
return ( return (
<Card> <Card>
<CardHeader <CardHeader
@@ -56,7 +36,7 @@ const RecommendationsList = () => {
action={<OptionMenu options={['Export', 'Snooze', 'Mark Done']} />} action={<OptionMenu options={['Export', 'Snooze', 'Mark Done']} />}
/> />
<CardContent className='flex flex-col gap-4'> <CardContent className='flex flex-col gap-4'>
{data.map((item, index) => ( {recommendations.map((item, index) => (
<div key={index} className='flex items-start gap-4'> <div key={index} className='flex items-start gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={38}> <CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={38}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} /> <i className={classnames(item.avatarIcon, 'text-[22px]')} />
@@ -16,14 +16,17 @@ import type { ApexOptions } from 'apexcharts'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Vars - Soil moisture: today vs last week (7 days) interface SensorComparisonChartProps {
const series = [ data?: Record<string, unknown>
{ name: 'Today', data: [42, 45, 48, 52, 50, 48, 46] }, }
{ name: 'Last Week', data: [38, 40, 42, 45, 43, 40, 38] }
]
const SensorComparisonChart = () => { const SensorComparisonChart = ({ data }: SensorComparisonChartProps) => {
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const categories = (data?.categories as string[]) ?? ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const currentValue = data?.currentValue ?? 48
const vsLastWeek = (data?.vsLastWeek as string) ?? '+5% vs last week'
const theme = useTheme() const theme = useTheme()
if (series.length === 0) return null
const options: ApexOptions = { const options: ApexOptions = {
chart: { chart: {
@@ -46,7 +49,7 @@ const SensorComparisonChart = () => {
yaxis: { lines: { show: true } } yaxis: { lines: { show: true } }
}, },
xaxis: { xaxis: {
categories: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], categories,
labels: { labels: {
style: { colors: 'var(--mui-palette-text-disabled)' } style: { colors: 'var(--mui-palette-text-disabled)' }
}, },
@@ -77,9 +80,9 @@ const SensorComparisonChart = () => {
/> />
<CardContent> <CardContent>
<div className='flex items-center gap-4 mbe-4'> <div className='flex items-center gap-4 mbe-4'>
<Typography variant='h4'>48%</Typography> <Typography variant='h4'>{currentValue}%</Typography>
<Typography color='success.main' variant='body2'> <Typography color='success.main' variant='body2'>
+5% vs last week {vsLastWeek}
</Typography> </Typography>
</div> </div>
<AppReactApexCharts type='area' height={280} width='100%' series={series} options={options} /> <AppReactApexCharts type='area' height={280} width='100%' series={series} options={options} />
@@ -18,18 +18,17 @@ import OptionMenu from '@core/components/option-menu'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Vars - Today vs ideal ranges (normalized 0-100) interface SensorRadarChartProps {
const series = [ data?: Record<string, unknown>
{ name: 'Today', data: [75, 65, 80, 70, 85, 60] }, }
{ name: 'Ideal', data: [80, 70, 75, 75, 90, 50] }
]
const labels = ['Temp', 'Humidity', 'pH', 'EC', 'Light', 'Wind'] const SensorRadarChart = ({ data }: SensorRadarChartProps) => {
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const SensorRadarChart = () => { const labels = (data?.labels as string[]) ?? []
const theme = useTheme() const theme = useTheme()
const textDisabled = 'var(--mui-palette-text-disabled)' const textDisabled = 'var(--mui-palette-text-disabled)'
const divider = 'var(--mui-palette-divider)' const divider = 'var(--mui-palette-divider)'
if (series.length === 0) return null
const options: ApexOptions = { const options: ApexOptions = {
chart: { chart: {
@@ -61,7 +60,7 @@ const SensorRadarChart = () => {
show: true, show: true,
style: { style: {
fontSize: '13px', fontSize: '13px',
colors: Array(6).fill(textDisabled) colors: Array(labels.length || 6).fill(textDisabled)
} }
} }
}, },
+8 -12
View File
@@ -20,18 +20,14 @@ type SensorDataType = {
unit: string unit: string
} }
const data: SensorDataType[] = [ interface SensorValuesListProps {
{ title: '28°C', subtitle: 'Air Temperature', trendNumber: 2.1, unit: '°C' }, data?: Record<string, unknown>
{ title: '24°C', subtitle: 'Soil Temperature', trendNumber: -0.5, trend: 'negative', unit: '°C' }, }
{ title: '65%', subtitle: 'Air Humidity', trendNumber: 3.2, unit: '%' },
{ title: '42%', subtitle: 'Soil Moisture (10cm)', trendNumber: -1.8, trend: 'negative', unit: '%' }, const SensorValuesList = ({ data }: SensorValuesListProps) => {
{ title: '6.8', subtitle: 'Soil pH', trendNumber: 0.2, unit: 'pH' }, const sensors = (data?.sensors as SensorDataType[] | undefined) ?? []
{ title: '1.2', subtitle: 'EC (dS/m)', trendNumber: 0.1, unit: 'dS/m' }, if (sensors.length === 0) return null
{ title: '850', subtitle: 'Light Intensity (lux)', trendNumber: 15.3, unit: 'lux' },
{ title: '12', subtitle: 'Wind Speed (km/h)', trendNumber: -2.4, trend: 'negative', unit: 'km/h' }
]
const SensorValuesList = () => {
return ( return (
<Card> <Card>
<CardHeader <CardHeader
@@ -40,7 +36,7 @@ const SensorValuesList = () => {
action={<OptionMenu options={['Last Hour', 'Last 24h', 'Last 7 Days']} />} action={<OptionMenu options={['Last Hour', 'Last 24h', 'Last 7 Days']} />}
/> />
<CardContent className='flex flex-col gap-4'> <CardContent className='flex flex-col gap-4'>
{data.map((item, index) => ( {sensors.map((item, index) => (
<div key={index} className='flex items-center gap-4'> <div key={index} className='flex items-center gap-4'>
<div className='flex flex-wrap justify-between items-center gap-x-4 gap-y-1 is-full'> <div className='flex flex-wrap justify-between items-center gap-x-4 gap-y-1 is-full'>
<div className='flex flex-col'> <div className='flex flex-col'>
@@ -15,20 +15,24 @@ import type { ApexOptions } from 'apexcharts'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Generate soil moisture data: rows = field zones (Z1-Z7), cols = hours (6am-6pm) interface HeatmapDataPoint {
const zones = ['Z1', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'Z7'] x: string
const hours = ['6h', '8h', '10h', '12h', '14h', '16h', '18h'] y: number
}
const series = zones.map((zone, i) => ({ interface HeatmapSeries {
name: zone, name: string
data: hours.map((h, j) => ({ data: HeatmapDataPoint[]
x: h, }
y: Math.floor(Math.random() * 40) + 35
}))
}))
const SoilMoistureHeatmap = () => { interface SoilMoistureHeatmapProps {
data?: Record<string, unknown>
}
const SoilMoistureHeatmap = ({ data }: SoilMoistureHeatmapProps) => {
const series = (data?.series as HeatmapSeries[]) ?? []
const theme = useTheme() const theme = useTheme()
if (series.length === 0) return null
const options: ApexOptions = { const options: ApexOptions = {
chart: { chart: {
@@ -19,12 +19,19 @@ import OptionMenu from '@core/components/option-menu'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Vars - 7-day water need prediction (m³) interface WaterNeedPredictionProps {
const series = [{ name: 'Water Need', data: [420, 450, 480, 460, 490, 510, 480] }] data?: Record<string, unknown>
}
const WaterNeedPrediction = () => { const WaterNeedPrediction = ({ data }: WaterNeedPredictionProps) => {
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const categories = (data?.categories as string[]) ?? ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7']
const totalNext7Days = data?.totalNext7Days ?? 0
const unit = (data?.unit as string) ?? 'm³'
const totalFormatted = typeof totalNext7Days === 'number' ? totalNext7Days.toLocaleString() : String(totalNext7Days)
const theme = useTheme() const theme = useTheme()
const primaryColor = theme.palette.primary.main const primaryColor = theme.palette.primary.main
if (series.length === 0) return null
const options: ApexOptions = { const options: ApexOptions = {
chart: { chart: {
@@ -48,7 +55,7 @@ const WaterNeedPrediction = () => {
xaxis: { lines: { show: false } } xaxis: { lines: { show: false } }
}, },
xaxis: { xaxis: {
categories: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6', 'Day 7'], categories,
labels: { style: { colors: 'var(--mui-palette-text-disabled)' } }, labels: { style: { colors: 'var(--mui-palette-text-disabled)' } },
axisBorder: { show: false }, axisBorder: { show: false },
axisTicks: { show: false } axisTicks: { show: false }
@@ -56,11 +63,11 @@ const WaterNeedPrediction = () => {
yaxis: { yaxis: {
labels: { labels: {
style: { colors: 'var(--mui-palette-text-disabled)' }, style: { colors: 'var(--mui-palette-text-disabled)' },
formatter: (val: number) => `${val} ` formatter: (val: number) => `${val} ${unit}`
} }
}, },
tooltip: { tooltip: {
y: { formatter: (val: number) => `${val} ` } y: { formatter: (val: number) => `${val} ${unit}` }
} }
} }
@@ -73,7 +80,9 @@ const WaterNeedPrediction = () => {
/> />
<CardContent> <CardContent>
<div className='flex items-center gap-4 mbe-4'> <div className='flex items-center gap-4 mbe-4'>
<Typography variant='h4'>3,290 m³</Typography> <Typography variant='h4'>
{totalFormatted} {unit}
</Typography>
<Typography variant='body2' color='text.secondary'> <Typography variant='body2' color='text.secondary'>
Total next 7 days Total next 7 days
</Typography> </Typography>
@@ -21,14 +21,26 @@ import CustomAvatar from '@core/components/mui/Avatar'
// Styled Component Imports // Styled Component Imports
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
// Vars - Yield comparison: this year vs last year (tons per month) type SummaryItem = {
const series = [ title: string
{ name: 'This Year', data: [35, 38, 40, 42, 45, 48, 50, 48, 46, 44, 42, 42] }, subtitle: string
{ name: 'Last Year', data: [32, 34, 36, 38, 40, 42, 44, 42, 40, 38, 36, 38] } amount: string
] avatarColor: string
avatarIcon: string
}
const YieldPredictionChart = () => { interface YieldPredictionChartProps {
data?: Record<string, unknown>
}
const YieldPredictionChart = ({ data }: YieldPredictionChartProps) => {
const series = (data?.series as Array<{ name: string; data: number[] }>) ?? []
const categories =
(data?.categories as string[]) ??
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const summary = (data?.summary as SummaryItem[]) ?? []
const theme = useTheme() const theme = useTheme()
if (series.length === 0) return null
const options: ApexOptions = { const options: ApexOptions = {
chart: { chart: {
@@ -48,7 +60,7 @@ const YieldPredictionChart = () => {
strokeDashArray: 4 strokeDashArray: 4
}, },
xaxis: { xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], categories,
labels: { style: { colors: 'var(--mui-palette-text-disabled)' } } labels: { style: { colors: 'var(--mui-palette-text-disabled)' } }
}, },
yaxis: { yaxis: {
@@ -62,11 +74,6 @@ const YieldPredictionChart = () => {
} }
} }
const summaryData = [
{ title: 'Predicted Yield', subtitle: 'This Season', amount: '42 ton', avatarColor: 'primary', avatarIcon: 'tabler-chart-bar' },
{ title: 'Harvest Date', subtitle: 'Est. Oct 15', amount: '+8%', avatarColor: 'success', avatarIcon: 'tabler-calendar' }
]
return ( return (
<Card> <Card>
<CardHeader <CardHeader
@@ -76,10 +83,11 @@ const YieldPredictionChart = () => {
/> />
<CardContent className='flex flex-col gap-4'> <CardContent className='flex flex-col gap-4'>
<AppReactApexCharts type='line' height={280} width='100%' series={series} options={options} /> <AppReactApexCharts type='line' height={280} width='100%' series={series} options={options} />
{summary.length > 0 && (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
{summaryData.map((item, index) => ( {summary.map((item, index) => (
<div key={index} className='flex items-center gap-4'> <div key={index} className='flex items-center gap-4'>
<CustomAvatar skin='light' variant='rounded' color={item.avatarColor} size={38}> <CustomAvatar skin='light' variant='rounded' color={item.avatarColor as 'primary'} size={38}>
<i className={classnames(item.avatarIcon, 'text-[22px]')} /> <i className={classnames(item.avatarIcon, 'text-[22px]')} />
</CustomAvatar> </CustomAvatar>
<div className='flex justify-between items-center is-full'> <div className='flex justify-between items-center is-full'>
@@ -96,6 +104,7 @@ const YieldPredictionChart = () => {
</div> </div>
))} ))}
</div> </div>
)}
</CardContent> </CardContent>
</Card> </Card>
) )