- Complete UI refactor with charcoal/bone color palette - Add Space Grotesk font for headlines, Inter for body - Update all 24+ pages with new design system - Add LinearPrimitives reusable components - Improve dark mode support throughout - Add subtle micro-animations and transitions
224 lines
15 KiB
TypeScript
224 lines
15 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { BarChart3, TrendingUp, Users, Leaf } from 'lucide-react';
|
|
import { analyticsApi, YieldAnalytics, TaskAnalytics } from '../lib/analyticsApi';
|
|
import { PageHeader, EmptyState, MetricCard, CardSkeleton } from '../components/ui/LinearPrimitives';
|
|
|
|
export default function ReportsPage() {
|
|
const [yieldData, setYieldData] = useState<YieldAnalytics | null>(null);
|
|
const [taskData, setTaskData] = useState<TaskAnalytics | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'yield' | 'tasks'>('yield');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
analyticsApi.getYield(),
|
|
analyticsApi.getTasks()
|
|
]).then(([yield_, tasks]) => {
|
|
setYieldData(yield_);
|
|
setTaskData(tasks);
|
|
}).catch(console.error).finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-6 pb-20 animate-in">
|
|
<PageHeader title="Reports" subtitle="Analytics & Performance Metrics" />
|
|
|
|
{/* Tab Selector */}
|
|
<div className="card p-1 inline-flex gap-1">
|
|
<button
|
|
onClick={() => setActiveTab('yield')}
|
|
className={`
|
|
px-4 py-2 rounded-md text-sm font-medium transition-colors duration-fast
|
|
flex items-center gap-2
|
|
${activeTab === 'yield'
|
|
? 'bg-accent text-white'
|
|
: 'text-secondary hover:text-primary hover:bg-tertiary'
|
|
}
|
|
`}
|
|
>
|
|
<Leaf size={16} />
|
|
Yield Reports
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('tasks')}
|
|
className={`
|
|
px-4 py-2 rounded-md text-sm font-medium transition-colors duration-fast
|
|
flex items-center gap-2
|
|
${activeTab === 'tasks'
|
|
? 'bg-accent text-white'
|
|
: 'text-secondary hover:text-primary hover:bg-tertiary'
|
|
}
|
|
`}
|
|
>
|
|
<BarChart3 size={16} />
|
|
Task Analytics
|
|
</button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{Array.from({ length: 4 }).map((_, i) => <CardSkeleton key={i} />)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{activeTab === 'yield' && yieldData && (
|
|
<div className="space-y-6">
|
|
{/* Strain Performance */}
|
|
<div className="card overflow-hidden">
|
|
<div className="p-4 border-b border-subtle">
|
|
<h3 className="text-sm font-medium text-primary flex items-center gap-2">
|
|
<TrendingUp size={16} className="text-success" />
|
|
Strain Performance
|
|
</h3>
|
|
<p className="text-xs text-tertiary mt-1">Average yield per plant by strain</p>
|
|
</div>
|
|
<div className="p-4">
|
|
{yieldData.byStrain.length === 0 ? (
|
|
<p className="text-tertiary text-center py-8 text-sm">
|
|
No yield data recorded yet. Log weight during harvest stages.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{yieldData.byStrain.sort((a, b) => b.avgGramsPerPlant - a.avgGramsPerPlant).map((strain, i) => (
|
|
<div key={strain.strain} className="flex items-center gap-4">
|
|
<div className={`
|
|
w-7 h-7 rounded-md flex items-center justify-center font-semibold text-xs
|
|
${i === 0 ? 'bg-warning-muted text-warning' : 'bg-tertiary text-secondary'}
|
|
`}>
|
|
{i + 1}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-sm font-medium text-primary">{strain.strain}</span>
|
|
<span className="text-sm font-semibold text-success">{strain.avgGramsPerPlant}g / plant</span>
|
|
</div>
|
|
<div className="h-1.5 bg-tertiary rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-success rounded-full transition-all duration-slow"
|
|
style={{ width: `${Math.min(100, (strain.avgGramsPerPlant / 100) * 100)}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-tertiary mt-1">
|
|
{strain.batchCount} batch{strain.batchCount > 1 ? 'es' : ''} · {strain.totalGrams.toLocaleString()}g total · {strain.totalPlants} plants
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Batch Yields Table */}
|
|
<div className="card overflow-hidden">
|
|
<div className="p-4 border-b border-subtle">
|
|
<h3 className="text-sm font-medium text-primary">Batch Yields</h3>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-secondary">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-tertiary uppercase tracking-wider">Batch</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-tertiary uppercase tracking-wider">Strain</th>
|
|
<th className="px-4 py-3 text-right text-xs font-medium text-tertiary uppercase tracking-wider">Plants</th>
|
|
<th className="px-4 py-3 text-right text-xs font-medium text-tertiary uppercase tracking-wider">Total</th>
|
|
<th className="px-4 py-3 text-right text-xs font-medium text-tertiary uppercase tracking-wider">g/Plant</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-subtle">
|
|
{yieldData.byBatch.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-tertiary text-sm">
|
|
No batch yield data available.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
yieldData.byBatch.map(batch => (
|
|
<tr key={batch.batchId} className="hover:bg-tertiary transition-colors duration-fast">
|
|
<td className="px-4 py-3 text-sm font-medium text-primary">{batch.batchName}</td>
|
|
<td className="px-4 py-3 text-sm text-secondary">{batch.strain}</td>
|
|
<td className="px-4 py-3 text-sm text-secondary text-right">{batch.plantCount}</td>
|
|
<td className="px-4 py-3 text-sm font-semibold text-primary text-right">{batch.totalGrams.toLocaleString()}g</td>
|
|
<td className="px-4 py-3 text-sm font-semibold text-success text-right">{batch.gramsPerPlant}g</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'tasks' && taskData && (
|
|
<div className="space-y-6">
|
|
{/* Task Summary Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<MetricCard icon={BarChart3} label="Completed" value={taskData.summary.completed} accent="success" />
|
|
<MetricCard icon={BarChart3} label="Pending" value={taskData.summary.pending} accent="warning" />
|
|
<MetricCard icon={BarChart3} label="In Progress" value={taskData.summary.inProgress} accent="accent" />
|
|
<MetricCard icon={BarChart3} label="Overdue" value={taskData.summary.overdue} accent="destructive" />
|
|
</div>
|
|
|
|
{/* Completion Rate */}
|
|
<div className="card p-5">
|
|
<h3 className="text-sm font-medium text-primary mb-4">Completion Rate</h3>
|
|
<div className="h-2 bg-tertiary rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-success rounded-full transition-all duration-slow"
|
|
style={{ width: `${taskData.summary.total > 0 ? (taskData.summary.completed / taskData.summary.total * 100) : 0}%` }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-tertiary mt-2">
|
|
{taskData.summary.total > 0
|
|
? `${Math.round(taskData.summary.completed / taskData.summary.total * 100)}% complete`
|
|
: 'No tasks created yet'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Staff Leaderboard */}
|
|
<div className="card overflow-hidden">
|
|
<div className="p-4 border-b border-subtle">
|
|
<h3 className="text-sm font-medium text-primary flex items-center gap-2">
|
|
<Users size={16} className="text-accent" />
|
|
Staff Leaderboard (This Week)
|
|
</h3>
|
|
</div>
|
|
<div className="p-4">
|
|
{taskData.completedByUserThisWeek.length === 0 ? (
|
|
<p className="text-tertiary text-center py-8 text-sm">
|
|
No tasks completed this week.
|
|
</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{taskData.completedByUserThisWeek.sort((a, b) => b.completedCount - a.completedCount).map((user, i) => (
|
|
<div key={user.userId} className="flex items-center gap-3 p-3 rounded-md hover:bg-tertiary transition-colors duration-fast">
|
|
<div className={`
|
|
w-7 h-7 rounded-md flex items-center justify-center font-semibold text-xs
|
|
${i === 0 ? 'bg-warning-muted text-warning' :
|
|
i === 1 ? 'bg-tertiary text-secondary' :
|
|
i === 2 ? 'bg-warning-muted/50 text-warning' :
|
|
'bg-tertiary text-tertiary'}
|
|
`}>
|
|
{i + 1}
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="text-sm font-medium text-primary">{user.userName}</span>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="text-lg font-semibold text-accent">{user.completedCount}</span>
|
|
<span className="text-xs text-tertiary ml-1">tasks</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|