diff --git a/frontend/src/pages/Dashboard/Dashboard.js b/frontend/src/pages/Dashboard/Dashboard.js index 359b134..d47ee05 100644 --- a/frontend/src/pages/Dashboard/Dashboard.js +++ b/frontend/src/pages/Dashboard/Dashboard.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { MapIcon, @@ -10,10 +10,21 @@ import { PlusIcon, } from '@heroicons/react/24/outline'; import { useAuth } from '../../hooks/useAuth'; +import LoadingSpinner from '../../components/UI/LoadingSpinner'; +import { usersAPI, applicationsAPI, mowingAPI, propertiesAPI, weatherAPI } from '../../services/api'; const Dashboard = () => { const { user } = useAuth(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [userStats, setUserStats] = useState(null); + const [appsStats, setAppsStats] = useState(null); + const [recent, setRecent] = useState([]); + const [upcoming, setUpcoming] = useState([]); + const [weather, setWeather] = useState(null); + const [defaultProperty, setDefaultProperty] = useState(null); + const quickActions = [ { name: 'Add Property', @@ -45,82 +56,133 @@ const Dashboard = () => { }, ]; - const stats = [ + const thisMonthApplications = useMemo(() => { + if (!appsStats?.monthlyBreakdown) return 0; + const m = new Date().getMonth() + 1; // 1-12 + const entry = appsStats.monthlyBreakdown.find(x => x.month === m); + return entry ? entry.applications : 0; + }, [appsStats]); + + const stats = useMemo(() => ([ { name: 'Properties', - value: '3', - change: '+1', - changeType: 'positive', + value: String(userStats?.totalProperties ?? 0), + change: '', + changeType: 'neutral', icon: MapIcon, }, { - name: 'This Month\'s Applications', - value: '12', - change: '+4', - changeType: 'positive', + name: "This Month's Applications", + value: String(thisMonthApplications ?? 0), + change: '', + changeType: 'neutral', icon: CalendarDaysIcon, }, { name: 'Equipment Items', - value: '8', - change: '+2', - changeType: 'positive', + value: String(userStats?.totalEquipment ?? 0), + change: '', + changeType: 'neutral', icon: WrenchScrewdriverIcon, }, { - name: 'Products Used', - value: '15', - change: '+3', - changeType: 'positive', + name: 'Lawn Sections', + value: String(userStats?.totalSections ?? 0), + change: '', + changeType: 'neutral', icon: BeakerIcon, }, - ]; + ]), [userStats, thisMonthApplications]); - const recentActivity = [ - { - id: 1, - type: 'application', - title: 'Applied fertilizer to Front Yard', - property: 'Main Property', - date: '2 hours ago', - status: 'completed', - }, - { - id: 2, - type: 'plan', - title: 'Scheduled weed control treatment', - property: 'Back Yard', - date: 'Tomorrow at 9:00 AM', - status: 'planned', - }, - { - id: 3, - type: 'equipment', - title: 'Added new broadcast spreader', - property: null, - date: '3 days ago', - status: 'completed', - }, - ]; + const upcomingTasks = useMemo(() => ( + upcoming.slice(0, 5).map(plan => ({ + id: plan.id, + title: plan.productDetails?.length ? `Apply ${plan.productDetails.map(p=>p.productName).join(', ')}` : (plan.equipment_name ? `Run ${plan.equipment_name}` : 'Planned application'), + property: `${plan.property_name}${plan.section_names ? ' - ' + plan.section_names : ''}`, + date: plan.plannedDate ? new Date(plan.plannedDate).toLocaleString() : '', + priority: (() => { + if (!plan.plannedDate) return 'low'; + const days = (new Date(plan.plannedDate) - new Date()) / (1000*60*60*24); + return days <= 2 ? 'high' : days <= 7 ? 'medium' : 'low'; + })() + })) + ), [upcoming]); - const upcomingTasks = [ - { - id: 1, - title: 'Pre-emergent herbicide application', - property: 'Main Property - Front Yard', - date: 'March 15, 2024', - weather: 'Partly cloudy, 65°F', - priority: 'high', - }, - { - id: 2, - title: 'Spring fertilizer application', - property: 'Side Property - Back Lawn', - date: 'March 20, 2024', - weather: 'Sunny, 72°F', - priority: 'medium', - }, - ]; + useEffect(() => { + let isMounted = true; + const load = async () => { + try { + setLoading(true); + setError(null); + + const [userStatsRes, appsStatsRes, propsRes] = await Promise.all([ + usersAPI.getStats(), + applicationsAPI.getStats({ year: new Date().getFullYear() }), + propertiesAPI.getAll() + ]); + + if (!isMounted) return; + setUserStats(userStatsRes.data?.data?.stats || null); + setAppsStats(appsStatsRes.data?.data || null); + + // Upcoming plans + const upcomingRes = await applicationsAPI.getPlans({ upcoming: true, status: 'planned' }); + if (!isMounted) return; + const upcomingPlans = upcomingRes.data?.data?.plans || []; + setUpcoming(upcomingPlans); + + // Recent activity: combine applications + mowing + const [appLogsRes, mowingLogsRes] = await Promise.all([ + applicationsAPI.getLogs({ limit: 5 }), + mowingAPI.getLogs({ limit: 5 }) + ]); + if (!isMounted) return; + const appLogs = (appLogsRes.data?.data?.logs || []).map(l => ({ + id: `app-${l.id}`, + type: 'application', + title: `${l.productNames?.length ? 'Applied ' + l.productNames.join(', ') : 'Application'} on ${l.section_names || 'section'}`, + property: l.property_name, + date: l.applicationDate || l.createdAt, + status: 'completed' + })); + const mowLogs = (mowingLogsRes.data?.data?.logs || []).map(l => ({ + id: `mow-${l.id}`, + type: 'mowing', + title: `Mowed ${l.section_names || l.property_name || ''}`.trim(), + property: l.property_name, + date: l.session_date || l.created_at, + status: 'completed' + })); + const combined = [...appLogs, ...mowLogs] + .filter(i => i.date) + .sort((a,b)=> new Date(b.date) - new Date(a.date)) + .slice(0,5) + .map(i => ({ ...i, dateText: new Date(i.date).toLocaleString() })); + setRecent(combined); + + // Weather: pick first property with coordinates + const props = propsRes.data?.data?.properties || []; + const primary = props.find(p => p.latitude && p.longitude) || props[0]; + setDefaultProperty(primary || null); + if (primary) { + try { + const w = await weatherAPI.getCurrent(primary.id); + if (!isMounted) return; + setWeather(w.data?.data?.weather || null); + } catch (_) { + setWeather(null); + } + } + } catch (e) { + if (!isMounted) return; + setError(e?.response?.data?.message || e.message || 'Failed to load dashboard'); + } finally { + if (isMounted) setLoading(false); + } + }; + load(); + return () => { isMounted = false; }; + }, []); return (
{stat.name}
{stat.value}
- - {stat.change} - + {stat.change && ( + + {stat.change} + + )}{activity.property}
)} -{activity.date}
+{activity.dateText}
{task.property}
72°F
-Partly cloudy
+ {!defaultProperty && ( +{Math.round(weather.current?.temperature)}°F
+{weather.current?.conditions}
+- ✓ Good conditions for lawn applications today -
-+ {`Location: ${weather.location?.propertyName}`} +
+