ca-grow-ops-manager/frontend/src/pages/TasksPage.tsx
fullsizemalt e88814afef
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
fix: Expose Generate Zone button and refine styling (remove italics/caps, improve contrast)
2025-12-27 15:03:36 -08:00

310 lines
17 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Calendar, List, Plus, CheckCircle2, Clock,
RefreshCw, User, MapPin, Filter, MoreHorizontal,
Search, AlertCircle, LayoutGrid, CheckSquare,
ClipboardList, ArrowRight
} from 'lucide-react';
import { tasksApi, Task } from '../lib/tasksApi';
import { useAuth } from '../context/AuthContext';
import { useToast } from '../context/ToastContext';
import CreateTaskModal from '../components/tasks/CreateTaskModal';
import CompleteTaskModal from '../components/tasks/CompleteTaskModal';
import { Card } from '../components/ui/card';
import { cn } from '../lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
export default function TasksPage() {
const { user } = useAuth();
const { addToast } = useToast();
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [completionTask, setCompletionTask] = useState<Task | null>(null);
const [filter, setFilter] = useState<'all' | 'mine'>('mine');
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
loadTasks();
}, [filter]);
const loadTasks = async () => {
setLoading(true);
try {
const data = await tasksApi.getAll({
assigneeId: filter === 'mine' ? user?.id : undefined
});
setTasks(data);
} catch (e) {
console.error(e);
addToast('Failed to load tasks', 'error');
} finally {
setLoading(false);
}
};
const handleTaskCompleted = () => {
loadTasks();
setCompletionTask(null);
addToast('Task finalized and logged', 'success');
};
// Filter tasks by search
const filteredTasks = tasks.filter(t =>
t.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
t.room?.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const pendingTasks = filteredTasks.filter(t => t.status !== 'COMPLETED');
const completedTasks = filteredTasks.filter(t => t.status === 'COMPLETED');
return (
<div className="space-y-8 pb-12 overflow-hidden">
{/* Page Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-text-primary)]">
Operational Tasks
</h1>
<div className="flex items-center gap-3 mt-2">
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[var(--color-primary)]/10 text-[var(--color-primary)] text-[10px] font-bold uppercase tracking-wider border border-[var(--color-primary)]/20">
{pendingTasks.length} Pending
</div>
<span className="text-[var(--color-text-tertiary)] text-sm">
Real-time workflow management
</span>
</div>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-tertiary)]" size={16} />
<input
type="text"
placeholder="Filter by room or task..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 bg-[var(--color-bg-tertiary)] border border-[var(--color-border-subtle)] rounded-lg text-sm transition-all focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 w-64"
/>
</div>
<button
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-[var(--color-text-inverse)] px-4 py-2.5 rounded-xl font-bold text-sm shadow-lg transition-all"
>
<Plus size={18} />
New Task
</button>
</div>
</div>
{/* Metrics Row */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard label="Assigned to Me" value={tasks.filter(t => t.assigneeId === user?.id).length} icon={User} color="text-[var(--color-primary)]" />
<MetricCard label="Overdue" value={3} icon={AlertCircle} color="text-[var(--color-error)]" />
<MetricCard label="Due Today" value={tasks.filter(t => t.status !== 'COMPLETED').length} icon={Clock} color="text-[var(--color-warning)]" />
<MetricCard label="Completed 24h" value={completedTasks.length} icon={CheckCircle2} color="text-[var(--color-accent)]" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* Left Column: Filters & View Switching */}
<div className="lg:col-span-3 space-y-6">
<Card className="p-4 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)]">
<h4 className="text-[10px] font-bold uppercase tracking-wider text-[var(--color-text-tertiary)] mb-4 px-2">Focus</h4>
<div className="space-y-1">
<SidebarFilterButton active={filter === 'mine'} onClick={() => setFilter('mine')} label="My Work" icon={CheckSquare} count={tasks.filter(t => t.assigneeId === user?.id).length} />
<SidebarFilterButton active={filter === 'all'} onClick={() => setFilter('all')} label="Facility All" icon={LayoutGrid} count={tasks.length} />
</div>
<div className="h-px bg-slate-100 dark:bg-slate-800 my-4 mx-2" />
<h4 className="text-[10px] font-bold uppercase tracking-wider text-[var(--color-text-tertiary)] mb-4 px-2">Category</h4>
<div className="space-y-1">
<SidebarFilterButton active={false} onClick={() => { }} label="Irrigation" icon={Droplets} />
<SidebarFilterButton active={false} onClick={() => { }} label="IPM & Health" icon={Bug} />
<SidebarFilterButton active={false} onClick={() => { }} label="Compliance" icon={ClipboardList} />
</div>
</Card>
<Card className="p-4 bg-[var(--color-bg-tertiary)]/50 border-[var(--color-border-subtle)] border-dashed">
<p className="text-[10px] text-[var(--color-text-tertiary)] font-medium leading-relaxed">
<AlertCircle size={10} className="inline mr-1 mb-0.5" />
System Note:
</p>
<p className="text-[10px] text-[var(--color-text-tertiary)] mt-1 leading-relaxed">
Urgent maintenance scheduled for Flower A irrigation pump. All related tasks prioritized.
</p>
</Card>
</div>
{/* Right Column: Task Board */}
<div className="lg:col-span-9 space-y-8">
{/* Active Tasks Section */}
<div className="space-y-4">
<div className="flex items-center justify-between px-2">
<h3 className="text-xs font-bold uppercase tracking-wide text-[var(--color-text-tertiary)]">Active Board</h3>
<span className="text-[10px] font-bold text-[var(--color-text-tertiary)]">{pendingTasks.length} ITEMS</span>
</div>
<div className="space-y-3">
<AnimatePresence mode="popLayout">
{pendingTasks.map((task, idx) => (
<motion.div
key={task.id}
layout
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ delay: idx * 0.05 }}
>
<TaskRow
task={task}
onComplete={() => setCompletionTask(task)}
/>
</motion.div>
))}
</AnimatePresence>
{pendingTasks.length === 0 && (
<div className="py-12 text-center rounded-2xl border-2 border-dashed border-[var(--color-border-subtle)]">
<CheckCircle2 size={32} className="mx-auto text-[var(--color-primary)]/20 mb-3" />
<p className="text-sm font-medium text-[var(--color-text-tertiary)]">All Actions Complete</p>
</div>
)}
</div>
</div>
{/* Recently Completed Section */}
{completedTasks.length > 0 && (
<div className="space-y-4 pt-4 border-t border-[var(--color-border-subtle)]">
<div className="flex items-center justify-between px-2">
<h3 className="text-xs font-bold uppercase tracking-wide text-[var(--color-text-tertiary)]">Archive / Verification</h3>
<button className="text-[10px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-widest hover:text-[var(--color-primary)]">View History</button>
</div>
<div className="opacity-60 saturate-50 hover:opacity-100 transition-all duration-500 space-y-2">
{completedTasks.slice(0, 3).map(task => (
<div key={task.id} className="flex items-center justify-between p-4 bg-slate-50/50 dark:bg-slate-900/30 rounded-xl border border-[var(--color-border-subtle)]/50">
<div className="flex items-center gap-4">
<CheckCircle2 size={16} className="text-[var(--color-primary)]" />
<div>
<p className="text-xs font-bold text-[var(--color-text-tertiary)] line-through tracking-tight">{task.title}</p>
<p className="text-[10px] text-[var(--color-text-tertiary)] uppercase font-medium">{task.room?.name} Finalized by {task.assignee?.name}</p>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
<CreateTaskModal
isOpen={isCreateOpen}
onClose={() => setIsCreateOpen(false)}
onSuccess={() => loadTasks()}
/>
{completionTask && (
<CompleteTaskModal
task={completionTask}
onClose={() => setCompletionTask(null)}
onSuccess={handleTaskCompleted}
/>
)}
</div>
);
}
function MetricCard({ label, value, icon: Icon, color }: any) {
return (
<Card className="p-4 bg-[var(--color-bg-elevated)] border-[var(--color-border-subtle)] relative group overflow-hidden">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-[10px] font-bold text-[var(--color-text-tertiary)] uppercase tracking-wider">{label}</p>
<div className="text-2xl font-bold text-[var(--color-text-primary)]">
{value}
</div>
</div>
<div className={cn("p-2 rounded-lg bg-slate-100 dark:bg-slate-800/50", color)}>
<Icon size={18} />
</div>
</div>
</Card>
);
}
function SidebarFilterButton({ active, label, icon: Icon, count, onClick }: any) {
return (
<button
onClick={onClick}
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-xs font-bold transition-all group",
active
? "bg-[var(--color-primary)]/10 text-[var(--color-primary)] border border-emerald-500/20"
: "text-[var(--color-text-tertiary)] hover:bg-slate-100 dark:hover:bg-slate-900 hover:text-slate-900 dark:hover:text-slate-100"
)}
>
<div className="flex items-center gap-3">
<Icon size={16} className={active ? "text-[var(--color-primary)]" : "text-[var(--color-text-tertiary)] transition-colors group-hover:text-[var(--color-primary)]"} />
<span>{label}</span>
</div>
{count !== undefined && (
<span className={cn(
"text-[10px] px-1.5 py-0.5 rounded-md",
active ? "bg-[var(--color-primary)] text-white" : "bg-slate-200 dark:bg-slate-800 text-[var(--color-text-tertiary)]"
)}>{count}</span>
)}
</button>
);
}
function TaskRow({ task, onComplete }: any) {
return (
<div className="group flex items-center gap-4 p-5 bg-[var(--color-bg-elevated)] border border-[var(--color-border-subtle)] rounded-2xl hover:border-[var(--color-primary)]/50 transition-all">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h4 className="text-sm font-semibold text-[var(--color-text-primary)]">
{task.title}
</h4>
{task.priority === 'URGENT' && (
<span className="px-2 py-0.5 rounded text-[8px] font-bold uppercase tracking-wide bg-[var(--color-error)] text-white animate-pulse">
Priority: High
</span>
)}
</div>
<div className="flex items-center gap-6 text-xs text-[var(--color-text-tertiary)]">
<div className="flex items-center gap-1.5">
<MapPin size={12} className="text-[var(--color-primary)]/50" />
<span>{task.room?.name || 'GEN-01'}</span>
</div>
<div className="flex items-center gap-1.5">
<User size={12} className="text-[var(--color-text-tertiary)]" />
<span>{task.assignee?.name || 'Unassigned'}</span>
</div>
<div className="flex items-center gap-1.5">
<Clock size={12} className="text-[var(--color-text-tertiary)]" />
<span>Due {new Date(task.dueDate).toLocaleDateString()}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button className="p-2 hidden group-hover:flex items-center justify-center text-[var(--color-text-tertiary)] hover:text-slate-600 transition-all">
<MoreHorizontal size={18} />
</button>
<button
onClick={onComplete}
className="flex items-center gap-2 h-10 px-4 rounded-xl bg-[var(--color-bg-tertiary)] border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-[var(--color-text-inverse)] transition-all text-xs font-medium group/btn"
>
<CheckCircle2 size={16} className="text-[var(--color-text-tertiary)] group-hover/btn:text-white" />
<span className="opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">Finalize Action</span>
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
);
}
function Droplets({ size, className }: any) { return <ClipboardList size={size} className={className} />; }
function Bug({ size, className }: any) { return <AlertCircle size={size} className={className} />; }