Compare commits

..

10 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
2f66cd3f11 Implement profile page and navigation changes
- Implemented a profile page with necessary elements for the micro-saas purpose.
- Modified the dashboard to hide the navigation bar after login.
2025-03-10 09:37:11 +00:00
gpt-engineer-app[bot]
2f66c2ddcd Ensure authenticated access to dashboard
Implement authentication check for dashboard route, redirecting unauthenticated users to the authentication page.
2025-03-10 09:26:18 +00:00
gpt-engineer-app[bot]
60ee8bb62e Fix: Resolve TypeScript errors in table component
The component was throwing TypeScript errors because the React namespace did not have the HTMLTableProps, HTMLTableSectionProps and HTMLTableRowProps exported. These have been replaced with React.HTMLAttributes<HTMLTableElement> and React.HTMLAttributes<HTMLTableSectionElement> and React.HTMLAttributes<HTMLTableRowElement> respectively.
2025-03-10 09:17:05 +00:00
gpt-engineer-app[bot]
c37ebae745 Update API keys
The API keys have been updated.
2025-03-10 09:15:33 +00:00
gpt-engineer-app[bot]
85b88d9ee0 Run SQL migrations 2025-03-10 08:55:23 +00:00
gpt-engineer-app[bot]
746a8dbea0 Rebuild authentication page
Rebuild the authentication page.
2025-03-10 08:37:22 +00:00
gpt-engineer-app[bot]
4e4d446422 Remove social login options
Removes the social login options (Github, Google, Microsoft) and keeps only email/password authentication.
2025-03-09 12:43:02 +00:00
gpt-engineer-app[bot]
d3e4bd2df2 Implement authentication functionality
Adds authentication functionality to the application.
2025-03-09 12:41:42 +00:00
gpt-engineer-app[bot]
e1fb6c9bd0 Reverted to edit edt-531cfb36-92c8-430d-82fa-c370bdad5948: "Run SQL migrations
The SQL migrations have been reviewed and approved, and are now being run."
2025-03-09 12:38:34 +00:00
gpt-engineer-app[bot]
480b779a9a Implement sign-up and login
Implements sign-up and login functionality with options for email/password and potentially other providers like Google, GitHub, or Microsoft.
2025-03-09 12:30:51 +00:00
18 changed files with 2695 additions and 920 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", "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,59 +3,104 @@ 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 } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate, useLocation } 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();
// Protected route component // Enhanced Protected route component that preserves the intended destination
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { user, loading } = useAuth(); const { user, loading, isAuthenticated } = 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 (!user) { if (!isAuthenticated) {
return <Navigate to="/auth" />; // Redirect to the auth page but save the current location they were trying to access
return <Navigate to="/auth" state={{ from: location }} replace />;
} }
return <>{children}</>; return <>{children}</>;
}; };
const App = () => ( // Authentication guard to prevent authenticated users from accessing the auth page
<QueryClientProvider client={queryClient}> const AuthGuard = ({ children }: { children: React.ReactNode }) => {
<AuthProvider> const { isAuthenticated, loading } = useAuth();
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 />
<BrowserRouter> {!isDashboard && <Header />}
<div className={isDashboard ? "" : "pt-16 sm:pt-20"}>
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Index />} />
<Route path="/auth" element={<Auth />} /> <Route
{/* Protected routes example (Dashboard will be implemented later) */} path="/auth"
element={
<AuthGuard>
<Auth />
</AuthGuard>
}
/>
<Route <Route
path="/dashboard" path="/dashboard"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<div className="min-h-screen pt-20 px-4"> <Dashboard />
<div className="container mx-auto"> </ProtectedRoute>
<h1 className="text-3xl font-bold mb-6">Dashboard</h1> }
<p>This is a protected route. Dashboard implementation coming soon.</p> />
</div> <Route
</div> path="/profile"
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>
</BrowserRouter> </div>
</TooltipProvider> </TooltipProvider>
</>
);
};
const App = () => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<AppContent />
</AuthProvider> </AuthProvider>
</BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@ -1,76 +1,130 @@
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 { FileDown, Check } from 'lucide-react'; import { Download } from 'lucide-react';
import { toast } from 'sonner'; 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 { interface ExportOptionsProps {
tasks: Task[]; tasks: Task[];
} }
const ExportOptions = ({ tasks }: ExportOptionsProps) => { export const ExportOptions = ({ tasks }: ExportOptionsProps) => {
const [isExporting, setIsExporting] = useState(false); const [exporting, setExporting] = useState(false);
const exportToCSV = () => { const exportToCsv = () => {
try { 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 = [ const csvContent = [
headers.join(','), headers.join(','),
...tasks.map(task => [ ...tasks.map(task => {
`"${task.description.replace(/"/g, '""')}"`, return [
`"${task.department.replace(/"/g, '""')}"`, `"${task.task_name.replace(/"/g, '""')}"`,
task.deadline, `"${task.description?.replace(/"/g, '""') || ''}"`,
task.priority, `"${task.priority}"`,
task.status, `"${task.due_date ? new Date(task.due_date).toLocaleDateString() : ''}"`,
`"${task.context.replace(/"/g, '""')}"` `"${task.status}"`
].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', `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); 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('Export error:', error); console.error('Error exporting to CSV:', error);
toast.error('Failed to export CSV file');
} finally { } 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 ( return (
<div className="w-full max-w-xs mx-auto"> <DropdownMenu>
<Button <DropdownMenuTrigger asChild>
onClick={exportToCSV} <Button variant="outline" className="flex items-center">
disabled={isExporting || tasks.length === 0} <Download className="mr-2 h-4 w-4" />
className="w-full rounded-full" Export
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> </Button>
</div> </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

@ -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 } from 'lucide-react'; import { ShieldCheck, Menu, X, User, LogOut, UserCircle } from 'lucide-react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { import {
DropdownMenu, DropdownMenu,
@ -84,7 +84,10 @@ const Header = () => {
<Link to="/dashboard">Dashboard</Link> <Link to="/dashboard">Dashboard</Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to="/profile">Profile</Link> <Link to="/profile">
<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">
@ -129,7 +132,8 @@ const Header = () => {
</a> </a>
{user ? ( {user ? (
<> <>
<Link to="/profile" className="block py-2 text-base font-medium" onClick={() => setMobileMenuOpen(false)}> <Link to="/profile" className="flex items-center 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,157 +1,156 @@
import { useState } from 'react'; 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 { Badge } from '@/components/ui/badge';
import { Calendar, AlertCircle, Users, CornerDownRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { import {
Select, MoreVertical,
SelectContent, Calendar,
SelectItem, FileText,
SelectTrigger, Check,
SelectValue, X,
} from "@/components/ui/select"; 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 { interface TaskCardProps {
task: Task; task: Task;
onStatusChange: (id: string, status: 'Open' | 'In Progress' | 'Completed') => void; documentName: string;
onPriorityChange: (id: string, priority: 'High' | 'Medium' | 'Low') => void; onStatusChange: (status: string) => void;
} }
const TaskCard = ({ task, onStatusChange, onPriorityChange }: TaskCardProps) => { export const TaskCard = ({ task, documentName, onStatusChange }: TaskCardProps) => {
const [isExpanded, setIsExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const getPriorityColor = (priority: string) => { const getPriorityIcon = () => {
switch (priority) { switch (task.priority.toLowerCase()) {
case 'High': 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'; return <AlertCircle className="h-4 w-4 text-red-500" />;
case 'Medium': 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'; return <Clock className="h-4 w-4 text-amber-500" />;
case 'Low': 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'; return <Check className="h-4 w-4 text-green-500" />;
default: 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) => { const getPriorityBadge = () => {
switch (status) { switch (task.priority.toLowerCase()) {
case 'Completed': case 'high':
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 border-green-200 dark:border-green-800/30'; return <Badge variant="destructive" className="ml-2">{task.priority}</Badge>;
case 'In Progress': case 'medium':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200 dark:border-blue-800/30'; return <Badge variant="default" className="ml-2">{task.priority}</Badge>;
case 'Open': case 'low':
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="secondary" className="ml-2">{task.priority}</Badge>;
default: 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 getStatusBadge = () => {
const date = new Date(dateString); switch (task.status.toLowerCase()) {
return new Intl.DateTimeFormat('en-US', { case 'completed':
year: 'numeric', return <Badge className="bg-green-500 ml-2">{task.status}</Badge>;
month: 'short', case 'in progress':
day: 'numeric' return <Badge className="bg-blue-500 ml-2">{task.status}</Badge>;
}).format(date); 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 ( return (
<div className={cn( <Card className={`overflow-hidden transition-shadow hover:shadow-md ${
"glass-panel p-5 transition-all duration-300 cursor-pointer hover-lift", task.status === 'Completed' ? 'bg-muted/30' : ''
task.status === 'Completed' && "opacity-70", }`}>
isExpanded && "ring-1 ring-primary/20" <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>
)} )}
onClick={() => setIsExpanded(!isExpanded)} {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)}
> >
<div className="flex flex-col"> {expanded ? 'Show less' : 'Show more'}
<div className="flex justify-between items-start mb-3"> </Button>
<h3 className="font-medium text-base">{task.description}</h3> )}
<Badge variant="outline" className={`ml-2 ${getPriorityColor(task.priority)}`}> </CardContent>
{task.priority} <CardFooter className="p-4 pt-0 flex justify-between">
</Badge> <div className="flex items-center text-xs">
</div> {task.due_date && (
<div className="flex items-center mr-2">
<div className="flex flex-wrap gap-3 text-sm text-muted-foreground mb-3"> <Calendar className="h-3 w-3 mr-1" />
<div className="flex items-center"> <span>
<Users className="w-4 h-4 mr-1" /> {new Date(task.due_date).toLocaleDateString()}
{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> </span>
</div> </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>
</div>
</div>
)} )}
</div> </div>
</div> {getStatusBadge()}
</CardFooter>
</Card>
); );
}; };
export default TaskCard;

View File

@ -1,409 +1,419 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Task } from '@/utils/mockData'; import { useAuth } from '@/contexts/AuthContext';
import TaskCard from './TaskCard'; import { supabase } from '@/integrations/supabase/client';
import { Input } from '@/components/ui/input'; import { useToast } from '@/hooks/use-toast';
import { Download, Filter, Search, Check, X } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge'; 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 { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from '@/components/ui/select';
import { import {
DropdownMenu, Card,
DropdownMenuContent, CardContent,
DropdownMenuCheckboxItem, CardDescription,
DropdownMenuTrigger CardFooter,
} from "@/components/ui/dropdown-menu"; CardHeader,
import { cn } from '@/lib/utils'; CardTitle,
} from '@/components/ui/card';
import { TaskCard } from '@/components/TaskCard';
import { ExportOptions } from '@/components/ExportOptions';
interface TaskListProps { interface Task {
tasks: Task[]; id: string;
onTaskUpdate: (updatedTasks: Task[]) => void; task_name: string;
description: string;
priority: string;
due_date: string | null;
status: string;
created_at: string;
upload_id: string;
} }
const TaskList = ({ tasks, onTaskUpdate }: TaskListProps) => { interface Upload {
const [filteredTasks, setFilteredTasks] = useState<Task[]>(tasks); id: string;
const [searchQuery, setSearchQuery] = useState(''); file_name: string;
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 handleStatusChange = (id: string, status: 'Open' | 'In Progress' | 'Completed') => { const TaskList = () => {
const updatedTasks = tasks.map(task => const { user } = useAuth();
task.id === id ? { ...task, status } : task const { toast } = useToast();
); const [tasks, setTasks] = useState<Task[]>([]);
onTaskUpdate(updatedTasks); const [filteredTasks, setFilteredTasks] = useState<Task[]>([]);
}; const [uploads, setUploads] = useState<Upload[]>([]);
const [loading, setLoading] = useState(true);
const handlePriorityChange = (id: string, priority: 'High' | 'Medium' | 'Low') => { const [searchTerm, setSearchTerm] = useState('');
const updatedTasks = tasks.map(task => const [priorityFilter, setPriorityFilter] = useState<string>('');
task.id === id ? { ...task, priority } : task const [statusFilter, setStatusFilter] = useState<string>('');
); const [documentFilter, setDocumentFilter] = useState<string>('');
onTaskUpdate(updatedTasks); const [viewMode, setViewMode] = useState<'table' | 'card'>('table');
};
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);
};
useEffect(() => { useEffect(() => {
setFilteredTasks(filterTasks()); if (user) {
setIsFiltering( fetchTasks();
searchQuery !== '' || fetchUploads();
filterPriority.length > 0 || }
filterStatus.length > 0 || }, [user]);
filterDepartment.length > 0
);
}, [tasks, searchQuery, sortBy, filterPriority, filterStatus, filterDepartment]);
const clearFilters = () => { useEffect(() => {
setSearchQuery(''); applyFilters();
setFilterPriority([]); }, [tasks, searchTerm, priorityFilter, statusFilter, documentFilter]);
setFilterStatus([]);
setFilterDepartment([]); 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 fetchUploads = async () => {
const taskCompletion = Math.round((completedTasksCount / tasks.length) * 100) || 0; 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 ( return (
<div className="w-full max-w-5xl mx-auto"> <div className="space-y-6">
<div className="flex flex-col mb-8"> <Card>
<div className="flex flex-col lg:flex-row lg:items-center justify-between mb-4 gap-4"> <CardHeader>
<div> <CardTitle>Tasks</CardTitle>
<h2 className="text-2xl font-semibold">Tasks</h2> <CardDescription>
<p className="text-muted-foreground"> Actionable items extracted from your policy documents
{tasks.length} tasks extracted, {completedTasksCount} completed ({taskCompletion}%) </CardDescription>
</p> </CardHeader>
</div> <CardContent>
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-4">
<Button variant="outline" className="rounded-full" onClick={clearFilters} <div className="relative flex-1">
disabled={!isFiltering}> <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<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 <Input
type="search"
placeholder="Search tasks..." placeholder="Search tasks..."
value={searchQuery} className="pl-8"
onChange={(e) => setSearchQuery(e.target.value)} value={searchTerm}
className="pl-9 rounded-full" onChange={(e) => setSearchTerm(e.target.value)}
/> />
{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>
<div className="flex gap-2"> <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>
)}
</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>
{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 <Button
variant="ghost" variant="outline"
size="sm" size="icon"
className="text-xs h-7 px-2 rounded-full" onClick={() => setViewMode(viewMode === 'table' ? 'card' : 'table')}
onClick={clearFilters}
> >
Clear all {viewMode === 'table' ?
</Button> <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> </div>
}
</Button>
<ExportOptions tasks={filteredTasks} />
</div>
</div>
<div className="flex flex-wrap gap-2">
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Priorities</SelectItem>
<SelectItem value="High">High</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="Low">Low</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Statuses</SelectItem>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
<Select value={documentFilter} onValueChange={setDocumentFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Document" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Documents</SelectItem>
{uploads.map((upload) => (
<SelectItem key={upload.id} value={upload.id}>
{upload.file_name.length > 25
? `${upload.file_name.substring(0, 25)}...`
: upload.file_name}
</SelectItem>
))}
</SelectContent>
</Select>
{(searchTerm || priorityFilter || statusFilter || documentFilter) && (
<Button variant="ghost" onClick={clearFilters} className="h-10">
<X className="mr-2 h-3 w-3" />
Clear filters
</Button>
)} )}
</div> </div>
{filteredTasks.length === 0 ? ( {filteredTasks.length === 0 ? (
<div className="p-12 text-center glass-panel"> <div className="text-center py-8">
<p className="text-lg font-medium">No tasks match your filters</p> <div className="mb-4">
<p className="text-muted-foreground mt-2">Try adjusting your search or filters</p> <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 <Button
variant="outline" variant="outline"
className="mt-4 rounded-full" className="mt-4"
onClick={clearFilters} onClick={() => window.location.href = '/dashboard'}
> >
Clear all filters Upload Document
</Button> </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>
) : ( ) : (
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTasks.map((task) => ( {filteredTasks.map((task) => (
<TaskCard <TaskCard
key={task.id} key={task.id}
task={task} task={task}
onStatusChange={handleStatusChange} documentName={getDocumentName(task.upload_id)}
onPriorityChange={handlePriorityChange} onStatusChange={(newStatus) => updateTaskStatus(task.id, newStatus)}
/> />
))} ))}
</div> </div>
)} )}
</div> </div>
</CardContent>
<CardFooter className="flex justify-between">
<div className="text-sm text-muted-foreground">
Showing {filteredTasks.length} of {tasks.length} tasks
</div>
</CardFooter>
</Card>
</div>
); );
}; };

View File

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

View File

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

View File

@ -1,10 +1,11 @@
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.HTMLAttributes<HTMLTableElement> React.TableHTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table
@ -42,10 +43,7 @@ const TableFooter = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn("bg-primary-50 font-medium text-primary-900", className)}
"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,6 +12,7 @@ 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);
@ -21,13 +22,17 @@ 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);
} }
@ -39,10 +44,15 @@ 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);
} }
@ -51,7 +61,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 {
@ -83,6 +93,9 @@ 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",
@ -137,6 +150,9 @@ 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",
@ -159,6 +175,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
signIn, signIn,
signUp, signUp,
signOut, signOut,
isAuthenticated,
}} }}
> >
{children} {children}

View File

@ -1,3 +1,4 @@
// 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,6 +36,86 @@ 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 { Navigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { ShieldCheck, Mail, Lock, User } from 'lucide-react'; import { ShieldCheck, Mail, Lock, User, Loader2 } 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,24 +10,28 @@ 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 { user, signIn, signUp, loading } = useAuth(); const { 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');
// If user is already logged in, redirect to home page const location = useLocation();
if (user) { const navigate = useNavigate();
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) {
setAuthError(error.message); console.error('Sign in error:', error);
// Error is already handled by the toast in AuthContext
} }
}; };
@ -36,30 +40,38 @@ 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) {
setAuthError(error.message); console.error('Sign up error:', error);
// Error is already handled by the toast in AuthContext
} }
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-muted/30 px-4"> <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="max-w-md w-full"> <div className="w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex mb-6 p-4 bg-primary/10 rounded-full"> <div className="inline-flex items-center justify-center mb-4 w-16 h-16 rounded-full bg-primary/10">
<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">Sign in to access your account</p> <p className="text-muted-foreground">Secure access to your account</p>
</div> </div>
<Tabs defaultValue="login" className="w-full"> <Tabs
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">Login</TabsTrigger> <TabsTrigger value="login">Sign In</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger> <TabsTrigger value="register">Create Account</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="login"> <TabsContent value="login">
<Card> <Card className="border-2">
<CardHeader> <CardHeader>
<CardTitle>Welcome back</CardTitle> <CardTitle>Welcome back</CardTitle>
<CardDescription> <CardDescription>
@ -104,10 +116,17 @@ const Auth = () => {
<CardFooter> <CardFooter>
<Button <Button
type="submit" type="submit"
className="w-full rounded-full" className="w-full"
disabled={loading} disabled={loading}
> >
{loading ? 'Signing in...' : 'Sign In'} {loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button> </Button>
</CardFooter> </CardFooter>
</form> </form>
@ -115,7 +134,7 @@ const Auth = () => {
</TabsContent> </TabsContent>
<TabsContent value="register"> <TabsContent value="register">
<Card> <Card className="border-2">
<CardHeader> <CardHeader>
<CardTitle>Create an account</CardTitle> <CardTitle>Create an account</CardTitle>
<CardDescription> <CardDescription>
@ -176,10 +195,17 @@ const Auth = () => {
<CardFooter> <CardFooter>
<Button <Button
type="submit" type="submit"
className="w-full rounded-full" className="w-full"
disabled={loading} disabled={loading}
> >
{loading ? 'Creating account...' : 'Create Account'} {loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating account...
</>
) : (
'Create Account'
)}
</Button> </Button>
</CardFooter> </CardFooter>
</form> </form>

View File

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

241
src/pages/Profile.tsx Normal file
View File

@ -0,0 +1,241 @@
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

@ -0,0 +1,11 @@
# 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

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