Run SQL migrations

This commit is contained in:
gpt-engineer-app[bot] 2025-03-10 08:55:23 +00:00
parent 746a8dbea0
commit 85b88d9ee0
13 changed files with 2327 additions and 896 deletions

1070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,10 +45,14 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"docx": "^8.2.3",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.8.1",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"pdfjs-dist": "^3.11.174",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",

View File

@ -8,6 +8,8 @@ import { AuthProvider, useAuth } from "@/contexts/AuthContext";
import Index from "./pages/Index";
import Auth from "./pages/Auth";
import NotFound from "./pages/NotFound";
import Dashboard from "./pages/Dashboard";
import Header from "./components/Header";
const queryClient = new QueryClient();
@ -33,26 +35,23 @@ const App = () => (
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/auth" element={<Auth />} />
{/* Protected routes example (Dashboard will be implemented later) */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<div className="min-h-screen pt-20 px-4">
<div className="container mx-auto">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<p>This is a protected route. Dashboard implementation coming soon.</p>
</div>
</div>
</ProtectedRoute>
}
/>
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
<Header />
<div className="pt-16 sm:pt-20">
<Routes>
<Route path="/" element={<Index />} />
<Route path="/auth" element={<Auth />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</BrowserRouter>
</TooltipProvider>
</AuthProvider>

View File

@ -1,76 +1,130 @@
import { useState } from 'react';
import { Task } from '@/utils/mockData';
import { Button } from '@/components/ui/button';
import { FileDown, Check } from 'lucide-react';
import { toast } from 'sonner';
import { Download } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
interface Task {
id: string;
task_name: string;
description: string;
priority: string;
due_date: string | null;
status: string;
}
interface ExportOptionsProps {
tasks: Task[];
}
const ExportOptions = ({ tasks }: ExportOptionsProps) => {
const [isExporting, setIsExporting] = useState(false);
export const ExportOptions = ({ tasks }: ExportOptionsProps) => {
const [exporting, setExporting] = useState(false);
const exportToCSV = () => {
const exportToCsv = () => {
try {
setIsExporting(true);
setExporting(true);
const headers = ['Task Name', 'Description', 'Priority', 'Due Date', 'Status'];
// Create CSV content
const headers = ['Description', 'Department', 'Deadline', 'Priority', 'Status', 'Context'];
const csvContent = [
headers.join(','),
...tasks.map(task => [
`"${task.description.replace(/"/g, '""')}"`,
`"${task.department.replace(/"/g, '""')}"`,
task.deadline,
task.priority,
task.status,
`"${task.context.replace(/"/g, '""')}"`
].join(','))
...tasks.map(task => {
return [
`"${task.task_name.replace(/"/g, '""')}"`,
`"${task.description?.replace(/"/g, '""') || ''}"`,
`"${task.priority}"`,
`"${task.due_date ? new Date(task.due_date).toLocaleDateString() : ''}"`,
`"${task.status}"`
].join(',');
})
].join('\n');
// Create a Blob and download
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `secupolicy_tasks_${new Date().toISOString().split('T')[0]}.csv`);
link.setAttribute('download', `tasks_export_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success('CSV file exported successfully');
} catch (error) {
console.error('Export error:', error);
toast.error('Failed to export CSV file');
console.error('Error exporting to CSV:', error);
} finally {
setIsExporting(false);
setExporting(false);
}
};
const exportToPdf = () => {
try {
setExporting(true);
const doc = new jsPDF();
// Add title
doc.setFontSize(16);
doc.text('Policy Tasks Report', 14, 20);
// Add date
doc.setFontSize(10);
doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 14, 26);
// Prepare data for table
const tableData = tasks.map(task => [
task.task_name,
task.description?.length > 60
? task.description.substring(0, 60) + '...'
: task.description || '',
task.priority,
task.due_date ? new Date(task.due_date).toLocaleDateString() : '',
task.status
]);
// Create table
autoTable(doc, {
head: [['Task Name', 'Description', 'Priority', 'Due Date', 'Status']],
body: tableData,
startY: 32,
headStyles: { fillColor: [66, 66, 66] },
margin: { top: 30 },
styles: { overflow: 'linebreak' },
columnStyles: {
0: { cellWidth: 50 },
1: { cellWidth: 70 },
},
});
// Save the PDF
doc.save(`tasks_export_${new Date().toISOString().split('T')[0]}.pdf`);
} catch (error) {
console.error('Error exporting to PDF:', error);
} finally {
setExporting(false);
}
};
return (
<div className="w-full max-w-xs mx-auto">
<Button
onClick={exportToCSV}
disabled={isExporting || tasks.length === 0}
className="w-full rounded-full"
variant="outline"
>
{isExporting ? (
<>
<Check className="w-4 h-4 mr-2 animate-pulse" />
Exporting...
</>
) : (
<>
<FileDown className="w-4 h-4 mr-2" />
Export to CSV
</>
)}
</Button>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={exportToCsv} disabled={exporting || tasks.length === 0}>
Export as CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={exportToPdf} disabled={exporting || tasks.length === 0}>
Export as PDF
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default ExportOptions;

View File

@ -1,157 +1,156 @@
import { useState } from 'react';
import { Task } from '@/utils/mockData';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Calendar, AlertCircle, Users, CornerDownRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
MoreVertical,
Calendar,
FileText,
Check,
X,
AlertCircle,
Clock
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface Task {
id: string;
task_name: string;
description: string;
priority: string;
due_date: string | null;
status: string;
}
interface TaskCardProps {
task: Task;
onStatusChange: (id: string, status: 'Open' | 'In Progress' | 'Completed') => void;
onPriorityChange: (id: string, priority: 'High' | 'Medium' | 'Low') => void;
documentName: string;
onStatusChange: (status: string) => void;
}
const TaskCard = ({ task, onStatusChange, onPriorityChange }: TaskCardProps) => {
const [isExpanded, setIsExpanded] = useState(false);
export const TaskCard = ({ task, documentName, onStatusChange }: TaskCardProps) => {
const [expanded, setExpanded] = useState(false);
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'High':
return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200 dark:border-red-800/30';
case 'Medium':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800/30';
case 'Low':
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 border-green-200 dark:border-green-800/30';
const getPriorityIcon = () => {
switch (task.priority.toLowerCase()) {
case 'high':
return <AlertCircle className="h-4 w-4 text-red-500" />;
case 'medium':
return <Clock className="h-4 w-4 text-amber-500" />;
case 'low':
return <Check className="h-4 w-4 text-green-500" />;
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-800/30';
return null;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'Completed':
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 border-green-200 dark:border-green-800/30';
case 'In Progress':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200 dark:border-blue-800/30';
case 'Open':
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-800/30';
const getPriorityBadge = () => {
switch (task.priority.toLowerCase()) {
case 'high':
return <Badge variant="destructive" className="ml-2">{task.priority}</Badge>;
case 'medium':
return <Badge variant="default" className="ml-2">{task.priority}</Badge>;
case 'low':
return <Badge variant="secondary" className="ml-2">{task.priority}</Badge>;
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-800/30';
return <Badge variant="outline" className="ml-2">{task.priority}</Badge>;
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
const getStatusBadge = () => {
switch (task.status.toLowerCase()) {
case 'completed':
return <Badge className="bg-green-500 ml-2">{task.status}</Badge>;
case 'in progress':
return <Badge className="bg-blue-500 ml-2">{task.status}</Badge>;
case 'open':
return <Badge variant="outline" className="ml-2">{task.status}</Badge>;
default:
return <Badge variant="outline" className="ml-2">{task.status}</Badge>;
}
};
const calculateDaysLeft = (dateString: string) => {
const today = new Date();
const deadline = new Date(dateString);
const diffTime = deadline.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const daysLeft = calculateDaysLeft(task.deadline);
const isUrgent = daysLeft <= 14 && daysLeft > 0;
const isOverdue = daysLeft < 0;
return (
<div className={cn(
"glass-panel p-5 transition-all duration-300 cursor-pointer hover-lift",
task.status === 'Completed' && "opacity-70",
isExpanded && "ring-1 ring-primary/20"
)}
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex flex-col">
<div className="flex justify-between items-start mb-3">
<h3 className="font-medium text-base">{task.description}</h3>
<Badge variant="outline" className={`ml-2 ${getPriorityColor(task.priority)}`}>
{task.priority}
</Badge>
</div>
<div className="flex flex-wrap gap-3 text-sm text-muted-foreground mb-3">
<div className="flex items-center">
<Users className="w-4 h-4 mr-1" />
{task.department}
</div>
<div className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
<span className={cn(
isUrgent && "text-amber-600 dark:text-amber-400",
isOverdue && "text-red-600 dark:text-red-400"
)}>
{formatDate(task.deadline)}
{isUrgent && ` (${daysLeft} days left)`}
{isOverdue && ` (Overdue by ${Math.abs(daysLeft)} days)`}
</span>
</div>
</div>
<div className="flex justify-between items-center">
<Badge variant="outline" className={getStatusColor(task.status)}>
{task.status}
</Badge>
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
<Select
defaultValue={task.priority}
onValueChange={(value) => onPriorityChange(task.id, value as 'High' | 'Medium' | 'Low')}
>
<SelectTrigger className="w-[110px] h-8 text-xs">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="High">High</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="Low">Low</SelectItem>
</SelectContent>
</Select>
<Select
defaultValue={task.status}
onValueChange={(value) => onStatusChange(task.id, value as 'Open' | 'In Progress' | 'Completed')}
>
<SelectTrigger className="w-[110px] h-8 text-xs">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border animate-fade-up">
<div className="flex items-start gap-2 text-sm text-muted-foreground">
<CornerDownRight className="w-4 h-4 mt-0.5 shrink-0" />
<div>
<p className="font-medium mb-1">Context from Policy:</p>
<p>{task.context}</p>
</div>
<Card className={`overflow-hidden transition-shadow hover:shadow-md ${
task.status === 'Completed' ? 'bg-muted/30' : ''
}`}>
<CardHeader className="p-4 pb-0 flex flex-row items-start justify-between">
<div>
<CardTitle className="text-base line-clamp-2">
{task.task_name}
</CardTitle>
<div className="flex flex-wrap items-center text-xs text-muted-foreground mt-1">
<div className="flex items-center mr-3">
{getPriorityIcon()}
<span className="ml-1">{task.priority}</span>
</div>
<div className="flex items-center">
<FileText className="h-3 w-3 mr-1" />
<span className="truncate max-w-[120px]">{documentName}</span>
</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{task.status !== 'Completed' && (
<DropdownMenuItem onClick={() => onStatusChange('Completed')}>
<Check className="mr-2 h-4 w-4" />
Mark as completed
</DropdownMenuItem>
)}
{task.status === 'Completed' && (
<DropdownMenuItem onClick={() => onStatusChange('Open')}>
<X className="mr-2 h-4 w-4" />
Mark as open
</DropdownMenuItem>
)}
{task.status !== 'In Progress' && task.status !== 'Completed' && (
<DropdownMenuItem onClick={() => onStatusChange('In Progress')}>
<Clock className="mr-2 h-4 w-4" />
Mark as in progress
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</CardHeader>
<CardContent className="p-4">
<div className={`text-sm ${expanded ? '' : 'line-clamp-2'}`}>
{task.description}
</div>
{task.description && task.description.length > 100 && (
<Button
variant="link"
className="p-0 h-auto text-xs mt-1"
onClick={() => setExpanded(!expanded)}
>
{expanded ? 'Show less' : 'Show more'}
</Button>
)}
</div>
</div>
</CardContent>
<CardFooter className="p-4 pt-0 flex justify-between">
<div className="flex items-center text-xs">
{task.due_date && (
<div className="flex items-center mr-2">
<Calendar className="h-3 w-3 mr-1" />
<span>
{new Date(task.due_date).toLocaleDateString()}
</span>
</div>
)}
</div>
{getStatusBadge()}
</CardFooter>
</Card>
);
};
export default TaskCard;

View File

@ -1,408 +1,418 @@
import { useState, useEffect } from 'react';
import { Task } from '@/utils/mockData';
import TaskCard from './TaskCard';
import { Input } from '@/components/ui/input';
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 { Search, Filter, CheckCircle2, List, X } from 'lucide-react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuCheckboxItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { cn } from '@/lib/utils';
} 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 TaskListProps {
tasks: Task[];
onTaskUpdate: (updatedTasks: Task[]) => void;
interface Task {
id: string;
task_name: string;
description: string;
priority: string;
due_date: string | null;
status: string;
created_at: string;
upload_id: string;
}
const TaskList = ({ tasks, onTaskUpdate }: TaskListProps) => {
const [filteredTasks, setFilteredTasks] = useState<Task[]>(tasks);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('deadline');
const [filterPriority, setFilterPriority] = useState<string[]>([]);
const [filterStatus, setFilterStatus] = useState<string[]>([]);
const [filterDepartment, setFilterDepartment] = useState<string[]>([]);
const [isFiltering, setIsFiltering] = useState(false);
interface Upload {
id: string;
file_name: string;
}
const handleStatusChange = (id: string, status: 'Open' | 'In Progress' | 'Completed') => {
const updatedTasks = tasks.map(task =>
task.id === id ? { ...task, status } : task
);
onTaskUpdate(updatedTasks);
};
const handlePriorityChange = (id: string, priority: 'High' | 'Medium' | 'Low') => {
const updatedTasks = tasks.map(task =>
task.id === id ? { ...task, priority } : task
);
onTaskUpdate(updatedTasks);
};
const uniqueDepartments = [...new Set(tasks.map(task => task.department))];
const sortTasks = (tasksToSort: Task[]) => {
return [...tasksToSort].sort((a, b) => {
switch (sortBy) {
case 'deadline':
return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
case 'priority':
const priorityOrder = { High: 0, Medium: 1, Low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
case 'department':
return a.department.localeCompare(b.department);
default:
return 0;
}
});
};
const filterTasks = () => {
let result = tasks;
// Search filter
if (searchQuery) {
result = result.filter(task =>
task.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
task.department.toLowerCase().includes(searchQuery.toLowerCase()) ||
task.context.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Priority filter
if (filterPriority.length > 0) {
result = result.filter(task => filterPriority.includes(task.priority));
}
// Status filter
if (filterStatus.length > 0) {
result = result.filter(task => filterStatus.includes(task.status));
}
// Department filter
if (filterDepartment.length > 0) {
result = result.filter(task => filterDepartment.includes(task.department));
}
// Sort the filtered results
return sortTasks(result);
};
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(() => {
setFilteredTasks(filterTasks());
setIsFiltering(
searchQuery !== '' ||
filterPriority.length > 0 ||
filterStatus.length > 0 ||
filterDepartment.length > 0
);
}, [tasks, searchQuery, sortBy, filterPriority, filterStatus, filterDepartment]);
if (user) {
fetchTasks();
fetchUploads();
}
}, [user]);
const clearFilters = () => {
setSearchQuery('');
setFilterPriority([]);
setFilterStatus([]);
setFilterDepartment([]);
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 completedTasksCount = tasks.filter(task => task.status === 'Completed').length;
const taskCompletion = Math.round((completedTasksCount / tasks.length) * 100) || 0;
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="w-full max-w-5xl mx-auto">
<div className="flex flex-col mb-8">
<div className="flex flex-col lg:flex-row lg:items-center justify-between mb-4 gap-4">
<div>
<h2 className="text-2xl font-semibold">Tasks</h2>
<p className="text-muted-foreground">
{tasks.length} tasks extracted, {completedTasksCount} completed ({taskCompletion}%)
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<Button variant="outline" className="rounded-full" onClick={clearFilters}
disabled={!isFiltering}>
<CheckCircle2 className="w-4 h-4 mr-2" />
View All Tasks
</Button>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[180px] rounded-full">
<List className="w-4 h-4 mr-2" />
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="deadline">Sort by Deadline</SelectItem>
<SelectItem value="priority">Sort by Priority</SelectItem>
<SelectItem value="department">Sort by Department</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 mb-2">
<div className="relative flex-grow">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 rounded-full"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
)}
</div>
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="rounded-full">
<Filter className="w-4 h-4 mr-2" />
Priority
{filterPriority.length > 0 && (
<Badge className="ml-2 bg-primary/10 text-primary border-none">
{filterPriority.length}
</Badge>
)}
<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>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuCheckboxItem
checked={filterPriority.includes('High')}
onCheckedChange={(checked) => {
if (checked) {
setFilterPriority([...filterPriority, 'High']);
} else {
setFilterPriority(filterPriority.filter(p => p !== 'High'));
}
}}
>
High
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filterPriority.includes('Medium')}
onCheckedChange={(checked) => {
if (checked) {
setFilterPriority([...filterPriority, 'Medium']);
} else {
setFilterPriority(filterPriority.filter(p => p !== 'Medium'));
}
}}
>
Medium
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filterPriority.includes('Low')}
onCheckedChange={(checked) => {
if (checked) {
setFilterPriority([...filterPriority, 'Low']);
} else {
setFilterPriority(filterPriority.filter(p => p !== 'Low'));
}
}}
>
Low
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<ExportOptions tasks={filteredTasks} />
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="rounded-full">
<Filter className="w-4 h-4 mr-2" />
Status
{filterStatus.length > 0 && (
<Badge className="ml-2 bg-primary/10 text-primary border-none">
{filterStatus.length}
</Badge>
)}
<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>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuCheckboxItem
checked={filterStatus.includes('Open')}
onCheckedChange={(checked) => {
if (checked) {
setFilterStatus([...filterStatus, 'Open']);
} else {
setFilterStatus(filterStatus.filter(s => s !== 'Open'));
}
}}
>
Open
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filterStatus.includes('In Progress')}
onCheckedChange={(checked) => {
if (checked) {
setFilterStatus([...filterStatus, 'In Progress']);
} else {
setFilterStatus(filterStatus.filter(s => s !== 'In Progress'));
}
}}
>
In Progress
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filterStatus.includes('Completed')}
onCheckedChange={(checked) => {
if (checked) {
setFilterStatus([...filterStatus, 'Completed']);
} else {
setFilterStatus(filterStatus.filter(s => s !== 'Completed'));
}
}}
>
Completed
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="rounded-full">
<Filter className="w-4 h-4 mr-2" />
Department
{filterDepartment.length > 0 && (
<Badge className="ml-2 bg-primary/10 text-primary border-none">
{filterDepartment.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{uniqueDepartments.map(dept => (
<DropdownMenuCheckboxItem
key={dept}
checked={filterDepartment.includes(dept)}
onCheckedChange={(checked) => {
if (checked) {
setFilterDepartment([...filterDepartment, dept]);
} else {
setFilterDepartment(filterDepartment.filter(d => d !== dept));
}
}}
{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'}
>
{dept}
</DropdownMenuCheckboxItem>
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)}
/>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{isFiltering && (
<div className="flex flex-wrap gap-2 my-3">
{filterPriority.map(priority => (
<Badge
key={`priority-${priority}`}
variant="outline"
className={cn(
"rounded-full px-3 py-1 cursor-pointer hover:bg-background",
priority === 'High' && "bg-red-100/50 dark:bg-red-900/20",
priority === 'Medium' && "bg-yellow-100/50 dark:bg-yellow-900/20",
priority === 'Low' && "bg-green-100/50 dark:bg-green-900/20"
)}
onClick={() => {
setFilterPriority(filterPriority.filter(p => p !== priority));
}}
>
{priority} <X className="ml-1 w-3 h-3" />
</Badge>
))}
{filterStatus.map(status => (
<Badge
key={`status-${status}`}
variant="outline"
className={cn(
"rounded-full px-3 py-1 cursor-pointer hover:bg-background",
status === 'Completed' && "bg-green-100/50 dark:bg-green-900/20",
status === 'In Progress' && "bg-blue-100/50 dark:bg-blue-900/20"
)}
onClick={() => {
setFilterStatus(filterStatus.filter(s => s !== status));
}}
>
{status} <X className="ml-1 w-3 h-3" />
</Badge>
))}
{filterDepartment.map(dept => (
<Badge
key={`dept-${dept}`}
variant="outline"
className="rounded-full px-3 py-1 cursor-pointer hover:bg-background"
onClick={() => {
setFilterDepartment(filterDepartment.filter(d => d !== dept));
}}
>
{dept} <X className="ml-1 w-3 h-3" />
</Badge>
))}
{searchQuery && (
<Badge
variant="outline"
className="rounded-full px-3 py-1 cursor-pointer hover:bg-background"
onClick={() => setSearchQuery('')}
>
Search: {searchQuery} <X className="ml-1 w-3 h-3" />
</Badge>
</div>
)}
<Button
variant="ghost"
size="sm"
className="text-xs h-7 px-2 rounded-full"
onClick={clearFilters}
>
Clear all
</Button>
</div>
)}
</div>
{filteredTasks.length === 0 ? (
<div className="p-12 text-center glass-panel">
<p className="text-lg font-medium">No tasks match your filters</p>
<p className="text-muted-foreground mt-2">Try adjusting your search or filters</p>
<Button
variant="outline"
className="mt-4 rounded-full"
onClick={clearFilters}
>
Clear all filters
</Button>
</div>
) : (
<div className="grid grid-cols-1 gap-4">
{filteredTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
onPriorityChange={handlePriorityChange}
/>
))}
</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>
);
};

View File

@ -1,168 +1,206 @@
import { useState, useRef } from 'react';
import { Upload, FileType, FileUp, AlertCircle, Check } from 'lucide-react';
import { useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
import { Upload, FileIcon, Loader2, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { Progress } from '@/components/ui/progress';
interface UploadSectionProps {
onUploadComplete: (fileName: string) => void;
onUploadComplete: () => void;
}
const UploadSection = ({ onUploadComplete }: UploadSectionProps) => {
const [isDragging, setIsDragging] = useState(false);
const { user } = useAuth();
const { toast } = useToast();
const [file, setFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
};
const allowedFileTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
const handleDragLeave = () => {
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
const droppedFile = e.dataTransfer.files[0];
if (isValidFile(droppedFile)) {
setFile(droppedFile);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile && isValidFile(selectedFile)) {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const selectedFile = e.target.files[0];
if (!allowedFileTypes.includes(selectedFile.type)) {
toast({
title: 'Invalid file type',
description: 'Please upload a PDF or DOCX file',
variant: 'destructive',
});
return;
}
setFile(selectedFile);
}
};
const isValidFile = (file: File) => {
const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
if (!validTypes.includes(file.type)) {
toast.error("Please upload a PDF or Word document");
return false;
}
if (file.size > 10 * 1024 * 1024) { // 10MB limit
toast.error("File size exceeds 10MB limit");
return false;
}
return true;
const clearFile = () => {
setFile(null);
};
const handleUpload = async () => {
if (!file) return;
if (!file || !user) return;
setIsUploading(true);
// Simulate file upload with a delay
try {
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success(`${file.name} uploaded successfully`);
onUploadComplete(file.name);
} catch (error) {
toast.error("Upload failed. Please try again.");
console.error("Upload error:", error);
setUploading(true);
setUploadProgress(0);
// First, create an entry in the uploads table
const fileName = file.name;
const fileType = file.type;
const filePath = `${user.id}/${Date.now()}_${fileName}`;
const { data: uploadData, error: uploadError } = await supabase
.from('uploads')
.insert({
user_id: user.id,
file_name: fileName,
file_path: filePath,
file_type: fileType,
status: 'Processing'
})
.select()
.single();
if (uploadError) throw uploadError;
// Simulate progress for a better UX
const progressInterval = setInterval(() => {
setUploadProgress((prev) => {
const increment = Math.random() * 10;
const newProgress = Math.min(prev + increment, 90);
return newProgress;
});
}, 300);
// Upload the file to storage
const { error: storageError } = await supabase
.storage
.from('documents')
.upload(filePath, file);
clearInterval(progressInterval);
if (storageError) throw storageError;
// Call the edge function to process the document
const processingResponse = await supabase.functions.invoke('process-document', {
body: {
uploadId: uploadData.id,
userId: user.id,
filePath: filePath,
fileType: fileType
}
});
if (processingResponse.error) throw new Error(processingResponse.error.message);
setUploadProgress(100);
toast({
title: 'Upload successful',
description: 'Your document is now being processed',
});
// Clear file and reset progress after 1 second
setTimeout(() => {
setFile(null);
setUploadProgress(0);
onUploadComplete();
}, 1000);
} catch (error: any) {
console.error('Upload error:', error);
toast({
title: 'Upload failed',
description: error.message || 'An error occurred while uploading the document',
variant: 'destructive',
});
} finally {
setIsUploading(false);
setUploading(false);
}
};
return (
<div className="w-full max-w-3xl mx-auto">
<div className="text-center mb-8">
<h2 className="text-2xl font-semibold mb-2">Upload Your Policy Document</h2>
<p className="text-muted-foreground">Upload a PDF or Word document to extract actionable tasks</p>
</div>
<div
className={`
glass-panel p-8
border-2 border-dashed
flex flex-col items-center justify-center
transition-all duration-300
${isDragging ? 'border-primary bg-primary/5' : 'border-border'}
${file ? 'border-green-500/30 bg-green-50/30 dark:bg-green-900/10' : ''}
`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
className="hidden"
accept=".pdf,.doc,.docx"
/>
{!file ? (
<>
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
<Upload className="w-8 h-8 text-primary" />
</div>
<h3 className="text-lg font-medium mb-2">Drag and drop your file here</h3>
<p className="text-sm text-muted-foreground mb-6 max-w-md text-center">
Support for PDF and Word documents up to 10MB
</p>
<Button
onClick={() => fileInputRef.current?.click()}
variant="outline"
className="rounded-full"
>
<FileUp className="w-4 h-4 mr-2" />
Browse Files
<div className="space-y-4">
{!file ? (
<div className="border-2 border-dashed rounded-lg p-8 text-center">
<label className="flex flex-col items-center justify-center cursor-pointer space-y-3">
<Upload className="h-10 w-10 text-muted-foreground" />
<span className="text-lg font-medium">Drag and drop or click to upload</span>
<span className="text-sm text-muted-foreground">PDF or DOCX files only</span>
<input
type="file"
className="hidden"
accept=".pdf,.docx,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
onChange={handleFileChange}
/>
<Button variant="outline" type="button">
Select file
</Button>
</>
) : (
<div className="w-full">
<div className="flex items-center justify-center mb-6">
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center">
<FileType className="w-6 h-6 text-green-600 dark:text-green-400" />
</label>
</div>
) : (
<div className="border rounded-lg p-6 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-primary/10 p-2 rounded">
<FileIcon className="h-6 w-6 text-primary" />
</div>
<div>
<p className="font-medium">{file.name}</p>
<p className="text-sm text-muted-foreground">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</div>
<div className="text-center mb-6">
<h3 className="text-lg font-medium mb-1 flex items-center justify-center">
{file.name}
<Check className="w-4 h-4 text-green-500 ml-2" />
</h3>
<p className="text-sm text-muted-foreground">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
<div className="flex justify-center space-x-3">
<Button
variant="outline"
size="sm"
className="rounded-full"
onClick={() => {
setFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}}
>
Change
</Button>
<Button
onClick={handleUpload}
disabled={isUploading}
size="sm"
className="rounded-full min-w-24"
>
{isUploading ? 'Processing...' : 'Analyze Document'}
</Button>
</div>
<Button
variant="ghost"
size="icon"
onClick={clearFile}
disabled={uploading}
>
<X className="h-4 w-4" />
</Button>
</div>
)}
</div>
{uploading && (
<div className="space-y-2">
<Progress value={uploadProgress} className="h-2" />
<p className="text-xs text-muted-foreground">{uploadProgress.toFixed(0)}% complete</p>
</div>
)}
<div className="flex justify-end">
<Button
onClick={handleUpload}
disabled={uploading}
className="flex items-center gap-2"
>
{uploading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="h-4 w-4" />
Upload Document
</>
)}
</Button>
</div>
</div>
)}
<div className="mt-4 text-center text-sm text-muted-foreground flex items-center justify-center">
<AlertCircle className="w-4 h-4 mr-2" />
Your documents are processed securely and never stored permanently
<div className="text-sm text-muted-foreground">
<p>After uploading, your document will be automatically processed to extract actionable tasks.</p>
</div>
</div>
);

View File

@ -1,26 +1,37 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
value?: number
max?: number
indicatorClassName?: string
}
>(({ className, value, max = 100, indicatorClassName, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<div
className={cn(
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName
)}
style={{
transform: `translateX(-${100 - ((value || 0) / max) * 100}%)`,
}}
/>
</div>
)
})
Progress.displayName = "Progress"
export { Progress }

View File

@ -1,10 +1,11 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
React.HTMLTableProps
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
@ -18,7 +19,7 @@ Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
React.HTMLTableSectionProps
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
@ -26,7 +27,7 @@ TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
React.HTMLTableSectionProps
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
@ -38,14 +39,11 @@ TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
React.HTMLTableSectionProps
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
))
@ -53,7 +51,7 @@ TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
React.HTMLTableRowProps
>(({ className, ...props }, ref) => (
<tr
ref={ref}

View File

@ -36,6 +36,86 @@ export type Database = {
}
Relationships: []
}
tasks: {
Row: {
created_at: string
description: string | null
due_date: string | null
id: string
priority: string | null
status: string
task_name: string
updated_at: string
upload_id: string
user_id: string
}
Insert: {
created_at?: string
description?: string | null
due_date?: string | null
id?: string
priority?: string | null
status?: string
task_name: string
updated_at?: string
upload_id: string
user_id: string
}
Update: {
created_at?: string
description?: string | null
due_date?: string | null
id?: string
priority?: string | null
status?: string
task_name?: string
updated_at?: string
upload_id?: string
user_id?: string
}
Relationships: [
{
foreignKeyName: "tasks_upload_id_fkey"
columns: ["upload_id"]
isOneToOne: false
referencedRelation: "uploads"
referencedColumns: ["id"]
},
]
}
uploads: {
Row: {
created_at: string
file_name: string
file_path: string
file_type: string
id: string
status: string
updated_at: string
user_id: string
}
Insert: {
created_at?: string
file_name: string
file_path: string
file_type: string
id?: string
status?: string
updated_at?: string
user_id: string
}
Update: {
created_at?: string
file_name?: string
file_path?: string
file_type?: string
id?: string
status?: string
updated_at?: string
user_id?: string
}
Relationships: []
}
}
Views: {
[_ in never]: never

View File

@ -1,167 +1,196 @@
import { useState } from 'react';
import Header from '@/components/Header';
import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useToast } from '@/hooks/use-toast';
import { supabase } from '@/integrations/supabase/client';
import { Upload, FilePlus, FileText, AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import UploadSection from '@/components/UploadSection';
import TaskList from '@/components/TaskList';
import ExportOptions from '@/components/ExportOptions';
import { Task, generateTasks } from '@/utils/mockData';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AlertCircle } from 'lucide-react';
import { toast } from 'sonner';
const Dashboard = () => {
const [uploadCompleted, setUploadCompleted] = useState(false);
const [documentName, setDocumentName] = useState('');
const [tasks, setTasks] = useState<Task[]>([]);
const { user } = useAuth();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState<'upload' | 'tasks'>('upload');
const [uploadCount, setUploadCount] = useState(0);
const [taskCount, setTaskCount] = useState(0);
const [recentUploads, setRecentUploads] = useState<any[]>([]);
const [processingUploads, setProcessingUploads] = useState<number>(0);
const handleUploadComplete = (fileName: string) => {
// Simulate task extraction with a delay
setTimeout(() => {
const extractedTasks = generateTasks(fileName);
setTasks(extractedTasks);
setDocumentName(fileName);
setUploadCompleted(true);
toast.success(`${extractedTasks.length} tasks extracted successfully`, {
description: 'Tasks are now ready for review and prioritization'
useEffect(() => {
if (user) {
fetchDashboardData();
}
}, [user]);
const fetchDashboardData = async () => {
try {
// Get upload count
const { count: uploadCountData, error: uploadCountError } = await supabase
.from('uploads')
.select('*', { count: 'exact', head: true })
.eq('user_id', user?.id);
if (uploadCountError) throw uploadCountError;
setUploadCount(uploadCountData || 0);
// Get task count
const { count: taskCountData, error: taskCountError } = await supabase
.from('tasks')
.select('*', { count: 'exact', head: true })
.eq('user_id', user?.id);
if (taskCountError) throw taskCountError;
setTaskCount(taskCountData || 0);
// Get recent uploads
const { data: recentUploadsData, error: recentUploadsError } = await supabase
.from('uploads')
.select('*')
.eq('user_id', user?.id)
.order('created_at', { ascending: false })
.limit(5);
if (recentUploadsError) throw recentUploadsError;
setRecentUploads(recentUploadsData || []);
// Get processing uploads count
const { count: processingCount, error: processingError } = await supabase
.from('uploads')
.select('*', { count: 'exact', head: true })
.eq('user_id', user?.id)
.eq('status', 'Processing');
if (processingError) throw processingError;
setProcessingUploads(processingCount || 0);
} catch (error) {
console.error('Error fetching dashboard data:', error);
toast({
title: 'Error',
description: 'Failed to load dashboard data',
variant: 'destructive',
});
}, 1500);
};
const handleTaskUpdate = (updatedTasks: Task[]) => {
setTasks(updatedTasks);
}
};
return (
<div className="min-h-screen flex flex-col">
<Header />
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<main className="flex-grow pt-20">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Tabs defaultValue="upload" className="w-full">
<div className="flex justify-center mb-8">
<TabsList className="rounded-full h-12">
<TabsTrigger value="upload" className="rounded-full px-6">
Upload Document
</TabsTrigger>
<TabsTrigger
value="tasks"
className="rounded-full px-6"
disabled={!uploadCompleted}
onClick={() => {
if (!uploadCompleted) {
toast.error("Please upload a document first");
}
}}
>
Review Tasks
</TabsTrigger>
<TabsTrigger
value="export"
className="rounded-full px-6"
disabled={!uploadCompleted}
onClick={() => {
if (!uploadCompleted) {
toast.error("Please upload a document first");
}
}}
>
Export
</TabsTrigger>
</TabsList>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Documents</CardTitle>
<CardDescription>Total uploaded documents</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{uploadCount}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Tasks</CardTitle>
<CardDescription>Extracted actionable items</CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{taskCount}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between space-y-0">
<div>
<CardTitle className="text-lg">Processing</CardTitle>
<CardDescription>Documents being analyzed</CardDescription>
</div>
<TabsContent value="upload" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<UploadSection onUploadComplete={handleUploadComplete} />
{uploadCompleted && (
<div className="flex justify-center mt-6">
<div className="glass-panel px-4 py-2 text-sm flex items-center">
<AlertCircle className="w-4 h-4 mr-2 text-green-500" />
Document processed successfully! Continue to Review Tasks.
</div>
</div>
)}
</TabsContent>
<TabsContent value="tasks" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
{uploadCompleted ? (
<>
<div className="mb-8 text-center">
<div className="mb-2 text-sm font-medium text-muted-foreground">
Extracted tasks from
{processingUploads > 0 && (
<div className="rounded-full bg-amber-500/20 p-1">
<AlertTriangle className="h-5 w-5 text-amber-500" />
</div>
)}
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{processingUploads}</p>
</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4 mb-8">
<Button
variant={activeTab === 'upload' ? 'default' : 'outline'}
onClick={() => setActiveTab('upload')}
className="flex items-center gap-2"
>
<Upload className="h-4 w-4" />
Upload Document
</Button>
<Button
variant={activeTab === 'tasks' ? 'default' : 'outline'}
onClick={() => setActiveTab('tasks')}
className="flex items-center gap-2"
>
<FileText className="h-4 w-4" />
View Tasks
</Button>
</div>
{activeTab === 'upload' ? (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Upload Policy Document</CardTitle>
<CardDescription>
Upload your IT security or compliance policy documents to extract actionable tasks
</CardDescription>
</CardHeader>
<CardContent>
<UploadSection onUploadComplete={fetchDashboardData} />
</CardContent>
</Card>
{recentUploads.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Recent Uploads</CardTitle>
<CardDescription>
Your most recent document uploads
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentUploads.map((upload) => (
<div key={upload.id} className="flex items-center justify-between border-b pb-4 last:border-0">
<div className="flex items-center gap-3">
<FilePlus className="h-6 w-6 text-muted-foreground" />
<div>
<p className="font-medium">{upload.file_name}</p>
<p className="text-sm text-muted-foreground">
{new Date(upload.created_at).toLocaleDateString()}
</p>
</div>
</div>
<div>
<span className={`px-2 py-1 rounded-full text-xs ${
upload.status === 'Completed'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400'
}`}>
{upload.status}
</span>
</div>
</div>
<h3 className="text-xl font-semibold">{documentName}</h3>
</div>
<TaskList tasks={tasks} onTaskUpdate={handleTaskUpdate} />
</>
) : (
<div className="text-center p-12">
<p className="text-muted-foreground">Please upload a document first</p>
))}
</div>
)}
</TabsContent>
<TabsContent value="export" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
{uploadCompleted ? (
<div className="max-w-3xl mx-auto text-center">
<div className="mb-8">
<h3 className="text-2xl font-semibold mb-2">Export Task List</h3>
<p className="text-muted-foreground">
Download your tasks to share with your team or import into your project management tool
</p>
</div>
<div className="glass-panel p-8 mb-8">
<div className="text-left mb-6">
<h4 className="font-medium mb-1">Export Summary</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li>Document: {documentName}</li>
<li>Total Tasks: {tasks.length}</li>
<li>Open Tasks: {tasks.filter(t => t.status === 'Open').length}</li>
<li>In Progress Tasks: {tasks.filter(t => t.status === 'In Progress').length}</li>
<li>Completed Tasks: {tasks.filter(t => t.status === 'Completed').length}</li>
<li>High Priority Tasks: {tasks.filter(t => t.priority === 'High').length}</li>
</ul>
</div>
<ExportOptions tasks={tasks} />
</div>
<p className="text-sm text-muted-foreground mt-4">
Need to make changes? Go back to the <TabsTrigger value="tasks" className="text-primary underline bg-transparent p-0 h-auto">Review Tasks</TabsTrigger> tab.
</p>
</div>
) : (
<div className="text-center p-12">
<p className="text-muted-foreground">Please upload a document first</p>
</div>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
)}
</div>
</main>
<footer className="bg-muted/30 py-6">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
<div className="text-sm text-muted-foreground">
© 2024 SecuPolicy. All rights reserved.
</div>
<div className="flex space-x-6">
<a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Privacy Policy
</a>
<a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Terms of Service
</a>
<a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Contact
</a>
</div>
</div>
</div>
</footer>
) : (
<TaskList />
)}
</div>
);
};

View File

@ -0,0 +1,2 @@
ffwcnetryatmpcnjkegg

View File

@ -0,0 +1,155 @@
import "https://deno.land/x/xhr@0.1.0/mod.ts";
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.38.1";
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
const OPENAI_API_KEY = Deno.env.get('OPENAI_API_KEY');
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || "https://ffwcnetryatmpcnjkegg.supabase.co";
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || "";
serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const { uploadId, userId, filePath, fileType } = await req.json();
// Create Supabase client with service role key to bypass RLS
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
auth: {
persistSession: false,
},
});
// Fetch the file content from Supabase Storage
const { data: fileData, error: fileError } = await supabase
.storage
.from('documents')
.download(filePath);
if (fileError) {
console.error('Error downloading file:', fileError);
return new Response(
JSON.stringify({ error: 'Error downloading file' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Convert file content to text
const text = await fileData.text();
// Extract tasks using OpenAI
const tasks = await extractTasksWithOpenAI(text, uploadId, userId);
// Save extracted tasks to the database
for (const task of tasks) {
const { error: taskError } = await supabase
.from('tasks')
.insert(task);
if (taskError) {
console.error('Error inserting task:', taskError);
}
}
// Update upload status to 'Completed'
const { error: updateError } = await supabase
.from('uploads')
.update({ status: 'Completed' })
.eq('id', uploadId);
if (updateError) {
console.error('Error updating upload status:', updateError);
return new Response(
JSON.stringify({ error: 'Error updating upload status' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
return new Response(
JSON.stringify({ success: true, tasksExtracted: tasks.length }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Error processing document:', error);
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
});
async function extractTasksWithOpenAI(text: string, uploadId: string, userId: string) {
try {
// Call OpenAI API to extract tasks
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: `
You are a specialized task extractor for IT security and compliance policy documents.
Your job is to identify specific actionable tasks from policy documents.
For each task, extract the following information:
1. Task name (short, clear description of what needs to be done)
2. Description (more detailed explanation)
3. Priority (High, Medium, Low)
4. Due date (if mentioned)
Format your response as a valid JSON array of task objects with these properties:
[
{
"task_name": "string",
"description": "string",
"priority": "string",
"due_date": "ISO date string or null"
}
]
If no due date is mentioned for a task, return null for that field.
Only return the JSON array, nothing else.
`
},
{
role: 'user',
content: text
}
]
}),
});
const data = await response.json();
const extractedTasksJson = data.choices[0].message.content;
// Parse the extracted tasks
const extractedTasks = JSON.parse(extractedTasksJson);
// Format tasks for database insertion
return extractedTasks.map((task: any) => ({
upload_id: uploadId,
user_id: userId,
task_name: task.task_name,
description: task.description,
priority: task.priority,
due_date: task.due_date,
status: 'Open'
}));
} catch (error) {
console.error('Error extracting tasks with OpenAI:', error);
throw new Error('Failed to extract tasks from document');
}
}