+
+
+ handleChange('event_name', e.target.value)}
+ placeholder="Event Name"
+ required
+ className="border-slate-200"
+ />
+
+
+
+ {(formData.order_type === 'one_time' || formData.order_type === 'rapid' || formData.order_type === 'permanent') && (
+
+
+ handleChange('date', e.target.value)}
+ required
+ className="border-slate-200"
+ />
+
)}
- {formData.is_recurring && (
+ {/* Hub Selection - Show if vendor has multiple locations */}
+ {isVendor && vendorHubs.length > 1 ? (
-
+
- )}
-
- {/* Contract Type and PO Reference */}
-
+ ) : (
-
-
-
-
-
+
handleChange('po_reference', e.target.value)}
- placeholder="PO reference"
+ id="hub"
+ value={formData.hub || ""}
+ onChange={(e) => handleChange('hub', e.target.value)}
+ placeholder="Hub location"
className="border-slate-200"
/>
-
-
- {/* Business Selection (only for vendors) */}
- {currentUser?.user_role === "vendor" && (
-
-
-
-
)}
+
- {/* Business Display (for clients - read-only) */}
- {currentUser?.user_role === "client" && formData.business_name && (
-
-
-
-
-
-
{formData.business_name}
- {formData.client_email && (
-
{formData.client_email}
+ {/* PO Reference - Now Optional */}
+
+
+ handleChange('po_reference', e.target.value)}
+ placeholder="PO reference"
+ className="border-slate-200"
+ />
+
+
+ {/* Client/Business Selection */}
+ {isVendor && (
+
+
+
+ {formData.business_id && businesses.find(b => b.id === formData.business_id)?.rate_group && (
+
+ ℹ️ Rate Group: {businesses.find(b => b.id === formData.business_id)?.rate_group} (Auto-detected from location)
+
+ )}
+
+ )}
+
+ {/* Zero Risk Backup Staff */}
+
+
+
{
+ handleChange('include_backup', checked);
+ if (!checked) handleChange('backup_staff_count', 0);
+ }}
+ className="mt-1"
+ />
+
+
+
+ Extra pool of staff in case of call-outs or no-shows
+
+ {formData.include_backup && (
+
+
+ handleChange('backup_staff_count', parseInt(e.target.value) || 1)}
+ className="w-24 mt-1 border-green-300"
+ />
+
+ )}
+
+
+
+
+
+
+
+ {/* Shifts Section */}
+ {formData.shifts.map((shift, shiftIndex) => (
+
+
+
+
+
+ {shiftIndex + 1}
+
+
+
{shift.shift_name}
+ {shift.location_address && (
+
+
+ {shift.location_address}
+
+ )}
+
+
+
+
+
+ {/* Roles */}
+
+ {shift.roles.map((role, roleIndex) => (
+
+
+
+ {roleIndex + 1}
+
+
+ {/* Service/Role with Search */}
+
+
+
setRoleSearchOpen(prev => ({ ...prev, [`${shiftIndex}-${roleIndex}`]: open }))}
+ >
+
+
+
+
+
+
+ No role found.
+
+ {availableRoles.map((roleName) => (
+ {
+ handleRoleChange(shiftIndex, roleIndex, 'role', roleName);
+ setRoleSearchOpen(prev => ({ ...prev, [`${shiftIndex}-${roleIndex}`]: false }));
+ }}
+ >
+ {roleName}
+
+ ))}
+
+
+
+
+
+
+ {/* Count */}
+
+
+
+
+
handleRoleChange(shiftIndex, roleIndex, 'count', parseInt(e.target.value) || 1)}
+ className="w-16 text-center"
+ />
+
+
+
+
+ {/* Start Time */}
+
+
+ handleRoleChange(shiftIndex, roleIndex, 'start_time', e.target.value)}
+ className="border-slate-200"
+ />
+
+
+ {/* End Time */}
+
+
+ handleRoleChange(shiftIndex, roleIndex, 'end_time', e.target.value)}
+ className="border-slate-200"
+ />
+
+
+
+ {shift.roles.length > 1 && (
+
+ )}
+
+
+
+ {/* Department */}
+
+
+ handleRoleChange(shiftIndex, roleIndex, 'department', e.target.value)}
+ placeholder="Department"
+ className="border-slate-200"
+ />
+
+
+ {/* Hours (Auto-calculated) */}
+
+
+
+
+ {role.hours || 0}
+
+
+
+
+ {/* Break (min) */}
+
+
+ handleRoleChange(shiftIndex, roleIndex, 'break_minutes', parseInt(e.target.value) || 0)}
+ className="border-slate-200"
+ />
+
+
+ {/* Uniform */}
+
+
+
+
+
+
+ {/* Rate and Total */}
+
+
+
+
+ $
+
+
+
+
+
+
+ ${(role.total_value || 0).toFixed(2)}
+
- )}
+ ))}
+
+ {/* Add Role Button */}
+
+
+
+ {/* Shift Total */}
+
+ Shift Total:
+
+ ${shift.roles.reduce((sum, r) => sum + (r.total_value || 0), 0).toFixed(2)}
+
+ ))}
- {/* Shifts Section */}
-
+ {/* Add Another Shift Button */}
+
- {/* Action Buttons */}
-
-
-
-
-
-
+ {/* Other Instructions */}
+
+
+
+
+
+
+ {/* Grand Total */}
+
+
+
+
+
Grand Total
+
+ {formData.shifts.reduce((sum, s) => sum + s.roles.reduce((roleSum, r) => roleSum + (r.count || 1), 0), 0)} roles across {formData.shifts.length} shift(s)
+
+
+
+ ${(formData.total || 0).toFixed(2)}
+
-
+
+
+
+ {/* Action Buttons */}
+
+
+
+
);
diff --git a/src/components/events/EventFormWizard.jsx b/src/components/events/EventFormWizard.jsx
new file mode 100644
index 00000000..036ed484
--- /dev/null
+++ b/src/components/events/EventFormWizard.jsx
@@ -0,0 +1,915 @@
+
+import React, { useState, useEffect } from "react";
+import { base44 } from "@/api/base44Client";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Calendar, Zap, RefreshCw, Users, Building2, Shield,
+ Plus, Minus, Trash2, Search, X, FileText, Save
+} from "lucide-react";
+import { format } from "date-fns";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Calendar as CalendarComponent } from "@/components/ui/calendar";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Textarea } from "@/components/ui/textarea";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
+
+const UNIFORM_TYPES = ["Type 1", "Type 2", "Type 3", "All Black", "Business Casual", "Chef Whites"];
+
+export default function EventFormWizard({ event, onSubmit, isSubmitting, currentUser, onCancel }) {
+ const [formData, setFormData] = useState(event || {
+ event_name: "",
+ order_type: "one_time",
+ recurrence_type: "single",
+ recurrence_start_date: "",
+ recurrence_end_date: "",
+ scatter_dates: [],
+ recurring_days: [],
+ recurring_frequency: "weekly",
+ business_id: "",
+ business_name: "",
+ hub: "",
+ department: "",
+ po_reference: "",
+ status: "Draft",
+ date: "",
+ include_backup: false,
+ backup_staff_count: 0,
+ shifts: [{
+ shift_name: "Shift 1",
+ location_address: "",
+ same_as_billing: true,
+ roles: [{
+ role: "",
+ department: "",
+ count: 1,
+ start_time: "09:00",
+ end_time: "17:00",
+ hours: 8,
+ uniform: "Type 1",
+ break_minutes: 30,
+ rate_per_hour: 0,
+ total_value: 0
+ }]
+ }],
+ notes: "",
+ total: 0
+ });
+
+ const [roleSearchOpen, setRoleSearchOpen] = useState({});
+
+ const { data: user } = useQuery({
+ queryKey: ['current-user-form'],
+ queryFn: () => base44.auth.me(),
+ enabled: !currentUser,
+ });
+
+ const currentUserData = currentUser || user;
+ const userRole = currentUserData?.user_role || currentUserData?.role || "admin";
+ const isVendor = userRole === "vendor";
+
+ const { data: businesses = [] } = useQuery({
+ queryKey: ['businesses'],
+ queryFn: () => base44.entities.Business.list(),
+ initialData: [],
+ });
+
+ const { data: allRates = [] } = useQuery({
+ queryKey: ['vendor-rates-all'],
+ queryFn: () => base44.entities.VendorRate.list(),
+ initialData: [],
+ });
+
+ const availableRoles = [...new Set(allRates.map(r => r.role_name))].sort();
+
+ useEffect(() => {
+ if (event) {
+ setFormData(event);
+ }
+ }, [event]);
+
+ const handleChange = (field, value) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleBusinessChange = (businessId) => {
+ const selectedBusiness = businesses.find(b => b.id === businessId);
+ if (selectedBusiness) {
+ setFormData(prev => ({
+ ...prev,
+ business_id: businessId,
+ business_name: selectedBusiness.business_name || "",
+ shifts: prev.shifts.map(shift => ({
+ ...shift,
+ location_address: shift.same_as_billing ? selectedBusiness.address || shift.location_address : shift.location_address
+ }))
+ }));
+ }
+ };
+
+ const calculateHours = (startTime, endTime, breakMinutes = 0) => {
+ if (!startTime || !endTime) return 0;
+ const [startHour, startMin] = startTime.split(':').map(Number);
+ const [endHour, endMin] = endTime.split(':').map(Number);
+ const startMinutes = startHour * 60 + startMin;
+ const endMinutes = endHour * 60 + endMin;
+ let totalMinutes = endMinutes - startMinutes;
+ if (totalMinutes < 0) totalMinutes += 24 * 60; // Handle overnight shifts
+ totalMinutes -= (breakMinutes || 0); // Subtract break
+ return Math.max(0, totalMinutes / 60);
+ };
+
+ const getRateForRole = (roleName) => {
+ const rate = allRates.find(r => r.role_name === roleName && r.is_active);
+ return rate ? parseFloat(rate.client_rate || 0) : 0;
+ };
+
+ const handleRoleChange = (shiftIndex, roleIndex, field, value) => {
+ setFormData(prev => {
+ const newShifts = [...prev.shifts];
+ const role = newShifts[shiftIndex].roles[roleIndex];
+ role[field] = value;
+
+ if (field === 'role') {
+ const rate = getRateForRole(value);
+ role.rate_per_hour = rate;
+ }
+
+ if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') {
+ role.hours = calculateHours(role.start_time, role.end_time, role.break_minutes);
+ }
+
+ role.total_value = (role.rate_per_hour || 0) * (role.hours || 0) * (role.count || 1);
+
+ return { ...prev, shifts: newShifts };
+ });
+ updateGrandTotal();
+ };
+
+ const updateGrandTotal = () => {
+ setTimeout(() => {
+ setFormData(prev => {
+ const total = prev.shifts.reduce((sum, shift) => {
+ const shiftTotal = shift.roles.reduce((roleSum, role) => roleSum + (role.total_value || 0), 0);
+ return sum + shiftTotal;
+ }, 0);
+ return { ...prev, total };
+ });
+ }, 0);
+ };
+
+ const handleAddRole = (shiftIndex) => {
+ setFormData(prev => {
+ const newShifts = [...prev.shifts];
+ newShifts[shiftIndex].roles.push({
+ role: "",
+ department: "",
+ count: 1,
+ start_time: "09:00",
+ end_time: "17:00",
+ hours: 8,
+ uniform: "Type 1",
+ break_minutes: 30, // Default to 30 min non-payable
+ rate_per_hour: 0,
+ total_value: 0
+ });
+ return { ...prev, shifts: newShifts };
+ });
+ };
+
+ const handleRemoveRole = (shiftIndex, roleIndex) => {
+ setFormData(prev => {
+ const newShifts = [...prev.shifts];
+ newShifts[shiftIndex].roles.splice(roleIndex, 1);
+ return { ...prev, shifts: newShifts };
+ });
+ updateGrandTotal();
+ };
+
+ const handleOrderTypeChange = (type) => {
+ setFormData(prev => ({
+ ...prev,
+ order_type: type,
+ date: "",
+ recurrence_start_date: "",
+ recurrence_end_date: "",
+ scatter_dates: [],
+ recurring_days: [],
+ recurring_frequency: "weekly",
+ recurrence_type: type === "recurring" ? "date_range" : "single"
+ }));
+ };
+
+ const handleScatterDateSelect = (dates) => {
+ setFormData(prev => ({
+ ...prev,
+ scatter_dates: dates?.map(d => format(d, 'yyyy-MM-dd')).sort() || []
+ }));
+ };
+
+ const handleDayToggle = (day) => {
+ setFormData(prev => {
+ const days = prev.recurring_days || [];
+ const exists = days.includes(day);
+ return {
+ ...prev,
+ recurring_days: exists ? days.filter(d => d !== day) : [...days, day].sort((a,b) => a-b)
+ };
+ });
+ };
+
+ const handleSubmit = (isDraft = false) => {
+ const status = isDraft ? "Draft" :
+ formData.order_type === "rapid" ? "Active" : "Pending";
+ onSubmit({ ...formData, status });
+ };
+
+ return (
+ <>
+
+
+
+ {/* Left Column - Order Type & Details */}
+
+ {/* Order Type */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Recurring Options - Enhanced with Calendar */}
+ {formData.order_type === 'recurring' && (
+
+
+ {['weekly', 'monthly', 'custom'].map(freq => (
+
+ ))}
+
+
+ {/* Weekly: Day Selector */}
+ {formData.recurring_frequency === 'weekly' && (
+
+
+
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, idx) => {
+ const isSelected = (formData.recurring_days || []).includes(idx);
+ return (
+
+ );
+ })}
+
+
+ )}
+
+ {/* Monthly: Date Range Picker */}
+ {formData.recurring_frequency === 'monthly' && (
+
+
+
+
+ This order will repeat monthly between the selected dates
+
+
+ )}
+
+ {/* Custom: Multiple Date Picker */}
+ {formData.recurring_frequency === 'custom' && (
+
+
+
+
+
+
+
+ new Date(d))}
+ onSelect={(dates) => handleScatterDateSelect(dates)}
+ numberOfMonths={2}
+ className="rounded-md"
+ />
+
+
+ {formData.scatter_dates && formData.scatter_dates.length > 0 && (
+
+
Selected dates:
+
+ {formData.scatter_dates.slice(0, 3).map((date, idx) => (
+
+ {format(new Date(date), 'MMM d')}
+
+ ))}
+ {formData.scatter_dates.length > 3 && (
+
+ +{formData.scatter_dates.length - 3} more
+
+ )}
+
+
+ )}
+
+ )}
+
+ )}
+
+
+
+ {/* Event Details - REORDERED */}
+
+
+
+
+ {/* 1. Hub (first) */}
+
+
+ handleChange('hub', e.target.value)}
+ placeholder="Hub location"
+ className="h-8 text-sm"
+ required
+ />
+
+
+ {/* 2. Department (new field) */}
+
+
+ handleChange('department', e.target.value)}
+ placeholder="Department name"
+ className="h-8 text-sm"
+ />
+
+
+ {/* 3. Date (required - only for non-recurring) */}
+ {formData.order_type !== 'recurring' && (
+
+
+ handleChange('date', e.target.value)}
+ className="h-8 text-sm"
+ required
+ />
+
+ )}
+
+ {/* 4. Event Name (now optional) */}
+
+
+ handleChange('event_name', e.target.value)}
+ placeholder="Event name (optional)"
+ className="h-8 text-sm"
+ />
+
+
+ {/* 5. PO Reference (optional) */}
+
+
+ handleChange('po_reference', e.target.value)}
+ placeholder="Purchase order number (optional)"
+ className="h-8 text-sm"
+ />
+
+
+ {isVendor && (
+
+
+
+
+ )}
+
+
+ handleChange('include_backup', checked)}
+ />
+
+
+
+
+
+ {/* Grand Total */}
+
+
+
+
Total
+
${(formData.total || 0).toFixed(2)}
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+
+ {/* Right Column - Shifts & Roles */}
+
+
+
+ {formData.shifts.map((shift, shiftIdx) => (
+
+
+
+
{shift.shift_name}
+
+
+ {shift.roles.map((role, roleIdx) => (
+
+ {/* Row 1: Role, Count, Times, Hours */}
+
+ {/* Role */}
+
+
+
setRoleSearchOpen(prev => ({ ...prev, [`${shiftIdx}-${roleIdx}`]: open }))}>
+
+
+
+
+
+
+ No role found.
+
+ {availableRoles.map((roleName) => (
+ {
+ handleRoleChange(shiftIdx, roleIdx, 'role', roleName);
+ setRoleSearchOpen(prev => ({ ...prev, [`${shiftIdx}-${roleIdx}`]: false }));
+ }}
+ className="text-xs"
+ >
+ {roleName}
+
+ ))}
+
+
+
+
+
+
+ {/* Count - More Visible */}
+
+
+
+
+
+ {role.count || 1}
+
+
+
+
+
+ {/* Start Time */}
+
+
+ handleRoleChange(shiftIdx, roleIdx, 'start_time', e.target.value)}
+ className="h-8 text-xs"
+ />
+
+
+ {/* End Time */}
+
+
+ handleRoleChange(shiftIdx, roleIdx, 'end_time', e.target.value)}
+ className="h-8 text-xs"
+ />
+
+
+ {/* Hours */}
+
+
+
+ {role.hours || 0}h
+
+
+
+ {/* Delete */}
+
+ {shift.roles.length > 1 && (
+
+ )}
+
+
+
+ {/* Row 2: Break Selection */}
+
+
+
+
+
+
+
+
+
+ {/* Rate & Total */}
+
+ Rate: ${(role.rate_per_hour || 0).toFixed(2)}/hr
+ Total: ${(role.total_value || 0).toFixed(2)}
+
+
+ ))}
+
+
+
+
+ ))}
+
+ {/* Notes */}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/messaging/MessageThread.jsx b/src/components/messaging/MessageThread.jsx
index 3dd6c241..c4633fbf 100644
--- a/src/components/messaging/MessageThread.jsx
+++ b/src/components/messaging/MessageThread.jsx
@@ -1,3 +1,4 @@
+
import React, { useRef, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -5,6 +6,18 @@ import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { FileText } from "lucide-react";
+// Safe date formatter
+const safeFormatDate = (dateString, formatStr) => {
+ if (!dateString) return "";
+ try {
+ const date = new Date(dateString);
+ if (isNaN(date.getTime())) return "";
+ return format(date, formatStr);
+ } catch {
+ return "";
+ }
+};
+
export default function MessageThread({ messages, currentUserId }) {
const messagesEndRef = useRef(null);
@@ -73,7 +86,7 @@ export default function MessageThread({ messages, currentUserId }) {
- {message.created_date && format(new Date(message.created_date), "MMM d, h:mm a")}
+ {safeFormatDate(message.created_date, "MMM d, h:mm a")}
@@ -83,4 +96,4 @@ export default function MessageThread({ messages, currentUserId }) {
);
-}
\ No newline at end of file
+}
diff --git a/src/components/vendor/DocumentViewer.jsx b/src/components/vendor/DocumentViewer.jsx
index c43c4d31..d22798a5 100644
--- a/src/components/vendor/DocumentViewer.jsx
+++ b/src/components/vendor/DocumentViewer.jsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -7,91 +7,54 @@ import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from "@/components/ui/dialog";
-import {
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "@/components/ui/tabs";
-import {
- FileText,
- Download,
- Printer,
- CheckCircle2,
+ Download,
+ Printer,
+ CheckCircle2,
Clock,
- StickyNote,
Eye,
- FileDown,
- Highlighter,
- MessageSquare,
- Smile,
- Edit3,
- Plus,
- X,
- AlertCircle,
ZoomIn,
ZoomOut,
Maximize2,
Minimize2,
ChevronLeft,
ChevronRight,
- Sparkles,
- Mic,
- Bookmark,
- Search,
- BookOpen,
- Brain,
- Shield,
- DollarSign,
- Scale,
- FileWarning,
- Lightbulb,
- TrendingUp,
- Filter
+ FileText,
+ RefreshCw,
+ PenTool,
+ Upload, // Added Upload icon
} from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
-import { format } from "date-fns";
-import { base44 } from "@/api/base44Client";
-import { ScrollArea } from "@/components/ui/scroll-area";
+import { useQuery } from "@tanstack/react-query"; // Added useQuery import
+import { base44 } from "@/api/base44Client"; // Added base44 import
-export default function DocumentViewer({
- documentUrl,
- documentName,
+export default function DocumentViewer({
+ documentUrl,
+ documentName,
documentType = "Contract",
- onSaveNotes,
onAcknowledge,
initialNotes = "",
isAcknowledged = false,
timeSpent = 0,
- initialAnnotations = []
}) {
const [notes, setNotes] = useState(initialNotes);
const [startTime] = useState(Date.now());
const [reviewTime, setReviewTime] = useState(timeSpent);
- const [annotations, setAnnotations] = useState(initialAnnotations || []);
- const [showAnnotationDialog, setShowAnnotationDialog] = useState(false);
- const [currentAnnotation, setCurrentAnnotation] = useState(null);
- const [annotationTool, setAnnotationTool] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const [zoom, setZoom] = useState(100);
const [isFullscreen, setIsFullscreen] = useState(false);
- const [bookmarks, setBookmarks] = useState([]);
- const [searchQuery, setSearchQuery] = useState("");
- const [aiSummary, setAiSummary] = useState(null);
- const [aiAnalysis, setAiAnalysis] = useState(null);
- const [isAnalyzing, setIsAnalyzing] = useState(false);
- const [activeTab, setActiveTab] = useState("annotations");
- const [noteCategory, setNoteCategory] = useState("general");
- const [isRecording, setIsRecording] = useState(false);
- const [viewMode, setViewMode] = useState("side-by-side"); // side-by-side, pdf-only, notes-only
+ const [signerName, setSignerName] = useState("");
+ const [signatureData, setSignatureData] = useState(null);
+ const [isDrawing, setIsDrawing] = useState(false);
+
+ const canvasRef = useRef(null);
const { toast } = useToast();
+ // Fetch current user to check for saved signature
+ const { data: currentUser } = useQuery({
+ queryKey: ['current-user-doc-viewer'],
+ queryFn: () => base44.auth.me(),
+ });
+
// Track time spent
useEffect(() => {
const interval = setInterval(() => {
@@ -102,60 +65,24 @@ export default function DocumentViewer({
return () => clearInterval(interval);
}, [startTime, timeSpent]);
- // AI Document Analysis
- const handleAIAnalysis = async () => {
- setIsAnalyzing(true);
- try {
- const response = await base44.integrations.Core.InvokeLLM({
- prompt: `Analyze this ${documentType} document and provide:
- 1. A concise executive summary (2-3 sentences)
- 2. Key obligations and responsibilities
- 3. Important dates and deadlines
- 4. Financial terms and payment details
- 5. Risk factors and concerns
- 6. Recommendations for the vendor
-
- Focus on practical, actionable insights for a vendor reviewing this document.`,
- file_urls: [documentUrl],
- response_json_schema: {
- type: "object",
- properties: {
- summary: { type: "string" },
- key_obligations: { type: "array", items: { type: "string" } },
- important_dates: { type: "array", items: { type: "string" } },
- financial_terms: { type: "array", items: { type: "string" } },
- risk_factors: { type: "array", items: { type: "string" } },
- recommendations: { type: "array", items: { type: "string" } },
- key_clauses: {
- type: "array",
- items: {
- type: "object",
- properties: {
- title: { type: "string" },
- content: { type: "string" },
- importance: { type: "string", enum: ["high", "medium", "low"] }
- }
- }
- }
- }
- }
- });
-
- setAiAnalysis(response);
- toast({
- title: "✨ AI Analysis Complete",
- description: "Document analyzed successfully",
- });
- } catch (error) {
- toast({
- title: "Analysis Failed",
- description: "Unable to analyze document. Please try again.",
- variant: "destructive",
- });
- } finally {
- setIsAnalyzing(false);
+ // Initialize canvas
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (canvas) {
+ const ctx = canvas.getContext('2d');
+ ctx.strokeStyle = '#1C323E';
+ ctx.lineWidth = 2;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
}
- };
+ }, []);
+
+ // Pre-fill signer name from user profile
+ useEffect(() => {
+ if (currentUser?.full_name && !signerName) {
+ setSignerName(currentUser.full_name);
+ }
+ }, [currentUser, signerName]);
const handlePrint = () => {
const printWindow = window.open(documentUrl, '_blank');
@@ -179,355 +106,171 @@ export default function DocumentViewer({
document.body.removeChild(link);
};
- const handleSaveNotes = () => {
- if (onSaveNotes) {
- const reviewData = {
- notes,
- reviewTime,
- annotations,
- bookmarks,
- aiAnalysis,
- lastPage: pageNumber,
- zoom,
- viewMode
- };
- onSaveNotes(reviewData);
-
+ const handleAcknowledge = async () => {
+ if (!signerName.trim()) {
toast({
- title: "✅ Progress Saved",
- description: `Saved ${annotations.length} annotations, ${bookmarks.length} bookmarks, and your notes`,
+ title: "Name Required",
+ description: "Please enter your full name before accepting.",
+ variant: "destructive",
});
+ return;
+ }
+
+ if (!signatureData) {
+ toast({
+ title: "Signature Required",
+ description: "Please sign the document before accepting.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ // Save signature to user profile for future use
+ if (currentUser?.id && signatureData) {
+ try {
+ await base44.auth.updateMe({ saved_signature: signatureData });
+ } catch (error) {
+ console.error("Failed to save signature to profile:", error);
+ }
}
- };
- const handleAcknowledge = () => {
if (onAcknowledge) {
- const reviewData = {
+ onAcknowledge({
notes,
reviewTime,
- annotations,
- bookmarks,
- aiAnalysis,
- lastPage: pageNumber,
- zoom,
- viewMode
- };
- onAcknowledge(reviewData);
- }
- };
-
- const handleAddAnnotation = (type) => {
- setAnnotationTool(type);
- setCurrentAnnotation({
- id: Date.now().toString(),
- type: type,
- page_number: pageNumber,
- selected_text: "",
- comment: "",
- emoji: "",
- category: noteCategory,
- color: type === 'highlight' ? 'yellow' : 'blue',
- created_date: new Date().toISOString()
- });
- setShowAnnotationDialog(true);
- };
-
- const handleSaveAnnotation = () => {
- if (currentAnnotation) {
- if (currentAnnotation.type === 'highlight' && !currentAnnotation.selected_text) {
- toast({
- title: "Missing Text",
- description: "Please enter the text you want to highlight",
- variant: "destructive",
- });
- return;
- }
-
- if ((currentAnnotation.type === 'comment' || currentAnnotation.type === 'question') && !currentAnnotation.comment) {
- toast({
- title: "Missing Comment",
- description: "Please enter a comment or question",
- variant: "destructive",
- });
- return;
- }
-
- setAnnotations([...annotations, currentAnnotation]);
- setShowAnnotationDialog(false);
- setCurrentAnnotation(null);
- setAnnotationTool(null);
-
- toast({
- title: "Annotation Added",
- description: `${currentAnnotation.type.charAt(0).toUpperCase() + currentAnnotation.type.slice(1)} added to page ${pageNumber}`,
+ signerName,
+ signature: signatureData,
+ acknowledgedDate: new Date().toISOString()
});
}
};
- const handleDeleteAnnotation = (annotationId) => {
- setAnnotations(annotations.filter(a => a.id !== annotationId));
- };
-
- const handleAddBookmark = () => {
- const bookmark = {
- id: Date.now().toString(),
- page: pageNumber,
- label: `Page ${pageNumber}`,
- created_date: new Date().toISOString()
- };
- setBookmarks([...bookmarks, bookmark]);
- toast({
- title: "Bookmark Added",
- description: `Page ${pageNumber} bookmarked`,
- });
- };
-
- const handleVoiceNote = async () => {
- if (!isRecording) {
- // Check if browser supports speech recognition
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
-
- if (!SpeechRecognition) {
- toast({
- title: "Not Supported",
- description: "Voice notes are not supported in your browser. Please use Chrome or Edge.",
- variant: "destructive",
- });
- return;
- }
-
- const recognition = new SpeechRecognition();
- recognition.continuous = true;
- recognition.interimResults = true;
- recognition.lang = 'en-US'; // Set language
-
- let transcript = '';
-
- recognition.onstart = () => {
- setIsRecording(true);
- toast({
- title: "🎤 Recording Started",
- description: "Speak your notes...",
- });
- };
-
- recognition.onresult = (event) => {
- transcript = Array.from(event.results)
- .map(result => result[0].transcript)
- .join('');
- };
-
- recognition.onerror = (event) => {
- console.error('Speech recognition error:', event.error);
- setIsRecording(false);
- toast({
- title: "Recording Error",
- description: `Failed to record voice note: ${event.error}. Please try again.`,
- variant: "destructive",
- });
- };
-
- recognition.onend = () => {
- setIsRecording(false);
- if (transcript.trim()) {
- // Add voice note as annotation
- const voiceAnnotation = {
- id: Date.now().toString(),
- type: 'comment',
- page_number: pageNumber,
- selected_text: '',
- comment: `🎤 Voice Note: ${transcript}`,
- category: noteCategory,
- color: 'blue',
- created_date: new Date().toISOString()
- };
- setAnnotations([...annotations, voiceAnnotation]);
-
- toast({
- title: "✅ Voice Note Saved",
- description: "Your voice note has been added to annotations",
- });
- } else {
- toast({
- title: "Recording Stopped",
- description: "No speech was detected or recorded.",
- });
- }
- };
-
- recognition.start();
-
- // Auto-stop after 30 seconds
- setTimeout(() => {
- if (isRecording) { // Check if still recording before stopping
- recognition.stop();
- toast({
- title: "Recording Auto-Stopped",
- description: "Voice note recording stopped after 30 seconds.",
- });
- }
- }, 30000);
- } else {
- // If already recording and button is clicked again, stop recording manually
- // The browser's recognition object handles stopping on its own via onend if not continuous.
- // For continuous, we need to manually stop if the button is clicked again.
- // This part might need access to the `recognition` object if we want to stop it explicitly here.
- // For simplicity, let's assume `onend` handles it or the auto-stop will eventually trigger.
- setIsRecording(false); // Update UI state
- }
- };
-
- const handleExportNotes = () => {
- // Create a formatted text export of all notes and annotations
- let exportText = `DOCUMENT REVIEW NOTES\n`;
- exportText += `Document: ${documentName}\n`;
- exportText += `Review Date: ${format(new Date(), 'PPP')}\n`;
- exportText += `Total Review Time: ${reviewTime} minutes\n`;
- exportText += `\n${'='.repeat(50)}\n\n`;
-
- // Add AI Summary if available
- if (aiAnalysis && aiAnalysis.summary) {
- exportText += `AI ANALYSIS SUMMARY\n`;
- exportText += `${'-'.repeat(50)}\n`;
- exportText += `${aiAnalysis.summary}\n\n`;
- }
-
- // Add annotations grouped by page
- if (annotations.length > 0) {
- exportText += `ANNOTATIONS (${annotations.length})\n`;
- exportText += `${'-'.repeat(50)}\n`;
- Object.keys(groupedAnnotations).sort((a, b) => parseInt(a) - parseInt(b)).forEach(page => {
- exportText += `\nPage ${page}:\n`;
- groupedAnnotations[page].forEach((annotation, idx) => {
- exportText += ` ${idx + 1}. [${annotation.type.toUpperCase()}]`;
- if (annotation.category && annotation.category !== 'general') exportText += ` (${annotation.category})`;
- exportText += ` - ${format(new Date(annotation.created_date), 'MMM d, h:mm a')}\n`;
- if (annotation.selected_text) {
- exportText += ` Text: "${annotation.selected_text}"\n`;
- }
- if (annotation.comment) {
- exportText += ` Note: ${annotation.comment}\n`;
- }
- if (annotation.emoji) {
- exportText += ` Reaction: ${annotation.emoji}\n`;
- }
- exportText += `\n`;
- });
- });
- }
-
- // Add bookmarks
- if (bookmarks.length > 0) {
- exportText += `\nBOOKMARKS (${bookmarks.length})\n`;
- exportText += `${'-'.repeat(50)}\n`;
- bookmarks.forEach((bookmark, idx) => {
- exportText += ` ${idx + 1}. Page ${bookmark.page} - ${bookmark.label} - ${format(new Date(bookmark.created_date), 'MMM d, h:mm a')}\n`;
- });
- exportText += `\n`;
- }
-
- // Add summary notes
- if (notes) {
- exportText += `\nSUMMARY NOTES\n`;
- exportText += `${'-'.repeat(50)}\n`;
- exportText += `${notes}\n\n`;
- }
-
- // Create and download file
- const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `${documentName.replace(/[^a-z0-9]/gi, '_')}_Review_Notes_${format(new Date(), 'yyyy-MM-dd')}.txt`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
-
- toast({
- title: "📄 Notes Exported",
- description: "Your review notes have been downloaded",
- });
- };
-
const handleFullscreenToggle = () => {
if (!isFullscreen) {
- // Enter fullscreen
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
- } else if (document.documentElement.mozRequestFullScreen) { /* Firefox */
+ } else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
- } else if (document.documentElement.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
+ } else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen();
- } else if (document.documentElement.msRequestFullscreen) { /* IE/Edge */
+ } else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
}
} else {
- // Exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
- } else if (document.mozCancelFullScreen) { /* Firefox */
+ } else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
- } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
- document.webkitExitFullscreen();
- } else if (document.msExitFullscreen) { /* IE/Edge */
- document.msExitFullscreen();
+ } else if (document.webkitExitFullscreen) {
+ document.exitFullscreen(); // Corrected for webkit
+ } else if (document.msExitFullscreen) {
+ document.exitFullscreen(); // Corrected for ms
}
}
setIsFullscreen(!isFullscreen);
};
- const groupedAnnotations = annotations.reduce((acc, annotation) => {
- const page = annotation.page_number || 1;
- if (!acc[page]) acc[page] = [];
- acc[page].push(annotation);
- return acc;
- }, {});
+ // Signature pad functions
+ const startDrawing = (e) => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ const rect = canvas.getBoundingClientRect();
+
+ setIsDrawing(true);
+
+ const x = (e.clientX || e.touches?.[0]?.clientX) - rect.left;
+ const y = (e.clientY || e.touches?.[0]?.clientY) - rect.top;
+
+ ctx.beginPath();
+ ctx.moveTo(x, y);
+ };
- const filteredAnnotations = searchQuery
- ? annotations.filter(a =>
- a.selected_text?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- a.comment?.toLowerCase().includes(searchQuery.toLowerCase())
- )
- : annotations;
+ const draw = (e) => {
+ if (!isDrawing) return;
+
+ e.preventDefault();
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ const rect = canvas.getBoundingClientRect();
+
+ const x = (e.clientX || e.touches?.[0]?.clientX) - rect.left;
+ const y = (e.clientY || e.touches?.[0]?.clientY) - rect.top;
+
+ ctx.lineTo(x, y);
+ ctx.stroke();
+ };
- const getAnnotationIcon = (type) => {
- switch(type) {
- case 'highlight': return
;
- case 'comment': return
;
- case 'question': return
;
- case 'emoji': return
;
- case 'signature': return
;
- default: return
;
+ const stopDrawing = () => {
+ if (isDrawing) {
+ const canvas = canvasRef.current;
+ if (canvas) {
+ setSignatureData(canvas.toDataURL('image/png'));
+ }
+ setIsDrawing(false);
}
};
- const getAnnotationColor = (type) => {
- switch(type) {
- case 'highlight': return 'bg-yellow-100 text-yellow-700 border-yellow-300';
- case 'comment': return 'bg-blue-100 text-blue-700 border-blue-300';
- case 'question': return 'bg-red-100 text-red-700 border-red-300';
- case 'emoji': return 'bg-purple-100 text-purple-700 border-purple-300';
- case 'signature': return 'bg-green-100 text-green-700 border-green-300';
- default: return 'bg-slate-100 text-slate-700 border-slate-300';
+ const clearSignature = () => {
+ const canvas = canvasRef.current;
+ if (canvas) {
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
}
+ setSignatureData(null);
};
- const getCategoryIcon = (category) => {
- switch(category) {
- case 'concern': return
;
- case 'question': return
;
- case 'important': return
;
- case 'financial': return
;
- default: return
;
+ // NEW: Adopt saved signature
+ const adoptSavedSignature = () => {
+ if (!currentUser?.saved_signature) {
+ toast({
+ title: "No Saved Signature",
+ description: "You don't have a saved signature yet.",
+ variant: "destructive",
+ });
+ return;
}
+
+ const canvas = canvasRef.current;
+ if (!canvas) {
+ toast({
+ title: "Error",
+ description: "Signature pad not ready.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ const ctx = canvas.getContext('2d');
+ const img = new Image();
+
+ img.onload = () => {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+ setSignatureData(currentUser.saved_signature);
+ toast({
+ title: "Signature Adopted",
+ description: "Your saved signature has been loaded.",
+ });
+ };
+
+ img.onerror = () => {
+ toast({
+ title: "Failed to Load Signature",
+ description: "Could not load your saved signature.",
+ variant: "destructive",
+ });
+ };
+
+ img.src = currentUser.saved_signature;
};
const googleDocsViewerUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(documentUrl)}&embedded=true`;
return (
- {/* Modern Floating Control Bar */}
+ {/* Simplified Control Bar */}
@@ -546,74 +289,20 @@ export default function DocumentViewer({
{reviewTime} min
- {annotations.length > 0 && (
-
-
- {annotations.length} notes
-
- )}
- {/* Center: View Controls */}
-