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 (
@@ -134,6 +196,13 @@ const Dashboard = () => {

+ {error && ( +
{error}
+ )} + {loading && ( +
Loading dashboard…
+ )} + {/* Stats Grid */}
{stats.map((stat) => ( @@ -146,11 +215,13 @@ const Dashboard = () => {

{stat.name}

{stat.value}

- - {stat.change} - + {stat.change && ( + + {stat.change} + + )}
@@ -201,7 +272,7 @@ const Dashboard = () => {
- {recentActivity.map((activity) => ( + {recent.map((activity) => (
{ {activity.property && (

{activity.property}

)} -

{activity.date}

+

{activity.dateText}

))} @@ -242,10 +313,7 @@ const Dashboard = () => {

{task.property}

{task.date} -
- - {task.weather} -
+ {/* Weather summary for the specific task could be added here */}
{ Detailed forecast -
-
- -
-

72°F

-

Partly cloudy

+ {!defaultProperty && ( +
Add a property with coordinates to see local weather.
+ )} + {defaultProperty && !weather && ( +
Weather unavailable. Configure WEATHER_API_KEY to enable.
+ )} + {weather && ( + <> +
+
+ {weather.current?.icon ? ( + icon + ) : ( + + )} +
+

{Math.round(weather.current?.temperature)}°F

+

{weather.current?.conditions}

+
+
+
+ Wind: + {Math.round(weather.current?.windSpeed || 0)} mph +
+
+ Humidity: + {Math.round(weather.current?.humidity || 0)}% +
-
-
- Wind: - 5 mph -
-
- Humidity: - 45% -
-
-
-

- ✓ Good conditions for lawn applications today -

-
+
+

+ {`Location: ${weather.location?.propertyName}`} +

+
+ + )}
); }; -export default Dashboard; \ No newline at end of file +export default Dashboard;