421 lines
14 KiB
TypeScript
421 lines
14 KiB
TypeScript
|
|
import { useState, useEffect } from 'react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { Download, Filter, Search, Check, X } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { TaskCard } from '@/components/TaskCard';
|
|
import { ExportOptions } from '@/components/ExportOptions';
|
|
|
|
interface Task {
|
|
id: string;
|
|
task_name: string;
|
|
description: string;
|
|
priority: string;
|
|
due_date: string | null;
|
|
status: string;
|
|
created_at: string;
|
|
upload_id: string;
|
|
}
|
|
|
|
interface Upload {
|
|
id: string;
|
|
file_name: string;
|
|
}
|
|
|
|
const TaskList = () => {
|
|
const { user } = useAuth();
|
|
const { toast } = useToast();
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [filteredTasks, setFilteredTasks] = useState<Task[]>([]);
|
|
const [uploads, setUploads] = useState<Upload[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [priorityFilter, setPriorityFilter] = useState<string>('');
|
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
const [documentFilter, setDocumentFilter] = useState<string>('');
|
|
const [viewMode, setViewMode] = useState<'table' | 'card'>('table');
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchTasks();
|
|
fetchUploads();
|
|
}
|
|
}, [user]);
|
|
|
|
useEffect(() => {
|
|
applyFilters();
|
|
}, [tasks, searchTerm, priorityFilter, statusFilter, documentFilter]);
|
|
|
|
const fetchTasks = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const { data, error } = await supabase
|
|
.from('tasks')
|
|
.select('*')
|
|
.eq('user_id', user?.id)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) throw error;
|
|
setTasks(data || []);
|
|
setFilteredTasks(data || []);
|
|
} catch (error) {
|
|
console.error('Error fetching tasks:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to load tasks',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchUploads = async () => {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('uploads')
|
|
.select('id, file_name')
|
|
.eq('user_id', user?.id);
|
|
|
|
if (error) throw error;
|
|
setUploads(data || []);
|
|
} catch (error) {
|
|
console.error('Error fetching uploads:', error);
|
|
}
|
|
};
|
|
|
|
const applyFilters = () => {
|
|
let result = [...tasks];
|
|
|
|
// Apply search term filter
|
|
if (searchTerm) {
|
|
result = result.filter(
|
|
task =>
|
|
task.task_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
task.description.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Apply priority filter
|
|
if (priorityFilter) {
|
|
result = result.filter(task => task.priority === priorityFilter);
|
|
}
|
|
|
|
// Apply status filter
|
|
if (statusFilter) {
|
|
result = result.filter(task => task.status === statusFilter);
|
|
}
|
|
|
|
// Apply document filter
|
|
if (documentFilter) {
|
|
result = result.filter(task => task.upload_id === documentFilter);
|
|
}
|
|
|
|
setFilteredTasks(result);
|
|
};
|
|
|
|
const updateTaskStatus = async (taskId: string, newStatus: string) => {
|
|
try {
|
|
const { error } = await supabase
|
|
.from('tasks')
|
|
.update({ status: newStatus })
|
|
.eq('id', taskId);
|
|
|
|
if (error) throw error;
|
|
|
|
setTasks(prevTasks =>
|
|
prevTasks.map(task =>
|
|
task.id === taskId ? { ...task, status: newStatus } : task
|
|
)
|
|
);
|
|
|
|
toast({
|
|
title: 'Task updated',
|
|
description: `Task status changed to ${newStatus}`,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error updating task:', error);
|
|
toast({
|
|
title: 'Update failed',
|
|
description: 'Failed to update task status',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
const getDocumentName = (uploadId: string) => {
|
|
const upload = uploads.find(u => u.id === uploadId);
|
|
return upload ? upload.file_name : 'Unknown Document';
|
|
};
|
|
|
|
const getPriorityBadge = (priority: string) => {
|
|
switch (priority.toLowerCase()) {
|
|
case 'high':
|
|
return <Badge variant="destructive">{priority}</Badge>;
|
|
case 'medium':
|
|
return <Badge variant="default">{priority}</Badge>;
|
|
case 'low':
|
|
return <Badge variant="secondary">{priority}</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{priority}</Badge>;
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case 'completed':
|
|
return <Badge className="bg-green-500">{status}</Badge>;
|
|
case 'in progress':
|
|
return <Badge className="bg-blue-500">{status}</Badge>;
|
|
case 'open':
|
|
return <Badge variant="outline">{status}</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{status}</Badge>;
|
|
}
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
setSearchTerm('');
|
|
setPriorityFilter('');
|
|
setStatusFilter('');
|
|
setDocumentFilter('');
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-center p-8">
|
|
<div className="text-center">
|
|
<div className="mb-4">
|
|
<div className="h-8 w-8 mx-auto animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
|
</div>
|
|
<p className="text-muted-foreground">Loading tasks...</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Tasks</CardTitle>
|
|
<CardDescription>
|
|
Actionable items extracted from your policy documents
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="search"
|
|
placeholder="Search tasks..."
|
|
className="pl-8"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => setViewMode(viewMode === 'table' ? 'card' : 'table')}
|
|
>
|
|
{viewMode === 'table' ?
|
|
<div className="h-4 w-4 grid grid-cols-2 gap-0.5">
|
|
<div className="bg-foreground rounded-sm"></div>
|
|
<div className="bg-foreground rounded-sm"></div>
|
|
<div className="bg-foreground rounded-sm"></div>
|
|
<div className="bg-foreground rounded-sm"></div>
|
|
</div> :
|
|
<div className="h-4 w-4 flex flex-col justify-between">
|
|
<div className="h-[2px] w-full bg-foreground"></div>
|
|
<div className="h-[2px] w-full bg-foreground"></div>
|
|
<div className="h-[2px] w-full bg-foreground"></div>
|
|
</div>
|
|
}
|
|
</Button>
|
|
<ExportOptions tasks={filteredTasks} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue placeholder="Priority" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">All Priorities</SelectItem>
|
|
<SelectItem value="High">High</SelectItem>
|
|
<SelectItem value="Medium">Medium</SelectItem>
|
|
<SelectItem value="Low">Low</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-[140px]">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">All Statuses</SelectItem>
|
|
<SelectItem value="Open">Open</SelectItem>
|
|
<SelectItem value="In Progress">In Progress</SelectItem>
|
|
<SelectItem value="Completed">Completed</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={documentFilter} onValueChange={setDocumentFilter}>
|
|
<SelectTrigger className="w-[180px]">
|
|
<SelectValue placeholder="Document" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">All Documents</SelectItem>
|
|
{uploads.map((upload) => (
|
|
<SelectItem key={upload.id} value={upload.id}>
|
|
{upload.file_name.length > 25
|
|
? `${upload.file_name.substring(0, 25)}...`
|
|
: upload.file_name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{(searchTerm || priorityFilter || statusFilter || documentFilter) && (
|
|
<Button variant="ghost" onClick={clearFilters} className="h-10">
|
|
<X className="mr-2 h-3 w-3" />
|
|
Clear filters
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{filteredTasks.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<div className="mb-4">
|
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
|
<Filter className="h-6 w-6 text-muted-foreground" />
|
|
</div>
|
|
</div>
|
|
<h3 className="text-lg font-semibold">No tasks found</h3>
|
|
<p className="text-muted-foreground">
|
|
{tasks.length > 0
|
|
? 'Try changing your filters or search term'
|
|
: 'Upload a policy document to extract tasks'}
|
|
</p>
|
|
{tasks.length === 0 && (
|
|
<Button
|
|
variant="outline"
|
|
className="mt-4"
|
|
onClick={() => window.location.href = '/dashboard'}
|
|
>
|
|
Upload Document
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : viewMode === 'table' ? (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Task</TableHead>
|
|
<TableHead>Priority</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Document</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredTasks.map((task) => (
|
|
<TableRow key={task.id}>
|
|
<TableCell className="font-medium">{task.task_name}</TableCell>
|
|
<TableCell>{getPriorityBadge(task.priority)}</TableCell>
|
|
<TableCell>{getStatusBadge(task.status)}</TableCell>
|
|
<TableCell className="max-w-[200px] truncate">
|
|
{getDocumentName(task.upload_id)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex justify-end gap-2">
|
|
{task.status !== 'Completed' ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => updateTaskStatus(task.id, 'Completed')}
|
|
>
|
|
<Check className="mr-1 h-3 w-3" />
|
|
Mark Complete
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => updateTaskStatus(task.id, 'Open')}
|
|
>
|
|
<X className="mr-1 h-3 w-3" />
|
|
Reopen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{filteredTasks.map((task) => (
|
|
<TaskCard
|
|
key={task.id}
|
|
task={task}
|
|
documentName={getDocumentName(task.upload_id)}
|
|
onStatusChange={(newStatus) => updateTaskStatus(task.id, newStatus)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-between">
|
|
<div className="text-sm text-muted-foreground">
|
|
Showing {filteredTasks.length} of {tasks.length} tasks
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TaskList;
|