Compare commits

..

No commits in common. "2f66cd3f11aed956c24a90d50a6a432980c5a025" and "58c08af3e893fba91a1b2ad70b46b753c5625df5" have entirely different histories.

18 changed files with 917 additions and 2692 deletions

1070
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -3,104 +3,59 @@ import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route, Navigate, useLocation } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AuthProvider, useAuth } from "@/contexts/AuthContext"; import { AuthProvider, useAuth } from "@/contexts/AuthContext";
import Index from "./pages/Index"; import Index from "./pages/Index";
import Auth from "./pages/Auth"; import Auth from "./pages/Auth";
import NotFound from "./pages/NotFound"; import NotFound from "./pages/NotFound";
import Dashboard from "./pages/Dashboard";
import Profile from "./pages/Profile";
import Header from "./components/Header";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
// Enhanced Protected route component that preserves the intended destination // Protected route component
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { user, loading, isAuthenticated } = useAuth(); const { user, loading } = useAuth();
const location = useLocation();
if (loading) { if (loading) {
return <div className="min-h-screen flex items-center justify-center">Loading...</div>; return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
} }
if (!isAuthenticated) { if (!user) {
// Redirect to the auth page but save the current location they were trying to access return <Navigate to="/auth" />;
return <Navigate to="/auth" state={{ from: location }} replace />;
} }
return <>{children}</>; return <>{children}</>;
}; };
// Authentication guard to prevent authenticated users from accessing the auth page const App = () => (
const AuthGuard = ({ children }: { children: React.ReactNode }) => { <QueryClientProvider client={queryClient}>
const { isAuthenticated, loading } = useAuth(); <AuthProvider>
if (loading) {
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
}
if (isAuthenticated) {
// If already logged in, redirect to dashboard
return <Navigate to="/dashboard" replace />;
}
return <>{children}</>;
};
// App needs to be wrapped with BrowserRouter to use the AuthProvider with routing capabilities
const AppContent = () => {
const location = useLocation();
const isDashboard = location.pathname === '/dashboard';
return (
<>
<TooltipProvider> <TooltipProvider>
<Toaster /> <Toaster />
<Sonner /> <Sonner />
{!isDashboard && <Header />} <BrowserRouter>
<div className={isDashboard ? "" : "pt-16 sm:pt-20"}>
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Index />} />
<Route <Route path="/auth" element={<Auth />} />
path="/auth" {/* Protected routes example (Dashboard will be implemented later) */}
element={
<AuthGuard>
<Auth />
</AuthGuard>
}
/>
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Dashboard /> <div className="min-h-screen pt-20 px-4">
</ProtectedRoute> <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>
<Route </div>
path="/profile" </div>
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
</div> </BrowserRouter>
</TooltipProvider> </TooltipProvider>
</> </AuthProvider>
);
};
const App = () => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<AppContent />
</AuthProvider>
</BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@ -1,130 +1,76 @@
import { useState } from 'react'; import { useState } from 'react';
import { Task } from '@/utils/mockData';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Download } from 'lucide-react'; import { FileDown, Check } from 'lucide-react';
import { import { toast } from 'sonner';
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 { interface ExportOptionsProps {
tasks: Task[]; tasks: Task[];
} }
export const ExportOptions = ({ tasks }: ExportOptionsProps) => { const ExportOptions = ({ tasks }: ExportOptionsProps) => {
const [exporting, setExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
const exportToCsv = () => { const exportToCSV = () => {
try { try {
setExporting(true); setIsExporting(true);
const headers = ['Task Name', 'Description', 'Priority', 'Due Date', 'Status'];
// Create CSV content
const headers = ['Description', 'Department', 'Deadline', 'Priority', 'Status', 'Context'];
const csvContent = [ const csvContent = [
headers.join(','), headers.join(','),
...tasks.map(task => { ...tasks.map(task => [
return [ `"${task.description.replace(/"/g, '""')}"`,
`"${task.task_name.replace(/"/g, '""')}"`, `"${task.department.replace(/"/g, '""')}"`,
`"${task.description?.replace(/"/g, '""') || ''}"`, task.deadline,
`"${task.priority}"`, task.priority,
`"${task.due_date ? new Date(task.due_date).toLocaleDateString() : ''}"`, task.status,
`"${task.status}"` `"${task.context.replace(/"/g, '""')}"`
].join(','); ].join(','))
})
].join('\n'); ].join('\n');
// Create a Blob and download
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.setAttribute('href', url); link.setAttribute('href', url);
link.setAttribute('download', `tasks_export_${new Date().toISOString().split('T')[0]}.csv`); link.setAttribute('download', `secupolicy_tasks_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
toast.success('CSV file exported successfully');
} catch (error) { } catch (error) {
console.error('Error exporting to CSV:', error); console.error('Export error:', error);
toast.error('Failed to export CSV file');
} finally { } finally {
setExporting(false); setIsExporting(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 ( return (
<DropdownMenu> <div className="w-full max-w-xs mx-auto">
<DropdownMenuTrigger asChild> <Button
<Button variant="outline" className="flex items-center"> onClick={exportToCSV}
<Download className="mr-2 h-4 w-4" /> disabled={isExporting || tasks.length === 0}
Export className="w-full rounded-full"
</Button> variant="outline"
</DropdownMenuTrigger> >
<DropdownMenuContent align="end"> {isExporting ? (
<DropdownMenuItem onClick={exportToCsv} disabled={exporting || tasks.length === 0}> <>
Export as CSV <Check className="w-4 h-4 mr-2 animate-pulse" />
</DropdownMenuItem> Exporting...
<DropdownMenuItem onClick={exportToPdf} disabled={exporting || tasks.length === 0}> </>
Export as PDF ) : (
</DropdownMenuItem> <>
</DropdownMenuContent> <FileDown className="w-4 h-4 mr-2" />
</DropdownMenu> Export to CSV
</>
)}
</Button>
</div>
); );
}; };
export default ExportOptions;

View File

@ -2,7 +2,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ShieldCheck, Menu, X, User, LogOut, UserCircle } from 'lucide-react'; import { ShieldCheck, Menu, X, User, LogOut } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { import {
DropdownMenu, DropdownMenu,
@ -84,10 +84,7 @@ const Header = () => {
<Link to="/dashboard">Dashboard</Link> <Link to="/dashboard">Dashboard</Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/profile"> <Link to="/profile">Profile</Link>
<UserCircle className="mr-2 h-4 w-4" />
<span>Profile</span>
</Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut()} className="text-destructive"> <DropdownMenuItem onClick={() => signOut()} className="text-destructive">
@ -132,8 +129,7 @@ const Header = () => {
</a> </a>
{user ? ( {user ? (
<> <>
<Link to="/profile" className="flex items-center py-2 text-base font-medium" onClick={() => setMobileMenuOpen(false)}> <Link to="/profile" className="block py-2 text-base font-medium" onClick={() => setMobileMenuOpen(false)}>
<UserCircle className="mr-2 h-5 w-5" />
Profile Profile
</Link> </Link>
<Button <Button

View File

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

View File

@ -1,418 +1,408 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { Task } from '@/utils/mockData';
import { supabase } from '@/integrations/supabase/client'; import TaskCard from './TaskCard';
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 { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import { Search, Filter, CheckCircle2, List, X } from 'lucide-react';
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from "@/components/ui/select";
import { import {
Card, DropdownMenu,
CardContent, DropdownMenuContent,
CardDescription, DropdownMenuCheckboxItem,
CardFooter, DropdownMenuTrigger
CardHeader, } from "@/components/ui/dropdown-menu";
CardTitle, import { cn } from '@/lib/utils';
} from '@/components/ui/card';
import { TaskCard } from '@/components/TaskCard';
import { ExportOptions } from '@/components/ExportOptions';
interface Task { interface TaskListProps {
id: string; tasks: Task[];
task_name: string; onTaskUpdate: (updatedTasks: Task[]) => void;
description: string;
priority: string;
due_date: string | null;
status: string;
created_at: string;
upload_id: string;
} }
interface Upload { const TaskList = ({ tasks, onTaskUpdate }: TaskListProps) => {
id: string; const [filteredTasks, setFilteredTasks] = useState<Task[]>(tasks);
file_name: string; 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);
const TaskList = () => { const handleStatusChange = (id: string, status: 'Open' | 'In Progress' | 'Completed') => {
const { user } = useAuth(); const updatedTasks = tasks.map(task =>
const { toast } = useToast(); task.id === id ? { ...task, status } : task
const [tasks, setTasks] = useState<Task[]>([]); );
const [filteredTasks, setFilteredTasks] = useState<Task[]>([]); onTaskUpdate(updatedTasks);
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 () => { const handlePriorityChange = (id: string, priority: 'High' | 'Medium' | 'Low') => {
try { const updatedTasks = tasks.map(task =>
const { data, error } = await supabase task.id === id ? { ...task, priority } : task
.from('uploads') );
.select('id, file_name') onTaskUpdate(updatedTasks);
.eq('user_id', user?.id);
if (error) throw error;
setUploads(data || []);
} catch (error) {
console.error('Error fetching uploads:', error);
}
}; };
const applyFilters = () => { const uniqueDepartments = [...new Set(tasks.map(task => task.department))];
let result = [...tasks];
// Apply search term filter const sortTasks = (tasksToSort: Task[]) => {
if (searchTerm) { return [...tasksToSort].sort((a, b) => {
result = result.filter( switch (sortBy) {
task => case 'deadline':
task.task_name.toLowerCase().includes(searchTerm.toLowerCase()) || return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
task.description.toLowerCase().includes(searchTerm.toLowerCase()) 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())
); );
} }
// Apply priority filter // Priority filter
if (priorityFilter) { if (filterPriority.length > 0) {
result = result.filter(task => task.priority === priorityFilter); result = result.filter(task => filterPriority.includes(task.priority));
} }
// Apply status filter // Status filter
if (statusFilter) { if (filterStatus.length > 0) {
result = result.filter(task => task.status === statusFilter); result = result.filter(task => filterStatus.includes(task.status));
} }
// Apply document filter // Department filter
if (documentFilter) { if (filterDepartment.length > 0) {
result = result.filter(task => task.upload_id === documentFilter); result = result.filter(task => filterDepartment.includes(task.department));
} }
setFilteredTasks(result); // Sort the filtered results
return sortTasks(result);
}; };
const updateTaskStatus = async (taskId: string, newStatus: string) => { useEffect(() => {
try { setFilteredTasks(filterTasks());
const { error } = await supabase setIsFiltering(
.from('tasks') searchQuery !== '' ||
.update({ status: newStatus }) filterPriority.length > 0 ||
.eq('id', taskId); filterStatus.length > 0 ||
filterDepartment.length > 0
if (error) throw error; );
}, [tasks, searchQuery, sortBy, filterPriority, filterStatus, filterDepartment]);
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 = () => { const clearFilters = () => {
setSearchTerm(''); setSearchQuery('');
setPriorityFilter(''); setFilterPriority([]);
setStatusFilter(''); setFilterStatus([]);
setDocumentFilter(''); setFilterDepartment([]);
}; };
if (loading) { const completedTasksCount = tasks.filter(task => task.status === 'Completed').length;
return ( const taskCompletion = Math.round((completedTasksCount / tasks.length) * 100) || 0;
<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 ( return (
<div className="space-y-6"> <div className="w-full max-w-5xl mx-auto">
<Card> <div className="flex flex-col mb-8">
<CardHeader> <div className="flex flex-col lg:flex-row lg:items-center justify-between mb-4 gap-4">
<CardTitle>Tasks</CardTitle> <div>
<CardDescription> <h2 className="text-2xl font-semibold">Tasks</h2>
Actionable items extracted from your policy documents <p className="text-muted-foreground">
</CardDescription> {tasks.length} tasks extracted, {completedTasksCount} completed ({taskCompletion}%)
</CardHeader> </p>
<CardContent> </div>
<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"> <div className="flex flex-col sm:flex-row gap-3">
<Button <Button variant="outline" className="rounded-full" onClick={clearFilters}
variant="outline" disabled={!isFiltering}>
size="icon" <CheckCircle2 className="w-4 h-4 mr-2" />
onClick={() => setViewMode(viewMode === 'table' ? 'card' : 'table')} View All Tasks
> </Button>
{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={sortBy} onValueChange={setSortBy}>
<Select value={priorityFilter} onValueChange={setPriorityFilter}> <SelectTrigger className="w-[180px] rounded-full">
<SelectTrigger className="w-[140px]"> <List className="w-4 h-4 mr-2" />
<SelectValue placeholder="Priority" /> <SelectValue placeholder="Sort by" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="">All Priorities</SelectItem> <SelectItem value="deadline">Sort by Deadline</SelectItem>
<SelectItem value="High">High</SelectItem> <SelectItem value="priority">Sort by Priority</SelectItem>
<SelectItem value="Medium">Medium</SelectItem> <SelectItem value="department">Sort by Department</SelectItem>
<SelectItem value="Low">Low</SelectItem> </SelectContent>
</SelectContent> </Select>
</Select> </div>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}> <div className="flex flex-col sm:flex-row gap-3 mb-2">
<SelectTrigger className="w-[140px]"> <div className="relative flex-grow">
<SelectValue placeholder="Status" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
</SelectTrigger> <Input
<SelectContent> placeholder="Search tasks..."
<SelectItem value="">All Statuses</SelectItem> value={searchQuery}
<SelectItem value="Open">Open</SelectItem> onChange={(e) => setSearchQuery(e.target.value)}
<SelectItem value="In Progress">In Progress</SelectItem> className="pl-9 rounded-full"
<SelectItem value="Completed">Completed</SelectItem> />
</SelectContent> {searchQuery && (
</Select> <button
onClick={() => setSearchQuery('')}
<Select value={documentFilter} onValueChange={setDocumentFilter}> className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
<SelectTrigger className="w-[180px]"> >
<SelectValue placeholder="Document" /> <X className="w-4 h-4" />
</SelectTrigger> </button>
<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> </div>
</CardContent>
<CardFooter className="flex justify-between"> <div className="flex gap-2">
<div className="text-sm text-muted-foreground"> <DropdownMenu>
Showing {filteredTasks.length} of {tasks.length} tasks <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>
)}
</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>
<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>
)}
</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>
<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));
}
}}
>
{dept}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</CardFooter> </div>
</Card>
{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>
)}
<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>
)}
</div> </div>
); );
}; };

View File

@ -1,206 +1,168 @@
import { useState } from 'react'; import { useState, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { Upload, FileType, FileUp, AlertCircle, Check } from 'lucide-react';
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 { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { toast } from 'sonner';
interface UploadSectionProps { interface UploadSectionProps {
onUploadComplete: () => void; onUploadComplete: (fileName: string) => void;
} }
const UploadSection = ({ onUploadComplete }: UploadSectionProps) => { const UploadSection = ({ onUploadComplete }: UploadSectionProps) => {
const { user } = useAuth(); const [isDragging, setIsDragging] = useState(false);
const { toast } = useToast();
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0); const fileInputRef = useRef<HTMLInputElement>(null);
const allowedFileTypes = [ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
'application/pdf', e.preventDefault();
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' setIsDragging(true);
]; };
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleDragLeave = () => {
if (e.target.files && e.target.files[0]) { setIsDragging(false);
const selectedFile = e.target.files[0]; };
if (!allowedFileTypes.includes(selectedFile.type)) { const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
toast({ e.preventDefault();
title: 'Invalid file type', setIsDragging(false);
description: 'Please upload a PDF or DOCX file',
variant: 'destructive',
});
return;
}
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)) {
setFile(selectedFile); setFile(selectedFile);
} }
}; };
const clearFile = () => { const isValidFile = (file: File) => {
setFile(null); 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 handleUpload = async () => { const handleUpload = async () => {
if (!file || !user) return; if (!file) return;
setIsUploading(true);
// Simulate file upload with a delay
try { try {
setUploading(true); await new Promise(resolve => setTimeout(resolve, 2000));
setUploadProgress(0); toast.success(`${file.name} uploaded successfully`);
onUploadComplete(file.name);
// First, create an entry in the uploads table } catch (error) {
const fileName = file.name; toast.error("Upload failed. Please try again.");
const fileType = file.type; console.error("Upload error:", error);
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 { } finally {
setUploading(false); setIsUploading(false);
} }
}; };
return ( return (
<div className="space-y-4"> <div className="w-full max-w-3xl mx-auto">
{!file ? ( <div className="text-center mb-8">
<div className="border-2 border-dashed rounded-lg p-8 text-center"> <h2 className="text-2xl font-semibold mb-2">Upload Your Policy Document</h2>
<label className="flex flex-col items-center justify-center cursor-pointer space-y-3"> <p className="text-muted-foreground">Upload a PDF or Word document to extract actionable tasks</p>
<Upload className="h-10 w-10 text-muted-foreground" /> </div>
<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> <div
<input className={`
type="file" glass-panel p-8
className="hidden" border-2 border-dashed
accept=".pdf,.docx,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document" flex flex-col items-center justify-center
onChange={handleFileChange} transition-all duration-300
/> ${isDragging ? 'border-primary bg-primary/5' : 'border-border'}
<Button variant="outline" type="button"> ${file ? 'border-green-500/30 bg-green-50/30 dark:bg-green-900/10' : ''}
Select file `}
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
</Button> </Button>
</label> </>
</div> ) : (
) : ( <div className="w-full">
<div className="border rounded-lg p-6 space-y-4"> <div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-between"> <div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center">
<div className="flex items-center gap-3"> <FileType className="w-6 h-6 text-green-600 dark:text-green-400" />
<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> </div>
<Button <div className="text-center mb-6">
variant="ghost" <h3 className="text-lg font-medium mb-1 flex items-center justify-center">
size="icon" {file.name}
onClick={clearFile} <Check className="w-4 h-4 text-green-500 ml-2" />
disabled={uploading} </h3>
> <p className="text-sm text-muted-foreground">
<X className="h-4 w-4" /> {(file.size / 1024 / 1024).toFixed(2)} MB
</Button> </p>
</div> </div>
<div className="flex justify-center space-x-3">
{uploading && ( <Button
<div className="space-y-2"> variant="outline"
<Progress value={uploadProgress} className="h-2" /> size="sm"
<p className="text-xs text-muted-foreground">{uploadProgress.toFixed(0)}% complete</p> 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> </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> )}
)} </div>
<div className="text-sm text-muted-foreground"> <div className="mt-4 text-center text-sm text-muted-foreground flex items-center justify-center">
<p>After uploading, your document will be automatically processed to extract actionable tasks.</p> <AlertCircle className="w-4 h-4 mr-2" />
Your documents are processed securely and never stored permanently
</div> </div>
</div> </div>
); );

View File

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

View File

@ -1,11 +1,10 @@
import * as React from "react" import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Table = React.forwardRef< const Table = React.forwardRef<
HTMLTableElement, HTMLTableElement,
React.TableHTMLAttributes<HTMLTableElement> React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table
@ -43,7 +42,10 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn("bg-primary-50 font-medium text-primary-900", className)} className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props} {...props}
/> />
)) ))

View File

@ -1,8 +1,8 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { Session, User } from '@supabase/supabase-js'; import { Session, User } from '@supabase/supabase-js';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useNavigate, useLocation } from 'react-router-dom';
type AuthContextType = { type AuthContextType = {
session: Session | null; session: Session | null;
@ -12,7 +12,6 @@ type AuthContextType = {
signIn: (email: string, password: string) => Promise<void>; signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string, fullName: string) => Promise<void>; signUp: (email: string, password: string, fullName: string) => Promise<void>;
signOut: () => Promise<void>; signOut: () => Promise<void>;
isAuthenticated: boolean;
}; };
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
@ -22,17 +21,13 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<any | null>(null); const [profile, setProfile] = useState<any | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => { useEffect(() => {
// Get the initial session // Get the initial session
supabase.auth.getSession().then(({ data: { session } }) => { supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
setIsAuthenticated(!!session?.user);
if (session?.user) { if (session?.user) {
fetchProfile(session.user.id); fetchProfile(session.user.id);
} }
@ -44,15 +39,10 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
(_event, session) => { (_event, session) => {
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
setIsAuthenticated(!!session?.user);
if (session?.user) { if (session?.user) {
fetchProfile(session.user.id); fetchProfile(session.user.id);
} else { } else {
setProfile(null); setProfile(null);
// If on a protected route and logged out, redirect to auth
if (location.pathname === '/dashboard') {
navigate('/auth');
}
} }
setLoading(false); setLoading(false);
} }
@ -61,7 +51,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
return () => { return () => {
subscription.unsubscribe(); subscription.unsubscribe();
}; };
}, [navigate, location.pathname]); }, []);
const fetchProfile = async (userId: string) => { const fetchProfile = async (userId: string) => {
try { try {
@ -93,9 +83,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
title: "Success!", title: "Success!",
description: "You are now signed in.", description: "You are now signed in.",
}); });
// Redirect to dashboard on successful sign in
navigate('/dashboard');
} catch (error: any) { } catch (error: any) {
toast({ toast({
title: "Error signing in", title: "Error signing in",
@ -150,9 +137,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
title: "Signed out", title: "Signed out",
description: "You have been successfully signed out.", description: "You have been successfully signed out.",
}); });
// Always redirect to home page after sign out
navigate('/');
} catch (error: any) { } catch (error: any) {
toast({ toast({
title: "Error signing out", title: "Error signing out",
@ -175,7 +159,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
signIn, signIn,
signUp, signUp,
signOut, signOut,
isAuthenticated,
}} }}
> >
{children} {children}

View File

@ -1,4 +1,3 @@
// This file is automatically generated. Do not edit it directly. // This file is automatically generated. Do not edit it directly.
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import type { Database } from './types'; import type { Database } from './types';

View File

@ -36,86 +36,6 @@ export type Database = {
} }
Relationships: [] 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: { Views: {
[_ in never]: never [_ in never]: never

View File

@ -1,8 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { ShieldCheck, Mail, Lock, User, Loader2 } from 'lucide-react'; import { ShieldCheck, Mail, Lock, User } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@ -10,28 +10,24 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
const Auth = () => { const Auth = () => {
const { signIn, signUp, loading } = useAuth(); const { user, signIn, signUp, loading } = useAuth();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [fullName, setFullName] = useState(''); const [fullName, setFullName] = useState('');
const [authError, setAuthError] = useState<string | null>(null); const [authError, setAuthError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('login');
const location = useLocation(); // If user is already logged in, redirect to home page
const navigate = useNavigate(); if (user) {
return <Navigate to="/dashboard" />;
// Get the intended destination from the location state }
const from = (location.state as any)?.from?.pathname || '/dashboard';
const handleSignIn = async (e: React.FormEvent) => { const handleSignIn = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setAuthError(null); setAuthError(null);
try { try {
await signIn(email, password); await signIn(email, password);
// Redirect will be handled in the AuthContext after successful sign in
} catch (error: any) { } catch (error: any) {
console.error('Sign in error:', error); setAuthError(error.message);
// Error is already handled by the toast in AuthContext
} }
}; };
@ -40,38 +36,30 @@ const Auth = () => {
setAuthError(null); setAuthError(null);
try { try {
await signUp(email, password, fullName); await signUp(email, password, fullName);
// Switch to login tab after successful signup
setActiveTab('login');
} catch (error: any) { } catch (error: any) {
console.error('Sign up error:', error); setAuthError(error.message);
// Error is already handled by the toast in AuthContext
} }
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-muted/50 to-muted/30 px-4 py-8"> <div className="min-h-screen flex items-center justify-center bg-muted/30 px-4">
<div className="w-full max-w-md"> <div className="max-w-md w-full">
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center mb-4 w-16 h-16 rounded-full bg-primary/10"> <div className="inline-flex mb-6 p-4 bg-primary/10 rounded-full">
<ShieldCheck className="w-8 h-8 text-primary" /> <ShieldCheck className="w-8 h-8 text-primary" />
</div> </div>
<h1 className="text-3xl font-bold mb-2">SecuPolicy</h1> <h1 className="text-3xl font-bold mb-2">SecuPolicy</h1>
<p className="text-muted-foreground">Secure access to your account</p> <p className="text-muted-foreground">Sign in to access your account</p>
</div> </div>
<Tabs <Tabs defaultValue="login" className="w-full">
defaultValue="login"
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
>
<TabsList className="grid w-full grid-cols-2 mb-6"> <TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="login">Sign In</TabsTrigger> <TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Create Account</TabsTrigger> <TabsTrigger value="register">Register</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="login"> <TabsContent value="login">
<Card className="border-2"> <Card>
<CardHeader> <CardHeader>
<CardTitle>Welcome back</CardTitle> <CardTitle>Welcome back</CardTitle>
<CardDescription> <CardDescription>
@ -116,17 +104,10 @@ const Auth = () => {
<CardFooter> <CardFooter>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full rounded-full"
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? 'Signing in...' : 'Sign In'}
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button> </Button>
</CardFooter> </CardFooter>
</form> </form>
@ -134,7 +115,7 @@ const Auth = () => {
</TabsContent> </TabsContent>
<TabsContent value="register"> <TabsContent value="register">
<Card className="border-2"> <Card>
<CardHeader> <CardHeader>
<CardTitle>Create an account</CardTitle> <CardTitle>Create an account</CardTitle>
<CardDescription> <CardDescription>
@ -195,17 +176,10 @@ const Auth = () => {
<CardFooter> <CardFooter>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full rounded-full"
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? 'Creating account...' : 'Create Account'}
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating account...
</>
) : (
'Create Account'
)}
</Button> </Button>
</CardFooter> </CardFooter>
</form> </form>

View File

@ -1,196 +1,167 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import Header from '@/components/Header';
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 UploadSection from '@/components/UploadSection';
import TaskList from '@/components/TaskList'; 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 Dashboard = () => {
const { user } = useAuth(); const [uploadCompleted, setUploadCompleted] = useState(false);
const { toast } = useToast(); const [documentName, setDocumentName] = useState('');
const [activeTab, setActiveTab] = useState<'upload' | 'tasks'>('upload'); const [tasks, setTasks] = useState<Task[]>([]);
const [uploadCount, setUploadCount] = useState(0);
const [taskCount, setTaskCount] = useState(0);
const [recentUploads, setRecentUploads] = useState<any[]>([]);
const [processingUploads, setProcessingUploads] = useState<number>(0);
useEffect(() => { const handleUploadComplete = (fileName: string) => {
if (user) { // Simulate task extraction with a delay
fetchDashboardData(); setTimeout(() => {
} const extractedTasks = generateTasks(fileName);
}, [user]); setTasks(extractedTasks);
setDocumentName(fileName);
setUploadCompleted(true);
const fetchDashboardData = async () => { toast.success(`${extractedTasks.length} tasks extracted successfully`, {
try { description: 'Tasks are now ready for review and prioritization'
// 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 ( return (
<div className="container mx-auto px-4 py-8"> <div className="min-h-screen flex flex-col">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1> <Header />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> <main className="flex-grow pt-20">
<Card> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<CardHeader className="pb-2"> <Tabs defaultValue="upload" className="w-full">
<CardTitle className="text-lg">Documents</CardTitle> <div className="flex justify-center mb-8">
<CardDescription>Total uploaded documents</CardDescription> <TabsList className="rounded-full h-12">
</CardHeader> <TabsTrigger value="upload" className="rounded-full px-6">
<CardContent> Upload Document
<p className="text-3xl font-bold">{uploadCount}</p> </TabsTrigger>
</CardContent> <TabsTrigger
</Card> value="tasks"
className="rounded-full px-6"
<Card> disabled={!uploadCompleted}
<CardHeader className="pb-2"> onClick={() => {
<CardTitle className="text-lg">Tasks</CardTitle> if (!uploadCompleted) {
<CardDescription>Extracted actionable items</CardDescription> toast.error("Please upload a document first");
</CardHeader> }
<CardContent> }}
<p className="text-3xl font-bold">{taskCount}</p> >
</CardContent> Review Tasks
</Card> </TabsTrigger>
<TabsTrigger
<Card> value="export"
<CardHeader className="pb-2 flex flex-row items-center justify-between space-y-0"> className="rounded-full px-6"
<div> disabled={!uploadCompleted}
<CardTitle className="text-lg">Processing</CardTitle> onClick={() => {
<CardDescription>Documents being analyzed</CardDescription> if (!uploadCompleted) {
toast.error("Please upload a document first");
}
}}
>
Export
</TabsTrigger>
</TabsList>
</div> </div>
{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"> <TabsContent value="upload" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
<Button <UploadSection onUploadComplete={handleUploadComplete} />
variant={activeTab === 'upload' ? 'default' : 'outline'}
onClick={() => setActiveTab('upload')}
className="flex items-center gap-2"
>
<Upload className="h-4 w-4" />
Upload Document
</Button>
<Button {uploadCompleted && (
variant={activeTab === 'tasks' ? 'default' : 'outline'} <div className="flex justify-center mt-6">
onClick={() => setActiveTab('tasks')} <div className="glass-panel px-4 py-2 text-sm flex items-center">
className="flex items-center gap-2" <AlertCircle className="w-4 h-4 mr-2 text-green-500" />
> Document processed successfully! Continue to Review Tasks.
<FileText className="h-4 w-4" /> </div>
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>
))}
</div> </div>
</CardContent> )}
</Card> </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
</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>
</div> </div>
) : ( </main>
<TaskList />
)} <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>
</div> </div>
); );
}; };

View File

@ -1,241 +0,0 @@
import { useState } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useToast } from '@/hooks/use-toast';
import { supabase } from '@/integrations/supabase/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { User, Mail, Building, Briefcase, Lock, Save } from 'lucide-react';
const Profile = () => {
const { user, profile, signOut } = useAuth();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
fullName: profile?.full_name || '',
email: user?.email || '',
company: profile?.company || '',
role: profile?.role || '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) {
toast({
title: "Error",
description: "You must be logged in to update your profile",
variant: "destructive",
});
return;
}
try {
setIsLoading(true);
const { error } = await supabase
.from('profiles')
.update({
full_name: formData.fullName,
company: formData.company,
role: formData.role,
})
.eq('id', user.id);
if (error) throw error;
toast({
title: "Profile updated",
description: "Your profile has been successfully updated.",
});
} catch (error: any) {
console.error('Error updating profile:', error);
toast({
title: "Error updating profile",
description: error.message || "An error occurred while updating your profile",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
const getInitials = () => {
if (formData.fullName) {
return formData.fullName
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase();
}
return user?.email?.substring(0, 2).toUpperCase() || 'U';
};
if (!user) {
return (
<div className="container mx-auto px-4 py-8 text-center">
<h1 className="text-2xl font-bold mb-4">Profile not available</h1>
<p className="mb-4">You need to be logged in to view your profile.</p>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">My Profile</h1>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1">
<Card>
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<Avatar className="h-24 w-24">
<AvatarFallback className="text-2xl">{getInitials()}</AvatarFallback>
</Avatar>
</div>
<CardTitle className="text-xl">{profile?.full_name || user.email}</CardTitle>
<CardDescription>{profile?.role || 'SecuPolicy User'}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Mail className="h-5 w-5 text-muted-foreground" />
<span className="text-sm">{user.email}</span>
</div>
{profile?.company && (
<div className="flex items-center gap-3">
<Building className="h-5 w-5 text-muted-foreground" />
<span className="text-sm">{profile.company}</span>
</div>
)}
{profile?.role && (
<div className="flex items-center gap-3">
<Briefcase className="h-5 w-5 text-muted-foreground" />
<span className="text-sm">{profile.role}</span>
</div>
)}
</CardContent>
<CardFooter className="flex flex-col gap-2">
<Button
className="w-full"
variant="outline"
onClick={() => signOut()}
>
<Lock className="mr-2 h-4 w-4" />
Sign Out
</Button>
</CardFooter>
</Card>
</div>
<div className="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle>Edit Profile</CardTitle>
<CardDescription>
Update your personal information and company details
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="fullName">Full Name</Label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<User className="h-4 w-4 text-muted-foreground" />
</div>
<Input
id="fullName"
name="fullName"
placeholder="Your full name"
value={formData.fullName}
onChange={handleChange}
className="pl-10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Mail className="h-4 w-4 text-muted-foreground" />
</div>
<Input
id="email"
name="email"
type="email"
value={formData.email}
disabled
className="pl-10 bg-muted"
/>
</div>
<p className="text-xs text-muted-foreground">
Email address cannot be changed
</p>
</div>
<div className="space-y-2">
<Label htmlFor="company">Company</Label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Building className="h-4 w-4 text-muted-foreground" />
</div>
<Input
id="company"
name="company"
placeholder="Company name"
value={formData.company}
onChange={handleChange}
className="pl-10"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="role">Role</Label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Briefcase className="h-4 w-4 text-muted-foreground" />
</div>
<Input
id="role"
name="role"
placeholder="Your role at the company"
value={formData.role}
onChange={handleChange}
className="pl-10"
/>
</div>
</div>
</CardContent>
<CardFooter>
<Button
type="submit"
disabled={isLoading}
className="ml-auto"
>
<Save className="mr-2 h-4 w-4" />
{isLoading ? 'Saving...' : 'Save Changes'}
</Button>
</CardFooter>
</form>
</Card>
</div>
</div>
</div>
);
};
export default Profile;

View File

@ -1,11 +0,0 @@
# This file controls the settings for your Edge Function when deployed to Supabase.
[build]
command = ""
[deploy]
release_command = ""
[build.environment_variables]
OPENAI_API_KEY = "{{ OPENAI_API_KEY }}"

View File

@ -1,155 +0,0 @@
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');
}
}