ca-grow-ops-manager/frontend/src/pages/ReportsPage.tsx
fullsizemalt 71e58dd4c7
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
feat: Linear-inspired UI redesign with Space Grotesk headlines
- 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
2025-12-12 14:29:47 -08:00

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>
);
}