Compare commits
No commits in common. "2f66cd3f11aed956c24a90d50a6a432980c5a025" and "58c08af3e893fba91a1b2ad70b46b753c5625df5" have entirely different histories.
2f66cd3f11
...
58c08af3e8
1070
package-lock.json
generated
1070
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
83
src/App.tsx
83
src/App.tsx
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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="flex flex-col sm:flex-row gap-3">
|
||||||
<div className="relative flex-1">
|
<Button variant="outline" className="rounded-full" onClick={clearFilters}
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
disabled={!isFiltering}>
|
||||||
<Input
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||||
type="search"
|
View All Tasks
|
||||||
placeholder="Search tasks..."
|
</Button>
|
||||||
className="pl-8"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setViewMode(viewMode === 'table' ? 'card' : 'table')}
|
|
||||||
>
|
|
||||||
{viewMode === 'table' ?
|
|
||||||
<div className="h-4 w-4 grid grid-cols-2 gap-0.5">
|
|
||||||
<div className="bg-foreground rounded-sm"></div>
|
|
||||||
<div className="bg-foreground rounded-sm"></div>
|
|
||||||
<div className="bg-foreground rounded-sm"></div>
|
|
||||||
<div className="bg-foreground rounded-sm"></div>
|
|
||||||
</div> :
|
|
||||||
<div className="h-4 w-4 flex flex-col justify-between">
|
|
||||||
<div className="h-[2px] w-full bg-foreground"></div>
|
|
||||||
<div className="h-[2px] w-full bg-foreground"></div>
|
|
||||||
<div className="h-[2px] w-full bg-foreground"></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
<ExportOptions tasks={filteredTasks} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<Select value={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}>
|
|
||||||
<SelectTrigger className="w-[140px]">
|
<div className="flex flex-col sm:flex-row gap-3 mb-2">
|
||||||
<SelectValue placeholder="Status" />
|
<div className="relative flex-grow">
|
||||||
</SelectTrigger>
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||||
<SelectContent>
|
<Input
|
||||||
<SelectItem value="">All Statuses</SelectItem>
|
placeholder="Search tasks..."
|
||||||
<SelectItem value="Open">Open</SelectItem>
|
value={searchQuery}
|
||||||
<SelectItem value="In Progress">In Progress</SelectItem>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<SelectItem value="Completed">Completed</SelectItem>
|
className="pl-9 rounded-full"
|
||||||
</SelectContent>
|
/>
|
||||||
</Select>
|
{searchQuery && (
|
||||||
|
<button
|
||||||
<Select value={documentFilter} onValueChange={setDocumentFilter}>
|
onClick={() => setSearchQuery('')}
|
||||||
<SelectTrigger className="w-[180px]">
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
<SelectValue placeholder="Document" />
|
>
|
||||||
</SelectTrigger>
|
<X className="w-4 h-4" />
|
||||||
<SelectContent>
|
</button>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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',
|
const droppedFile = e.dataTransfer.files[0];
|
||||||
});
|
if (isValidFile(droppedFile)) {
|
||||||
return;
|
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>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
accept=".pdf,.docx,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
|
||||||
<Button variant="outline" type="button">
|
|
||||||
Select file
|
|
||||||
</Button>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="border rounded-lg p-6 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="bg-primary/10 p-2 rounded">
|
|
||||||
<FileIcon className="h-6 w-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{file.name}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={clearFile}
|
|
||||||
disabled={uploading}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</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
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={uploading}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{uploading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Uploading...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Upload className="h-4 w-4" />
|
|
||||||
Upload Document
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground">
|
<div
|
||||||
<p>After uploading, your document will be automatically processed to extract actionable tasks.</p>
|
className={`
|
||||||
|
glass-panel p-8
|
||||||
|
border-2 border-dashed
|
||||||
|
flex flex-col items-center justify-center
|
||||||
|
transition-all duration-300
|
||||||
|
${isDragging ? 'border-primary bg-primary/5' : 'border-border'}
|
||||||
|
${file ? 'border-green-500/30 bg-green-50/30 dark:bg-green-900/10' : ''}
|
||||||
|
`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
accept=".pdf,.doc,.docx"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!file ? (
|
||||||
|
<>
|
||||||
|
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<Upload className="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Drag and drop your file here</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-6 max-w-md text-center">
|
||||||
|
Support for PDF and Word documents up to 10MB
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full"
|
||||||
|
>
|
||||||
|
<FileUp className="w-4 h-4 mr-2" />
|
||||||
|
Browse Files
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center justify-center mb-6">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center">
|
||||||
|
<FileType className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<h3 className="text-lg font-medium mb-1 flex items-center justify-center">
|
||||||
|
{file.name}
|
||||||
|
<Check className="w-4 h-4 text-green-500 ml-2" />
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center space-x-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full"
|
||||||
|
onClick={() => {
|
||||||
|
setFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={isUploading}
|
||||||
|
size="sm"
|
||||||
|
className="rounded-full min-w-24"
|
||||||
|
>
|
||||||
|
{isUploading ? 'Processing...' : 'Analyze Document'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center text-sm text-muted-foreground flex items-center justify-center">
|
||||||
|
<AlertCircle className="w-4 h-4 mr-2" />
|
||||||
|
Your documents are processed securely and never stored permanently
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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 }
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -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}
|
||||||
|
@ -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';
|
||||||
@ -9,4 +8,4 @@ const SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiO
|
|||||||
// Import the supabase client like this:
|
// Import the supabase client like this:
|
||||||
// import { supabase } from "@/integrations/supabase/client";
|
// import { supabase } from "@/integrations/supabase/client";
|
||||||
|
|
||||||
export const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
|
export const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
|
@ -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
|
||||||
|
@ -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');
|
|
||||||
|
// 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) {
|
||||||
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>
|
||||||
|
@ -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);
|
||||||
const fetchDashboardData = async () => {
|
setUploadCompleted(true);
|
||||||
try {
|
|
||||||
// Get upload count
|
toast.success(`${extractedTasks.length} tasks extracted successfully`, {
|
||||||
const { count: uploadCountData, error: uploadCountError } = await supabase
|
description: 'Tasks are now ready for review and prioritization'
|
||||||
.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">
|
<TabsContent value="upload" className="mt-0 focus-visible:outline-none focus-visible:ring-0">
|
||||||
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
<UploadSection onUploadComplete={handleUploadComplete} />
|
||||||
</div>
|
|
||||||
)}
|
{uploadCompleted && (
|
||||||
</CardHeader>
|
<div className="flex justify-center mt-6">
|
||||||
<CardContent>
|
<div className="glass-panel px-4 py-2 text-sm flex items-center">
|
||||||
<p className="text-3xl font-bold">{processingUploads}</p>
|
<AlertCircle className="w-4 h-4 mr-2 text-green-500" />
|
||||||
</CardContent>
|
Document processed successfully! Continue to Review Tasks.
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'upload' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setActiveTab('upload')}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Upload className="h-4 w-4" />
|
|
||||||
Upload Document
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant={activeTab === 'tasks' ? 'default' : 'outline'}
|
|
||||||
onClick={() => setActiveTab('tasks')}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FileText className="h-4 w-4" />
|
|
||||||
View Tasks
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeTab === 'upload' ? (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Upload Policy Document</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Upload your IT security or compliance policy documents to extract actionable tasks
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<UploadSection onUploadComplete={fetchDashboardData} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{recentUploads.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Recent Uploads</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Your most recent document uploads
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{recentUploads.map((upload) => (
|
|
||||||
<div key={upload.id} className="flex items-center justify-between border-b pb-4 last:border-0">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FilePlus className="h-6 w-6 text-muted-foreground" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">{upload.file_name}</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{new Date(upload.created_at).toLocaleDateString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
|
||||||
upload.status === 'Completed'
|
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
|
|
||||||
: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400'
|
|
||||||
}`}>
|
|
||||||
{upload.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
@ -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 }}"
|
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user