220 lines
8.3 KiB
TypeScript
220 lines
8.3 KiB
TypeScript
import React from 'react';
|
|
import { Plant, Task, MaintenanceRecord, HarvestRecord } from '../types';
|
|
import { Calendar, AlertTriangle, CheckCircle, Sprout, Scissors, Droplets } from 'lucide-react';
|
|
|
|
interface DashboardProps {
|
|
plants: Plant[];
|
|
tasks: Task[];
|
|
maintenanceRecords: MaintenanceRecord[];
|
|
harvestRecords: HarvestRecord[];
|
|
onNavigate: (tab: string) => void;
|
|
}
|
|
|
|
const Dashboard: React.FC<DashboardProps> = ({
|
|
plants,
|
|
tasks,
|
|
maintenanceRecords,
|
|
harvestRecords,
|
|
onNavigate
|
|
}) => {
|
|
const upcomingTasks = tasks
|
|
.filter(task => !task.completed && new Date(task.deadline) >= new Date())
|
|
.sort((a, b) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime())
|
|
.slice(0, 5);
|
|
|
|
const plantsNeedingAttention = plants.filter(plant => plant.healthStatus === 'needs-attention');
|
|
|
|
const recentMaintenanceRecords = maintenanceRecords
|
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
.slice(0, 5);
|
|
|
|
const stats = [
|
|
{
|
|
label: 'Total Plants',
|
|
value: plants.length,
|
|
icon: Sprout,
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100'
|
|
},
|
|
{
|
|
label: 'Pending Tasks',
|
|
value: tasks.filter(task => !task.completed).length,
|
|
icon: Calendar,
|
|
color: 'text-blue-600',
|
|
bgColor: 'bg-blue-100'
|
|
},
|
|
{
|
|
label: 'Need Attention',
|
|
value: plantsNeedingAttention.length,
|
|
icon: AlertTriangle,
|
|
color: 'text-yellow-600',
|
|
bgColor: 'bg-yellow-100'
|
|
},
|
|
{
|
|
label: 'This Year Harvests',
|
|
value: harvestRecords.filter(record =>
|
|
new Date(record.date).getFullYear() === new Date().getFullYear()
|
|
).length,
|
|
icon: CheckCircle,
|
|
color: 'text-purple-600',
|
|
bgColor: 'bg-purple-100'
|
|
}
|
|
];
|
|
|
|
const getMaintenanceIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'pruning': return Scissors;
|
|
case 'watering': return Droplets;
|
|
default: return Calendar;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<h2 className="text-3xl font-bold text-green-800 mb-2">Garden Dashboard</h2>
|
|
<p className="text-green-600">Welcome to your garden management center</p>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{stats.map((stat, index) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div key={index} className="bg-white rounded-lg shadow-sm border border-green-100 p-6 hover:shadow-md transition-shadow">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">{stat.label}</p>
|
|
<p className="text-3xl font-bold text-gray-900 mt-1">{stat.value}</p>
|
|
</div>
|
|
<div className={`${stat.bgColor} p-3 rounded-lg`}>
|
|
<Icon className={`h-6 w-6 ${stat.color}`} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Upcoming Tasks */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-green-100">
|
|
<div className="p-6 border-b border-green-100">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="text-lg font-semibold text-green-800">Upcoming Tasks</h3>
|
|
<button
|
|
onClick={() => onNavigate('tasks')}
|
|
className="text-sm text-green-600 hover:text-green-700 transition-colors"
|
|
>
|
|
View all →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="p-6">
|
|
{upcomingTasks.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{upcomingTasks.map((task) => {
|
|
const plant = plants.find(p => p.id === task.plantId);
|
|
const isOverdue = new Date(task.deadline) < new Date();
|
|
return (
|
|
<div key={task.id} className="flex items-start space-x-3 p-3 bg-green-50 rounded-lg">
|
|
<Calendar className={`h-5 w-5 mt-0.5 ${isOverdue ? 'text-red-500' : 'text-green-600'}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-gray-900">{task.title}</p>
|
|
{plant && (
|
|
<p className="text-sm text-gray-600">{plant.variety}</p>
|
|
)}
|
|
<p className={`text-sm ${isOverdue ? 'text-red-600' : 'text-gray-500'}`}>
|
|
Due: {new Date(task.deadline).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-center py-8">No upcoming tasks</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Plants Needing Attention */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-green-100">
|
|
<div className="p-6 border-b border-green-100">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="text-lg font-semibold text-green-800">Plants Needing Attention</h3>
|
|
<button
|
|
onClick={() => onNavigate('plants')}
|
|
className="text-sm text-green-600 hover:text-green-700 transition-colors"
|
|
>
|
|
View all →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="p-6">
|
|
{plantsNeedingAttention.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{plantsNeedingAttention.map((plant) => (
|
|
<div key={plant.id} className="flex items-start space-x-3 p-3 bg-yellow-50 rounded-lg">
|
|
<AlertTriangle className="h-5 w-5 text-yellow-600 mt-0.5" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-gray-900">{plant.variety}</p>
|
|
<p className="text-sm text-gray-600 capitalize">{plant.type}</p>
|
|
{plant.notes && (
|
|
<p className="text-sm text-gray-500 mt-1">{plant.notes}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-center py-8">All plants are healthy!</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Maintenance */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-green-100">
|
|
<div className="p-6 border-b border-green-100">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="text-lg font-semibold text-green-800">Recent Maintenance</h3>
|
|
<button
|
|
onClick={() => onNavigate('maintenance')}
|
|
className="text-sm text-green-600 hover:text-green-700 transition-colors"
|
|
>
|
|
View all →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="p-6">
|
|
{recentMaintenanceRecords.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{recentMaintenanceRecords.map((record) => {
|
|
const plant = plants.find(p => p.id === record.plantId);
|
|
const Icon = getMaintenanceIcon(record.type);
|
|
return (
|
|
<div key={record.id} className="flex items-start space-x-3 p-3 bg-green-50 rounded-lg">
|
|
<Icon className="h-5 w-5 text-green-600 mt-0.5" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-gray-900 capitalize">{record.type}</p>
|
|
<p className="text-sm text-gray-600">{plant?.variety}</p>
|
|
<p className="text-sm text-gray-500">{record.description}</p>
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
{new Date(record.date).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 text-center py-8">No maintenance records yet</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard; |