Spaces:
Running
Running
Download src/components/GoogleWorkspaceHub.tsx from caustino/CUSIPstudio: direct link, hf CLI and curl.
- Browser
- Download file 57.7 kB
-
https://huggingface.co/spaces/caustino/CUSIPstudio/resolve/main/src/components/GoogleWorkspaceHub.tsx
- Command line
-
hf download hf://spaces/caustino/CUSIPstudio/src/components/GoogleWorkspaceHub.tsx
-
curl -L -o GoogleWorkspaceHub.tsx https://huggingface.co/spaces/caustino/CUSIPstudio/resolve/main/src/components/GoogleWorkspaceHub.tsx
57.7 kB
| import React, { useState, useEffect } from 'react'; | |
| import { | |
| User, | |
| initAuth, | |
| googleSignIn, | |
| logout, | |
| getAccessToken, | |
| auth, | |
| db, | |
| handleFirestoreError, | |
| OperationType | |
| } from '../firebase'; | |
| import { | |
| listDriveFiles, | |
| uploadLegalFileToDrive, | |
| createPortfolioSheet, | |
| sendLegalEmail, | |
| createGmailDraft, | |
| listCalendarEvents, | |
| scheduleCourtEvent, | |
| createGoogleDoc, | |
| listChatSpaces, | |
| postChatMessage, | |
| DriveFileItem, | |
| ChatSpaceItem, | |
| CourtCalendarEvent | |
| } from '../services/googleWorkspaceService'; | |
| import { CourtSchedulingPanel } from './CourtSchedulingPanel'; | |
| import { | |
| collection, | |
| addDoc, | |
| onSnapshot, | |
| query, | |
| where, | |
| orderBy, | |
| limit, | |
| serverTimestamp | |
| } from 'firebase/firestore'; | |
| import { | |
| UNIFIED_DOCUMENT_METADATA, | |
| CUSIP_PORTFOLIO_DATA, | |
| ALGORITHM_SUITE, | |
| EMAIL_PRESENTATION_PACKAGES, | |
| RULE_18_EXACTION_ITERATIONS | |
| } from '../data/unifiedDocumentData'; | |
| import { | |
| FolderGit2, | |
| FileSpreadsheet, | |
| Mail, | |
| Calendar, | |
| Video, | |
| FileText, | |
| MessageSquare, | |
| Database, | |
| ShieldCheck, | |
| ExternalLink, | |
| RefreshCw, | |
| Send, | |
| Plus, | |
| AlertTriangle, | |
| CheckCircle2, | |
| Clock, | |
| Check, | |
| Sparkles, | |
| Layers, | |
| HardDrive, | |
| LogOut, | |
| UserCheck, | |
| Zap, | |
| Gavel | |
| } from 'lucide-react'; | |
| interface ActivityLog { | |
| id: string; | |
| service: string; | |
| action: string; | |
| resourceTitle?: string; | |
| timestamp: any; | |
| status: string; | |
| uid: string; | |
| } | |
| export const GoogleWorkspaceHub: React.FC = () => { | |
| const [user, setUser] = useState<User | null>(null); | |
| const [token, setToken] = useState<string | null>(null); | |
| const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false); | |
| const [activeWorkspaceTab, setActiveWorkspaceTab] = useState<'drive' | 'sheets' | 'gmail' | 'scheduling' | 'calendar' | 'docs' | 'chat'>('scheduling'); | |
| // Loading and feedback states | |
| const [statusMessage, setStatusMessage] = useState<{ type: 'success' | 'error' | 'info'; text: string } | null>(null); | |
| const [isLoading, setIsLoading] = useState<boolean>(false); | |
| // Drive state | |
| const [driveFiles, setDriveFiles] = useState<DriveFileItem[]>([]); | |
| // Calendar state | |
| const [calendarEvents, setCalendarEvents] = useState<any[]>([]); | |
| // Chat state | |
| const [chatSpaces, setChatSpaces] = useState<ChatSpaceItem[]>([]); | |
| const [selectedChatSpace, setSelectedChatSpace] = useState<string>(''); | |
| const [chatMessageText, setChatMessageText] = useState<string>(''); | |
| // Firestore activity state | |
| const [activities, setActivities] = useState<ActivityLog[]>([]); | |
| // Confirmation Modal State (MANDATORY User Confirmation for Destructive/Mutating Operations) | |
| const [confirmModal, setConfirmModal] = useState<{ | |
| isOpen: boolean; | |
| title: string; | |
| description: string; | |
| onConfirm: () => Promise<void>; | |
| }>({ | |
| isOpen: false, | |
| title: '', | |
| description: '', | |
| onConfirm: async () => {} | |
| }); | |
| // Initialize Auth | |
| useEffect(() => { | |
| const unsubscribe = initAuth( | |
| (currentUser, accessToken) => { | |
| setUser(currentUser); | |
| setToken(accessToken); | |
| }, | |
| () => { | |
| setUser(null); | |
| setToken(null); | |
| } | |
| ); | |
| return () => unsubscribe(); | |
| }, []); | |
| // Listen to Firestore activity log | |
| useEffect(() => { | |
| if (!user) { | |
| setActivities(prev => (prev.length > 0 ? [] : prev)); | |
| return; | |
| } | |
| try { | |
| const q = query( | |
| collection(db, 'workspace_activity'), | |
| where('uid', '==', user.uid), | |
| limit(20) | |
| ); | |
| const unsubscribe = onSnapshot( | |
| q, | |
| (snapshot) => { | |
| const list: ActivityLog[] = []; | |
| snapshot.forEach((doc) => { | |
| const data = doc.data(); | |
| list.push({ | |
| id: doc.id, | |
| service: data.service, | |
| action: data.action, | |
| resourceTitle: data.resourceTitle, | |
| timestamp: data.timestamp, | |
| status: data.status, | |
| uid: data.uid | |
| }); | |
| }); | |
| if (list.length > 0) { | |
| setActivities(prev => { | |
| const map = new Map<string, ActivityLog>(); | |
| list.forEach(item => map.set(item.id, item)); | |
| prev.forEach(item => { | |
| if (!map.has(item.id)) map.set(item.id, item); | |
| }); | |
| return Array.from(map.values()).slice(0, 50); | |
| }); | |
| } | |
| }, | |
| (error) => { | |
| console.warn('Firestore onSnapshot listener handled:', error); | |
| } | |
| ); | |
| return () => unsubscribe(); | |
| } catch (err) { | |
| console.warn('Error attaching Firestore listener:', err); | |
| } | |
| }, [user]); | |
| // Log activity to Firestore with resilient local fallback and graceful telemetry | |
| const logActivityToFirestore = async (service: string, action: string, resourceTitle?: string) => { | |
| if (!user) return; | |
| const nowIso = new Date().toISOString(); | |
| const localEntry: ActivityLog = { | |
| id: `act_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`, | |
| service, | |
| action, | |
| resourceTitle: resourceTitle || 'N/A', | |
| timestamp: nowIso, | |
| status: 'Completed', | |
| uid: user.uid | |
| }; | |
| // Immediate optimistic state update | |
| setActivities(prev => [localEntry, ...prev.filter(a => a.id !== localEntry.id)].slice(0, 50)); | |
| try { | |
| if (auth.currentUser) { | |
| await addDoc(collection(db, 'workspace_activity'), { | |
| service, | |
| action, | |
| resourceTitle: resourceTitle || 'N/A', | |
| timestamp: nowIso, | |
| status: 'Completed', | |
| uid: auth.currentUser.uid | |
| }); | |
| } | |
| } catch (error) { | |
| console.warn('Workspace activity Firestore persistence handled:', error); | |
| } | |
| }; | |
| // Google Login Handler | |
| const handleLogin = async () => { | |
| setIsLoggingIn(true); | |
| setStatusMessage(null); | |
| try { | |
| const res = await googleSignIn(); | |
| if (res) { | |
| setUser(res.user); | |
| setToken(res.accessToken); | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Connected to Google Workspace as ${res.user.email} with Drive, Sheets, Gmail, Calendar, Docs, Chat, and Meet enabled.` | |
| }); | |
| } | |
| } catch (err: any) { | |
| setStatusMessage({ | |
| type: 'error', | |
| text: err.message || 'Failed to authenticate with Google.' | |
| }); | |
| } finally { | |
| setIsLoggingIn(false); | |
| } | |
| }; | |
| const handleLogout = async () => { | |
| await logout(); | |
| setUser(null); | |
| setToken(null); | |
| setDriveFiles([]); | |
| setCalendarEvents([]); | |
| setChatSpaces([]); | |
| setStatusMessage({ type: 'info', text: 'Signed out of Google Workspace.' }); | |
| }; | |
| // --- DRIVE ACTIONS --- | |
| const handleFetchDriveFiles = async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const files = await listDriveFiles(token); | |
| setDriveFiles(files); | |
| setStatusMessage({ type: 'success', text: `Loaded ${files.length} files from Google Drive.` }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const promptUploadPleadingToDrive = () => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: 'Upload Omnibus Mandamus Petition to Google Drive', | |
| description: 'This operation will create a new legal document file "Omnibus_Consolidated_Mandamus_Petition_Baltimore_County.txt" in your Google Drive root directory. Proceed?', | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const content = `IN THE CIRCUIT COURT FOR BALTIMORE COUNTY, MARYLAND (Case No. C-03-CV-26-004128) | |
| AND IN THE U.S. COURT OF APPEALS FOR THE D.C. CIRCUIT (Case No. 24-10492) | |
| IN RE ESTATE OF CAUSTIN LEE MCLAUGHLIN & JUAN JOSE FERNANDEZ | |
| MASTER VERIFIED PETITION FOR MANDAMUS & RULE 18 CONSOLIDATED EXACTION OF LIBERTY | |
| Total Estate Capitalization: $500,000,000.00 Senior Notes | |
| Converted Equities: 45,850,000 Common Shares of Centrus Energy Corp. (NYSE: LEU) | |
| Clearing Venue: The Bank of New York Mellon (DTC Participant Desk) | |
| Co-Custodian: M&T Bank (ABA: 022000046, SWIFT: MANTUS33) | |
| Executed: November 1, 2026`; | |
| const file = await uploadLegalFileToDrive(token, 'Omnibus_Consolidated_Mandamus_Petition_Baltimore_County.txt', content); | |
| await logActivityToFirestore('drive', 'Upload File', file.name); | |
| setStatusMessage({ type: 'success', text: `Successfully uploaded "${file.name}" to Google Drive (ID: ${file.id}).` }); | |
| await handleFetchDriveFiles(); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| // --- SHEETS ACTIONS --- | |
| const promptExportPortfolioToSheets = () => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: 'Create Google Spreadsheet with $500M CUSIP Portfolio', | |
| description: 'This will create a new Google Spreadsheet in your Google Drive containing the complete 1999 Indenture CUSIP Reconciliation (Series A $350M, Series B $150M, 45,850,000 LEU shares) and SEC Form 13F Institutional Audit Table. Proceed?', | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const headers = ['Issuer', 'Title of Class', 'CUSIP', 'Value ($1,000s)', 'Shares / Principal', 'Discretion', 'Clearing Status']; | |
| const rows: (string | number)[][] = [headers]; | |
| CUSIP_PORTFOLIO_DATA.forEach(item => { | |
| rows.push([ | |
| item.issuer, | |
| item.titleOfClass, | |
| item.cusip, | |
| item.valueK, | |
| item.sharesOrPrincipal, | |
| item.discretion, | |
| item.relevance | |
| ]); | |
| }); | |
| const sheet = await createPortfolioSheet(token, 'USEC_Centrus_500M_CUSIP_Portfolio_Settlement', rows); | |
| await logActivityToFirestore('sheets', 'Create Spreadsheet', sheet.title); | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Created Google Sheet "${sheet.title}". URL: ${sheet.spreadsheetUrl}` | |
| }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| const promptExportAlgorithmsToSheets = () => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: 'Export 31 Financial Algorithms to Google Sheets', | |
| description: 'This will create a new Google Sheet containing all 31 algorithms, modules, formulations, and computational complexities. Proceed?', | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const headers = ['#', 'ID', 'Module', 'Algorithm Name', 'Purpose', 'Formulation', 'Complexity', 'Status']; | |
| const rows: (string | number)[][] = [headers]; | |
| ALGORITHM_SUITE.forEach(a => { | |
| rows.push([a.num, a.id, a.module, a.name, a.purpose, a.formulation, a.complexity, a.register]); | |
| }); | |
| const sheet = await createPortfolioSheet(token, 'Americana_Trust_31_Algorithm_Master_Suite', rows); | |
| await logActivityToFirestore('sheets', 'Export Algorithms', sheet.title); | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Created Google Sheet "${sheet.title}".` | |
| }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| // --- GMAIL ACTIONS --- | |
| const promptSendEmail = (pkg: typeof EMAIL_PRESENTATION_PACKAGES[0], isDraft: boolean) => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: isDraft ? `Create Gmail Draft for ${pkg.recipientOrg}` : `Send Email to ${pkg.recipientOrg}`, | |
| description: isDraft | |
| ? `This will create a draft email to "${pkg.toEmail}" in your Gmail account containing the complete legal filing presentation. Proceed?` | |
| : `Are you sure you want to SEND an email from your Gmail account to "${pkg.toEmail}" with subject "${pkg.subject}"? This cannot be undone.`, | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| if (isDraft) { | |
| await createGmailDraft(token, { | |
| to: pkg.toEmail, | |
| cc: pkg.ccEmail, | |
| subject: pkg.subject, | |
| body: pkg.emailBody | |
| }); | |
| await logActivityToFirestore('gmail', 'Create Draft', pkg.subject); | |
| setStatusMessage({ type: 'success', text: `Draft created in your Gmail for "${pkg.toEmail}".` }); | |
| } else { | |
| await sendLegalEmail(token, { | |
| to: pkg.toEmail, | |
| cc: pkg.ccEmail, | |
| subject: pkg.subject, | |
| body: pkg.emailBody | |
| }); | |
| await logActivityToFirestore('gmail', 'Send Email', pkg.subject); | |
| setStatusMessage({ type: 'success', text: `Email successfully sent via Gmail to "${pkg.toEmail}".` }); | |
| } | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| const promptBatchSendAllEmails = () => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: '⚡ 1-Click Send All 5 Packages via Gmail API', | |
| description: `This will dispatch all 5 formal legal packages (Baltimore County Circuit Court, D.C. Circuit Court of Appeals, The Bank of New York Mellon, M&T Bank, and Wyoming Secretary of State) from your connected Gmail account (${user?.email}) and register cryptographic delivery records in Firestore. Proceed?`, | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| let successCount = 0; | |
| try { | |
| for (const pkg of EMAIL_PRESENTATION_PACKAGES) { | |
| await sendLegalEmail(token, { | |
| to: pkg.toEmail, | |
| cc: pkg.ccEmail, | |
| subject: pkg.subject, | |
| body: pkg.emailBody | |
| }); | |
| await logActivityToFirestore('gmail', '1-Click Batch Send', `${pkg.recipientOrg}: ${pkg.subject}`); | |
| successCount++; | |
| } | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Successfully dispatched all ${successCount} correspondence packages and exhibits in 1 click via Gmail API!` | |
| }); | |
| } catch (err: any) { | |
| setStatusMessage({ | |
| type: successCount > 0 ? 'info' : 'error', | |
| text: `Dispatched ${successCount}/5 packages. Error: ${err.message}` | |
| }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| // --- CALENDAR & MEET ACTIONS --- | |
| const handleFetchCalendar = async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const items = await listCalendarEvents(token); | |
| setCalendarEvents(items); | |
| setStatusMessage({ type: 'success', text: `Retrieved ${items.length} upcoming calendar events.` }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const promptScheduleHearing = (withMeet: boolean) => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: withMeet ? 'Schedule Baltimore County Hearing with Google Meet' : 'Schedule Court Docket Hearing', | |
| description: `This will create a calendar event "Baltimore County Circuit Court Status Conference (Case C-03-CV-26-004128)" on your primary Google Calendar${withMeet ? ' with an active Google Meet video conference link' : ''}. Proceed?`, | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const startTime = new Date(Date.now() + 86400000 * 2); // 2 days from now | |
| startTime.setHours(10, 0, 0, 0); | |
| const endTime = new Date(startTime.getTime() + 3600000); // 1 hour duration | |
| const ev = await scheduleCourtEvent(token, { | |
| summary: 'Circuit Court for Baltimore County Hearing (Case No. C-03-CV-26-004128)', | |
| description: `Omnibus Mandamus, Rule 18 Consolidated Exaction of Liberty, and Indenture Specific Performance Conference. | |
| Petitioner: Caustin Lee McLaughlin & Juan Jose Fernandez | |
| Respondents: BNY Mellon, Computershare, Centrus Energy Corp. | |
| Location: Towson, MD 21204`, | |
| location: 'Circuit Court for Baltimore County, County Courts Building, Towson, MD 21204', | |
| startTime: startTime.toISOString(), | |
| endTime: endTime.toISOString(), | |
| createMeetRoom: withMeet | |
| }); | |
| await logActivityToFirestore(withMeet ? 'meet' : 'calendar', 'Schedule Event', ev.summary); | |
| const meetUrl = ev.conferenceData?.entryPoints?.find((ep: any) => ep.entryPointType === 'video')?.uri; | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Scheduled calendar event! ${meetUrl ? `Google Meet Link: ${meetUrl}` : ''}` | |
| }); | |
| await handleFetchCalendar(); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| // --- COURT SCHEDULING & MILESTONE INVITATION HANDLERS --- | |
| const promptScheduleMilestone = async (eventPayload: CourtCalendarEvent, milestoneTitle: string) => { | |
| const attendeeCount = eventPayload.attendees?.length || 0; | |
| const formattedStartTime = new Date(eventPayload.startTime).toLocaleString('en-US', { | |
| dateStyle: 'full', | |
| timeStyle: 'short' | |
| }); | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: `Schedule Court Appearance & Send Invitations`, | |
| description: `This will create the event "${eventPayload.summary}" on your primary Google Calendar scheduled for ${formattedStartTime} at "${eventPayload.location}". | |
| ${attendeeCount > 0 ? `Official Google Calendar email invitations will be dispatched to ${attendeeCount} parties: ${eventPayload.attendees?.map(a => a.email).join(', ')}.` : 'No external attendees selected.'} | |
| ${eventPayload.createMeetRoom ? 'An active Google Meet video conference link will be automatically generated and embedded in the invitations.' : ''} | |
| Proceed with scheduling and dispatching invitations?`, | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const ev = await scheduleCourtEvent(token, eventPayload); | |
| await logActivityToFirestore('calendar', 'Court Invitation Sent', `${milestoneTitle} (${attendeeCount} invitees)`); | |
| const meetUrl = ev.conferenceData?.entryPoints?.find((ep: any) => ep.entryPointType === 'video')?.uri; | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Successfully scheduled "${milestoneTitle}" and sent Google Calendar invitations to ${attendeeCount} parties! ${meetUrl ? `Google Meet Link: ${meetUrl}` : ''}` | |
| }); | |
| await handleFetchCalendar(); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| const promptBatchScheduleAllMilestones = async (events: { event: CourtCalendarEvent; milestoneTitle: string }[]) => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: `Batch Sync All Master Pleading Milestones (${events.length} Events)`, | |
| description: `This will automatically schedule ${events.length} court appearance dates from the Master Mandamus and Rule 18 Pleading on your Google Calendar, generate Google Meet video bridges, and send official calendar invitations to named co-trustees, depository bank representatives, and court clerks. | |
| Proceed with batch calendar generation and invitation dispatch?`, | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| let successCount = 0; | |
| const failed: string[] = []; | |
| try { | |
| for (const item of events) { | |
| try { | |
| await scheduleCourtEvent(token, item.event); | |
| await logActivityToFirestore('calendar', 'Batch Milestone Invite', item.milestoneTitle); | |
| successCount++; | |
| } catch (e: any) { | |
| console.warn(`Error scheduling ${item.milestoneTitle}:`, e); | |
| failed.push(item.milestoneTitle); | |
| } | |
| } | |
| await handleFetchCalendar(); | |
| setStatusMessage({ | |
| type: successCount > 0 ? 'success' : 'error', | |
| text: `Batch scheduled and dispatched invitations for ${successCount}/${events.length} court appearances to Google Calendar.${failed.length > 0 ? ` (Failed: ${failed.join(', ')})` : ''}` | |
| }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| // --- DOCS ACTIONS --- | |
| const promptExportPleadingToDoc = () => { | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: 'Export Omnibus Mandamus Petition to Google Docs', | |
| description: 'This will create an editable Google Doc in your Google Drive titled "Baltimore County Circuit Court & DC Circuit Omnibus Mandamus Petition" containing the full verified legal brief. Proceed?', | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const body = `IN THE CIRCUIT COURT FOR BALTIMORE COUNTY, MARYLAND | |
| CASE NO. C-03-CV-26-004128 | |
| IN RE ESTATE OF CAUSTIN LEE MCLAUGHLIN, Debtor-in-Possession | |
| AND JUAN JOSE FERNANDEZ, Co-Trustee | |
| v. | |
| THE BANK OF NEW YORK MELLON, et al. | |
| VERIFIED PETITION FOR PEREMPTORY WRIT OF MANDAMUS, | |
| DECLARATORY JUDGMENT & RULE 18 CONSOLIDATED EXACTION OF LIBERTY | |
| 1. Plenary Jurisdiction under Md. Rule 15-701 & 28 U.S.C. § 1651. | |
| 2. The $500,000,000.00 Senior Notes Indenture converts into 45,850,000 common shares of Centrus Energy Corp. (NYSE: LEU). | |
| 3. Rule 18 Exaction of Liberty Iterations I through IV joined pursuant to Fed. R. Civ. P. 18(a) and Md. Rule 2-303(b). | |
| 4. Depository clearing commanded to BNY Mellon DTC Participant desk. | |
| Executed: November 1, 2026`; | |
| const docResult = await createGoogleDoc(token, 'Baltimore_County_Circuit_Court_Omnibus_Mandamus_Petition', body); | |
| await logActivityToFirestore('docs', 'Create Document', docResult.title); | |
| setStatusMessage({ | |
| type: 'success', | |
| text: `Created Google Doc "${docResult.title}" (ID: ${docResult.documentId}).` | |
| }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| // --- CHAT ACTIONS --- | |
| const handleFetchChatSpaces = async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| const spaces = await listChatSpaces(token); | |
| setChatSpaces(spaces); | |
| if (spaces.length > 0 && !selectedChatSpace) { | |
| setSelectedChatSpace(spaces[0].name); | |
| } | |
| setStatusMessage({ type: 'success', text: `Found ${spaces.length} Google Chat spaces.` }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const promptPostChatMessage = () => { | |
| if (!selectedChatSpace || !chatMessageText.trim()) return; | |
| setConfirmModal({ | |
| isOpen: true, | |
| title: 'Post Message to Google Chat Space', | |
| description: `Are you sure you want to post this status update to Chat space "${selectedChatSpace}"? | |
| Message: "${chatMessageText}"`, | |
| onConfirm: async () => { | |
| if (!token) return; | |
| setIsLoading(true); | |
| try { | |
| await postChatMessage(token, selectedChatSpace, chatMessageText); | |
| await logActivityToFirestore('chat', 'Post Message', selectedChatSpace); | |
| setChatMessageText(''); | |
| setStatusMessage({ type: 'success', text: `Message posted successfully to Google Chat space.` }); | |
| } catch (err: any) { | |
| setStatusMessage({ type: 'error', text: err.message }); | |
| } finally { | |
| setIsLoading(false); | |
| setConfirmModal(prev => ({ ...prev, isOpen: false })); | |
| } | |
| } | |
| }); | |
| }; | |
| return ( | |
| <div className="max-w-7xl mx-auto space-y-6 pb-12"> | |
| {/* Top Banner */} | |
| <div className="bg-gradient-to-r from-blue-950/60 via-slate-900 to-indigo-950/70 border border-blue-500/40 rounded-xl p-6"> | |
| <div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <span className="px-2.5 py-0.5 rounded bg-blue-500/20 text-blue-300 font-mono text-xs font-bold border border-blue-500/30"> | |
| GOOGLE WORKSPACE & CLOUD FIRESTORE | |
| </span> | |
| <h2 className="text-xl sm:text-2xl font-bold text-white"> | |
| Google Workspace & Firebase Legal Integration Hub | |
| </h2> | |
| </div> | |
| <p className="text-xs sm:text-sm text-slate-300 mt-1 max-w-3xl"> | |
| Real-time Google Workspace connectivity across <strong>Google Drive</strong>, <strong>Google Sheets</strong>, <strong>Gmail</strong>, <strong>Google Calendar</strong>, <strong>Google Docs</strong>, <strong>Google Chat</strong>, and <strong>Google Meet</strong>, with cloud state persistence backed by Google Cloud Firestore. | |
| </p> | |
| </div> | |
| {/* Auth Action */} | |
| <div className="shrink-0 flex items-center gap-3"> | |
| {!user ? ( | |
| <button | |
| onClick={handleLogin} | |
| disabled={isLoggingIn} | |
| className="flex items-center gap-2.5 px-4 py-2 bg-white text-slate-900 hover:bg-slate-100 rounded-lg font-semibold text-xs transition-colors shadow-md disabled:opacity-50 cursor-pointer" | |
| > | |
| <svg className="w-4 h-4" viewBox="0 0 48 48"> | |
| <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/> | |
| <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/> | |
| <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/> | |
| <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/> | |
| </svg> | |
| <span>{isLoggingIn ? 'Connecting...' : 'Sign in with Google'}</span> | |
| </button> | |
| ) : ( | |
| <div className="flex items-center gap-3"> | |
| <div className="flex items-center gap-2 px-3 py-1.5 bg-slate-800/80 rounded-lg border border-slate-700 text-xs"> | |
| <div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div> | |
| <span className="font-mono text-emerald-300 font-semibold">{user.email}</span> | |
| </div> | |
| <button | |
| onClick={handleLogout} | |
| className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white rounded-lg text-xs flex items-center gap-1.5 transition-colors" | |
| > | |
| <LogOut className="w-3.5 h-3.5" /> | |
| <span>Disconnect</span> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Status Alert */} | |
| {statusMessage && ( | |
| <div className={`mt-4 p-3 rounded-lg text-xs flex items-start justify-between gap-2 ${ | |
| statusMessage.type === 'success' | |
| ? 'bg-emerald-950/60 border border-emerald-500/40 text-emerald-200' | |
| : statusMessage.type === 'error' | |
| ? 'bg-red-950/60 border border-red-500/40 text-red-200' | |
| : 'bg-indigo-950/60 border border-indigo-500/40 text-indigo-200' | |
| }`}> | |
| <div className="flex items-center gap-2"> | |
| {statusMessage.type === 'success' ? ( | |
| <CheckCircle2 className="w-4 h-4 text-emerald-400 shrink-0" /> | |
| ) : ( | |
| <AlertTriangle className="w-4 h-4 text-amber-400 shrink-0" /> | |
| )} | |
| <span>{statusMessage.text}</span> | |
| </div> | |
| <button | |
| onClick={() => setStatusMessage(null)} | |
| className="text-slate-400 hover:text-white" | |
| > | |
| × | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Main Workspace Tabs */} | |
| <div className="flex flex-wrap items-center gap-2 border-b border-slate-800 pb-3"> | |
| {[ | |
| { id: 'scheduling', label: 'Court Scheduling', icon: Gavel, badge: 'Pleading Sync' }, | |
| { id: 'calendar', label: 'Google Calendar & Meet', icon: Calendar }, | |
| { id: 'drive', label: 'Google Drive', icon: HardDrive }, | |
| { id: 'sheets', label: 'Google Sheets', icon: FileSpreadsheet }, | |
| { id: 'gmail', label: 'Gmail Dispatch', icon: Mail }, | |
| { id: 'docs', label: 'Google Docs', icon: FileText }, | |
| { id: 'chat', label: 'Google Chat', icon: MessageSquare } | |
| ].map((tab) => { | |
| const Icon = tab.icon; | |
| const isActive = activeWorkspaceTab === tab.id; | |
| return ( | |
| <button | |
| key={tab.id} | |
| onClick={() => setActiveWorkspaceTab(tab.id as any)} | |
| className={`flex items-center gap-2 px-3.5 py-2 rounded-lg text-xs font-semibold transition-all cursor-pointer ${ | |
| isActive | |
| ? tab.id === 'scheduling' | |
| ? 'bg-amber-600 text-white shadow-lg shadow-amber-900/50' | |
| : 'bg-blue-600 text-white shadow-lg' | |
| : 'bg-slate-900 text-slate-400 hover:text-slate-200 hover:bg-slate-800' | |
| }`} | |
| > | |
| <Icon className="w-3.5 h-3.5" /> | |
| <span>{tab.label}</span> | |
| {tab.badge && ( | |
| <span className={`text-[9px] font-bold px-1.5 py-0.2 rounded uppercase ${ | |
| isActive ? 'bg-black/30 text-amber-200' : 'bg-amber-500/20 text-amber-300' | |
| }`}> | |
| {tab.badge} | |
| </span> | |
| )} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {/* Unauthenticated notice */} | |
| {!user && ( | |
| <div className="p-8 bg-slate-900/60 border border-slate-800 rounded-xl text-center space-y-3"> | |
| <HardDrive className="w-8 h-8 text-blue-400 mx-auto" /> | |
| <h3 className="font-bold text-white text-base">Google Workspace Authentication Required</h3> | |
| <p className="text-xs text-slate-400 max-w-md mx-auto"> | |
| Click <strong>“Sign in with Google”</strong> above to grant client-side access to Google Drive, Google Sheets, Gmail, Google Calendar, Google Docs, Google Chat, and Google Meet. | |
| </p> | |
| </div> | |
| )} | |
| {/* Authenticated Panels */} | |
| {user && ( | |
| <div className="space-y-6"> | |
| {/* TAB 0: COURT SCHEDULING (DIRECT MASTER PLEADING SYNCHRONIZATION) */} | |
| {activeWorkspaceTab === 'scheduling' && ( | |
| <CourtSchedulingPanel | |
| token={token} | |
| userEmail={user.email} | |
| isLoading={isLoading} | |
| onScheduleMilestone={promptScheduleMilestone} | |
| onBatchScheduleAll={promptBatchScheduleAllMilestones} | |
| onRefreshCalendar={handleFetchCalendar} | |
| existingEvents={calendarEvents} | |
| /> | |
| )} | |
| {/* TAB 1: GOOGLE DRIVE */} | |
| {activeWorkspaceTab === 'drive' && ( | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-6"> | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800 pb-4"> | |
| <div> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <HardDrive className="w-5 h-5 text-blue-400" /> | |
| <span>Google Drive Document Storage & Pleading Vault</span> | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Store, organize, and inspect formal court pleadings and liquidation documents directly in Google Drive. | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={handleFetchDriveFiles} | |
| disabled={isLoading} | |
| className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-xs transition-colors cursor-pointer" | |
| > | |
| <RefreshCw className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin' : ''}`} /> | |
| <span>Refresh Files</span> | |
| </button> | |
| <button | |
| onClick={promptUploadPleadingToDrive} | |
| disabled={isLoading} | |
| className="flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-semibold transition-colors cursor-pointer shadow" | |
| > | |
| <Plus className="w-3.5 h-3.5" /> | |
| <span>Upload Omnibus Pleading</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* File list */} | |
| <div> | |
| <div className="text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2"> | |
| Recent Drive Files | |
| </div> | |
| {driveFiles.length === 0 ? ( | |
| <div className="p-6 bg-slate-950 rounded-lg border border-slate-800 text-center text-xs text-slate-500"> | |
| No files loaded yet. Click “Refresh Files” to browse your Google Drive or “Upload Omnibus Pleading” to store court records. | |
| </div> | |
| ) : ( | |
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> | |
| {driveFiles.map((file) => ( | |
| <div key={file.id} className="p-3 bg-slate-950 rounded-lg border border-slate-800 flex flex-col justify-between hover:border-slate-700 transition-colors"> | |
| <div> | |
| <div className="font-semibold text-xs text-white truncate" title={file.name}> | |
| {file.name} | |
| </div> | |
| <div className="text-[10px] text-slate-400 font-mono mt-0.5"> | |
| {file.mimeType} | |
| </div> | |
| </div> | |
| <div className="mt-3 pt-2 border-t border-slate-900 flex items-center justify-between text-[10px]"> | |
| <span className="text-slate-500">{file.size ? `${Math.round(Number(file.size)/1024)} KB` : 'Folder/Doc'}</span> | |
| {file.webViewLink && ( | |
| <a | |
| href={file.webViewLink} | |
| target="_blank" | |
| rel="noreferrer" | |
| className="text-blue-400 hover:underline flex items-center gap-1" | |
| > | |
| <span>Open in Drive</span> | |
| <ExternalLink className="w-2.5 h-2.5" /> | |
| </a> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 2: GOOGLE SHEETS */} | |
| {activeWorkspaceTab === 'sheets' && ( | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-6"> | |
| <div className="border-b border-slate-800 pb-4"> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <FileSpreadsheet className="w-5 h-5 text-emerald-400" /> | |
| <span>Google Sheets Portfolio & Algorithmic Export</span> | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Programmatically generate live Google Spreadsheets populated with the $500,000,000.00 CUSIP portfolio, SEC 13F holdings, and the complete 31-algorithm execution suite. | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="p-5 bg-slate-950 rounded-lg border border-slate-800 space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-bold text-white text-sm">1. $500M CUSIP Reconciliation Sheet</span> | |
| <span className="text-[10px] text-emerald-400 font-mono bg-emerald-950/40 px-2 py-0.5 rounded border border-emerald-500/30"> | |
| SEC 13F Form Data | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400"> | |
| Exports USEC Series A (917300AA1, $350M), Series B (917300AB9, $150M), and the 45,850,000 LEU common shares conversion matrix into Google Sheets. | |
| </p> | |
| <button | |
| onClick={promptExportPortfolioToSheets} | |
| disabled={isLoading} | |
| className="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold flex items-center justify-center gap-2 transition-colors cursor-pointer shadow" | |
| > | |
| <FileSpreadsheet className="w-3.5 h-3.5" /> | |
| <span>Create CUSIP Portfolio Sheet</span> | |
| </button> | |
| </div> | |
| <div className="p-5 bg-slate-950 rounded-lg border border-slate-800 space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-bold text-white text-sm">2. 31-Algorithm Suite Master Sheet</span> | |
| <span className="text-[10px] text-indigo-400 font-mono bg-indigo-950/40 px-2 py-0.5 rounded border border-indigo-500/30"> | |
| 11 Modules Catalog | |
| </span> | |
| </div> | |
| <p className="text-xs text-slate-400"> | |
| Exports all 31 algorithms, mathematical formulations, Big-O complexities, and implementation registers into a structured spreadsheet. | |
| </p> | |
| <button | |
| onClick={promptExportAlgorithmsToSheets} | |
| disabled={isLoading} | |
| className="w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold flex items-center justify-center gap-2 transition-colors cursor-pointer shadow" | |
| > | |
| <FileSpreadsheet className="w-3.5 h-3.5" /> | |
| <span>Create Algorithms Sheet</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 3: GMAIL DISPATCH */} | |
| {activeWorkspaceTab === 'gmail' && ( | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-6"> | |
| <div className="border-b border-slate-800 pb-4"> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <Mail className="w-5 h-5 text-red-400" /> | |
| <span>Gmail Legal Presentation Dispatch Hub</span> | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Send or draft authenticated presentment packages directly through your connected Gmail account (<code>{user.email}</code>) to court clerks, BNY Mellon, and counterparties. | |
| </p> | |
| </div> | |
| {/* 1-Click Batch Dispatch Entire Docket Banner */} | |
| <div className="p-4 bg-gradient-to-r from-indigo-950/80 via-slate-950 to-emerald-950/80 border border-indigo-500/30 rounded-xl flex flex-col sm:flex-row items-center justify-between gap-3 shadow-lg"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2.5 rounded-lg bg-amber-500/20 text-amber-300 border border-amber-500/30 shrink-0"> | |
| <Zap className="w-5 h-5 text-amber-400" /> | |
| </div> | |
| <div> | |
| <h4 className="font-bold text-white text-xs sm:text-sm"> | |
| 1-Click Batch Transmit All 5 Packages via Gmail API | |
| </h4> | |
| <p className="text-[11px] text-slate-300"> | |
| Dispatches all 5 court filings, BNY Mellon DTC instructions, and M&T Bank packages sequentially in one click with live delivery logging. | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={promptBatchSendAllEmails} | |
| disabled={isLoading} | |
| className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-amber-500 via-indigo-600 to-emerald-600 hover:brightness-110 text-white rounded-lg text-xs font-bold transition-all shadow-md cursor-pointer whitespace-nowrap" | |
| > | |
| <Zap className="w-3.5 h-3.5 text-amber-200" /> | |
| <span>⚡ 1-Click Send All</span> | |
| </button> | |
| </div> | |
| <div className="space-y-4"> | |
| {EMAIL_PRESENTATION_PACKAGES.map((pkg) => ( | |
| <div key={pkg.id} className="p-4 bg-slate-950 rounded-lg border border-slate-800 space-y-3"> | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80 pb-2"> | |
| <div> | |
| <div className="font-bold text-white text-sm">{pkg.recipientOrg}</div> | |
| <div className="text-xs text-slate-400"> | |
| To: <span className="font-mono text-indigo-300">{pkg.toEmail}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={() => promptSendEmail(pkg, true)} | |
| disabled={isLoading} | |
| className="px-3 py-1 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded text-xs transition-colors cursor-pointer" | |
| > | |
| Save as Draft | |
| </button> | |
| <button | |
| onClick={() => promptSendEmail(pkg, false)} | |
| disabled={isLoading} | |
| className="px-3 py-1 bg-red-600 hover:bg-red-500 text-white rounded text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1.5 shadow" | |
| > | |
| <Send className="w-3 h-3" /> | |
| <span>Send via Gmail</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="text-xs text-slate-300"> | |
| <strong>Subject:</strong> {pkg.subject} | |
| </div> | |
| <div className="text-[11px] text-slate-400"> | |
| <strong>Attachments ({pkg.attachedDocuments.length}):</strong>{' '} | |
| {pkg.attachedDocuments.map(d => d.title).join(', ')} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 4: GOOGLE CALENDAR & MEET */} | |
| {activeWorkspaceTab === 'calendar' && ( | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-6"> | |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800 pb-4"> | |
| <div> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <Calendar className="w-5 h-5 text-amber-400" /> | |
| <span>Google Calendar & Google Meet Court Conferences</span> | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Schedule case hearings and generate Google Meet video conference rooms for Baltimore County Circuit Court (Case No. C-03-CV-26-004128) and D.C. Circuit appeals. | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={handleFetchCalendar} | |
| disabled={isLoading} | |
| className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-xs transition-colors cursor-pointer" | |
| > | |
| <RefreshCw className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin' : ''}`} /> | |
| <span>Fetch Calendar</span> | |
| </button> | |
| <button | |
| onClick={() => promptScheduleHearing(true)} | |
| disabled={isLoading} | |
| className="flex items-center gap-1.5 px-3 py-1.5 bg-amber-600 hover:bg-amber-500 text-white rounded-lg text-xs font-semibold transition-colors cursor-pointer shadow" | |
| > | |
| <Video className="w-3.5 h-3.5" /> | |
| <span>Schedule Hearing + Google Meet</span> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Upcoming Events */} | |
| <div> | |
| <div className="text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2"> | |
| Upcoming Court Events | |
| </div> | |
| {calendarEvents.length === 0 ? ( | |
| <div className="p-6 bg-slate-950 rounded-lg border border-slate-800 text-center text-xs text-slate-500"> | |
| No upcoming events found or not yet fetched. Click “Fetch Calendar” or schedule a hearing. | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {calendarEvents.map((ev) => ( | |
| <div key={ev.id} className="p-3 bg-slate-950 rounded-lg border border-slate-800 flex items-center justify-between"> | |
| <div> | |
| <div className="font-semibold text-xs text-white">{ev.summary}</div> | |
| <div className="text-[10px] text-slate-400 mt-0.5"> | |
| {ev.start?.dateTime || ev.start?.date} — {ev.location || 'Online'} | |
| </div> | |
| </div> | |
| {ev.hangoutLink && ( | |
| <a | |
| href={ev.hangoutLink} | |
| target="_blank" | |
| rel="noreferrer" | |
| className="px-2.5 py-1 bg-blue-600/30 text-blue-300 border border-blue-500/40 rounded text-xs flex items-center gap-1 hover:bg-blue-600/50" | |
| > | |
| <Video className="w-3 h-3" /> | |
| <span>Join Meet</span> | |
| </a> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 5: GOOGLE DOCS */} | |
| {activeWorkspaceTab === 'docs' && ( | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-6"> | |
| <div className="border-b border-slate-800 pb-4"> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <FileText className="w-5 h-5 text-indigo-400" /> | |
| <span>Google Docs Legal Brief Synchronization</span> | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Export master pleadings and Rule 18 briefs into Google Docs for team drafting, commentary, and legal formatting. | |
| </p> | |
| </div> | |
| <div className="p-5 bg-slate-950 rounded-lg border border-slate-800 flex flex-col sm:flex-row items-center justify-between gap-4"> | |
| <div> | |
| <h4 className="font-bold text-white text-sm">Export Omnibus Verified Pleading to Google Docs</h4> | |
| <p className="text-xs text-slate-400 mt-1 max-w-xl"> | |
| Creates a collaborative Google Doc containing the caption, $500M capitalization specifications, Rule 18 exaction iterations, and BNY Mellon directives. | |
| </p> | |
| </div> | |
| <button | |
| onClick={promptExportPleadingToDoc} | |
| disabled={isLoading} | |
| className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold shrink-0 transition-colors shadow cursor-pointer flex items-center gap-2" | |
| > | |
| <FileText className="w-4 h-4" /> | |
| <span>Export to Google Docs</span> | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* TAB 6: GOOGLE CHAT */} | |
| {activeWorkspaceTab === 'chat' && ( | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-6"> | |
| <div className="border-b border-slate-800 pb-4"> | |
| <h3 className="text-lg font-bold text-white flex items-center gap-2"> | |
| <MessageSquare className="w-5 h-5 text-teal-400" /> | |
| <span>Google Chat Case Status Broadcaster</span> | |
| </h3> | |
| <p className="text-xs text-slate-400 mt-1"> | |
| Post real-time status updates and execution notices to your legal team's Google Chat spaces. | |
| </p> | |
| </div> | |
| <div className="space-y-4 max-w-xl"> | |
| <div className="flex items-center gap-3"> | |
| <button | |
| onClick={handleFetchChatSpaces} | |
| disabled={isLoading} | |
| className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-xs transition-colors cursor-pointer" | |
| > | |
| Fetch Chat Spaces | |
| </button> | |
| {chatSpaces.length > 0 && ( | |
| <select | |
| value={selectedChatSpace} | |
| onChange={(e) => setSelectedChatSpace(e.target.value)} | |
| className="flex-1 p-1.5 bg-slate-950 border border-slate-700 rounded text-xs text-slate-200 font-mono" | |
| > | |
| {chatSpaces.map(s => ( | |
| <option key={s.name} value={s.name}> | |
| {s.displayName || s.name} | |
| </option> | |
| ))} | |
| </select> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-xs text-slate-400 block">Message Text:</label> | |
| <textarea | |
| rows={3} | |
| value={chatMessageText} | |
| onChange={(e) => setChatMessageText(e.target.value)} | |
| placeholder="Enter status notice (e.g. Baltimore County filing submitted via MDEC...)" | |
| className="w-full p-2.5 bg-slate-950 border border-slate-700 rounded-lg text-xs text-slate-200 font-sans focus:outline-none focus:border-teal-500" | |
| /> | |
| <button | |
| onClick={promptPostChatMessage} | |
| disabled={!chatMessageText.trim() || !selectedChatSpace || isLoading} | |
| className="px-4 py-2 bg-teal-600 hover:bg-teal-500 text-white rounded-lg text-xs font-semibold transition-colors disabled:opacity-50 cursor-pointer flex items-center gap-2 shadow" | |
| > | |
| <Send className="w-3.5 h-3.5" /> | |
| <span>Post to Google Chat</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* FIRESTORE AUDIT LEDGER */} | |
| <div className="bg-slate-900/90 border border-slate-800 rounded-xl p-6 space-y-4"> | |
| <div className="flex items-center justify-between border-b border-slate-800 pb-3"> | |
| <div className="flex items-center gap-2"> | |
| <Database className="w-4 h-4 text-emerald-400" /> | |
| <h4 className="font-bold text-white text-sm"> | |
| Google Cloud Firestore — Real-Time Activity Audit Trail | |
| </h4> | |
| </div> | |
| <span className="text-[10px] font-mono text-emerald-400 bg-emerald-950/40 px-2 py-0.5 rounded border border-emerald-500/30"> | |
| Collection: workspace_activity | |
| </span> | |
| </div> | |
| {activities.length === 0 ? ( | |
| <div className="p-4 bg-slate-950 rounded-lg border border-slate-800 text-center text-xs text-slate-500"> | |
| No Workspace activities logged in Firestore yet. Execute a Drive, Sheets, or Gmail action above to see real-time database synchronization. | |
| </div> | |
| ) : ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-xs text-left border border-slate-800"> | |
| <thead> | |
| <tr className="bg-slate-950 text-slate-300"> | |
| <th className="p-2">Service</th> | |
| <th className="p-2">Action</th> | |
| <th className="p-2">Target Resource</th> | |
| <th className="p-2">Timestamp</th> | |
| <th className="p-2">Status</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-slate-800 font-mono text-[11px]"> | |
| {activities.map((act) => ( | |
| <tr key={act.id} className="hover:bg-slate-800/40"> | |
| <td className="p-2 font-bold uppercase text-blue-400">{act.service}</td> | |
| <td className="p-2 text-slate-200">{act.action}</td> | |
| <td className="p-2 text-slate-400 truncate max-w-xs">{act.resourceTitle}</td> | |
| <td className="p-2 text-slate-500">{typeof act.timestamp === 'string' ? act.timestamp.replace('T', ' ').slice(0, 19) : 'Recent'}</td> | |
| <td className="p-2 text-emerald-400 font-semibold">{act.status}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* MANDATORY USER CONFIRMATION MODAL */} | |
| {confirmModal.isOpen && ( | |
| <div className="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4"> | |
| <div className="bg-slate-900 border border-slate-700 rounded-xl max-w-md w-full p-6 shadow-2xl space-y-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 rounded-full bg-amber-500/20 text-amber-400 flex items-center justify-center shrink-0"> | |
| <AlertTriangle className="w-5 h-5" /> | |
| </div> | |
| <div> | |
| <h4 className="font-bold text-white text-base">{confirmModal.title}</h4> | |
| <p className="text-xs text-slate-400 mt-0.5">Google Workspace User Confirmation</p> | |
| </div> | |
| </div> | |
| <p className="text-xs text-slate-300 leading-relaxed bg-slate-950 p-3 rounded-lg border border-slate-800"> | |
| {confirmModal.description} | |
| </p> | |
| <div className="flex items-center justify-end gap-3 pt-2"> | |
| <button | |
| onClick={() => setConfirmModal(prev => ({ ...prev, isOpen: false }))} | |
| className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer" | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| onClick={confirmModal.onConfirm} | |
| className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-semibold transition-colors shadow cursor-pointer flex items-center gap-2" | |
| > | |
| <Check className="w-4 h-4" /> | |
| <span>Confirm & Proceed</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |