pmtool / src /pages /communications.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
78.2 kB
import React, { useState, useEffect, useRef } from 'react';
import { format } from 'date-fns';
import {
MessageCircle,
Plus,
Search,
Edit,
Trash2,
Calendar,
Users,
Info,
MessageSquare,
Video,
Phone,
Building2,
MoreHorizontal,
Loader2,
LayoutDashboard,
Check,
AlertCircle,
PlusCircle,
Briefcase,
Filter,
ChevronDown,
CalendarIcon,
X
} from 'lucide-react';
import { Communication, ProjectApi, CommunicationUpdateRequest, CommunicationCreateRequest } from '../types';
import {
getAllCommunications,
createCommunication,
updateCommunication,
deleteCommunication,
} from '../services/communicationApi';
// UI Components
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import CustomDialog from '@/components/CustomDialog';
import Header from '@/components/layout/header';
import Sidebar from '@/components/layout/sidebar';
import { useIsMobile } from '@/hooks/use-mobile';
import { toast } from '@/lib/custom-toast';
import {
Select as RadixSelect,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { projectService } from '@/lib/api';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { useLocation, useNavigate } from 'react-router-dom';
import SlateRichTextEditor from "@/components/custom/SlateRichTextEditor";
// Mode icons mapping
const getModeIcon = (mode: string) => {
switch (mode.toLowerCase()) {
case 'in-person':
return <Building2 className="h-4 w-4 text-primary" />;
case 'virtual':
return <Video className="h-4 w-4 text-purple-500" />;
case 'phone':
return <Phone className="h-4 w-4 text-orange-500" />;
case 'email':
return <MessageSquare className="h-4 w-4 text-blue-500" />;
default:
return <Info className="h-4 w-4 text-gray-500" />;
}
};
// Badge variant mapping
const getModeBadgeVariant = (mode: string): "default" | "secondary" | "destructive" | "outline" => {
switch (mode.toLowerCase()) {
case 'in-person':
return 'default';
case 'virtual':
return 'secondary';
case 'phone':
return 'destructive';
case 'email':
return 'outline';
default:
return 'outline';
}
};
// Add HtmlContent component
const HtmlContent: React.FC<{ html: string }> = ({ html }) => {
return (
<div dangerouslySetInnerHTML={{ __html: html }} />
);
};
const Communications: React.FC = () => {
// State management
const isMobile = useIsMobile();
const [isSidebarOpen, setIsSidebarOpen] = useState(!isMobile);
const [communications, setCommunications] = useState<Communication[]>([]);
const [filteredCommunications, setFilteredCommunications] = useState<Communication[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [projects, setProjects] = useState<ProjectApi[]>([]);
const [projectsLoading, setProjectsLoading] = useState(false);
// Get query parameters
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const shouldOpenCreateDialog = queryParams.get('new') === 'true';
const preSelectedProjectId = queryParams.get('projectId');
// Close sidebar on mobile when route changes
useEffect(() => {
if (isMobile) {
setIsSidebarOpen(false);
}
}, [location, isMobile]);
// Update sidebar state when screen size changes
useEffect(() => {
setIsSidebarOpen(!isMobile);
}, [isMobile]);
// Filter states
const [modeFilter, setModeFilter] = useState<string | null>(null);
const [dateFilter, setDateFilter] = useState<Date | null>(null);
const [projectFilter, setProjectFilter] = useState<number | null>(null);
// Dialog states
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const [notification, setNotification] = useState<{ type: 'success' | 'error' | 'info'; message: string } | null>(null);
const [selectedCommunication, setSelectedCommunication] = useState<Communication | null>(null);
// Refs for event handlers
const dateFilterClickHandlerRef = useRef<((evt: MouseEvent) => void) | null>(null);
const modeFilterClickHandlerRef = useRef<((evt: MouseEvent) => void) | null>(null);
const projectFilterClickHandlerRef = useRef<((evt: MouseEvent) => void) | null>(null);
// Form data
const [formData, setFormData] = useState<Partial<Communication>>({
topic: '',
details: '',
decisionMade: '',
mode: 'In-person',
participants: '',
communicationDate: new Date().toISOString(),
projectId: preSelectedProjectId ? parseInt(preSelectedProjectId) : null,
});
const [submitLoading, setSubmitLoading] = useState(false);
const navigate = useNavigate();
// Fetch communications on component mount
useEffect(() => {
fetchCommunications();
fetchProjects();
}, []);
// Open create dialog if query param is present
useEffect(() => {
if (shouldOpenCreateDialog && projects.length > 0) {
handleOpenCreateDialog();
// Remove query parameters after opening dialog
navigate('/communications', { replace: true });
}
}, [shouldOpenCreateDialog, projects.length, navigate]);
// Fetch all projects
const fetchProjects = async () => {
try {
setProjectsLoading(true);
const projectsData = await projectService.getAll();
setProjects(projectsData);
setProjectsLoading(false);
} catch (err) {
console.error('Error fetching projects:', err);
setProjectsLoading(false);
}
};
// Filter communications when search query or filters change
useEffect(() => {
let filtered = communications;
// Text search filter
if (searchQuery.trim() !== '') {
const lowercaseQuery = searchQuery.toLowerCase();
filtered = filtered.filter(
(comm) =>
comm.topic.toLowerCase().includes(lowercaseQuery) ||
comm.details.toLowerCase().includes(lowercaseQuery) ||
comm.participants.toLowerCase().includes(lowercaseQuery) ||
comm.mode.toLowerCase().includes(lowercaseQuery)
);
}
// Mode filter
if (modeFilter) {
filtered = filtered.filter(
(comm) => comm.mode.toLowerCase() === modeFilter.toLowerCase()
);
}
// Date filter
if (dateFilter) {
const filterDate = new Date(dateFilter);
filterDate.setHours(0, 0, 0, 0);
filtered = filtered.filter((comm) => {
const commDate = new Date(comm.communicationDate);
commDate.setHours(0, 0, 0, 0);
return commDate.getTime() === filterDate.getTime();
});
}
// Project filter
if (projectFilter !== null) {
filtered = filtered.filter(
(comm) => comm.projectId === projectFilter
);
}
setFilteredCommunications(filtered);
}, [searchQuery, communications, modeFilter, dateFilter, projectFilter]);
const clearAllFilters = () => {
setSearchQuery('');
setModeFilter(null);
setDateFilter(null);
setProjectFilter(null);
};
// Toggle sidebar
const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
};
const fetchCommunications = async () => {
try {
setLoading(true);
setError(null); // Clear any previous errors
const response = await getAllCommunications();
console.log("Communications API response:", response);
// If response is an array, it's a direct data array
if (Array.isArray(response)) {
setCommunications(response);
setFilteredCommunications(response);
}
// If response has a success property, it's a ResponseData object
else if (response && typeof response.success !== 'undefined') {
if (response.success && Array.isArray(response.data)) {
setCommunications(response.data);
setFilteredCommunications(response.data);
} else {
// API returned success: false
setError(response.message || 'Failed to fetch communications');
}
}
// Unexpected response format
else {
console.error('Unexpected API response format:', response);
setError('Received invalid data format from server');
}
setLoading(false);
} catch (err) {
console.error('Error fetching communications:', err);
setError('Failed to connect to the server. Please try again later.');
setLoading(false);
}
};
const handleOpenCreateDialog = () => {
setSelectedCommunication(null);
setFormData({
topic: '',
details: '',
decisionMade: '',
mode: 'In-person',
participants: '',
communicationDate: new Date().toISOString(),
projectId: preSelectedProjectId ? parseInt(preSelectedProjectId) : null,
});
setIsCreateDialogOpen(true);
};
const handleOpenEditDialog = (communication: Communication) => {
setSelectedCommunication(communication);
setFormData({
communicationId: communication.communicationId,
topic: communication.topic,
details: communication.details,
decisionMade: communication.decisionMade,
mode: communication.mode,
participants: communication.participants,
communicationDate: communication.communicationDate,
projectId: communication.projectId,
});
setIsEditDialogOpen(true);
};
const handleCloseCreateDialog = () => {
setIsCreateDialogOpen(false);
};
const handleCloseEditDialog = () => {
setIsEditDialogOpen(false);
};
const handleOpenDeleteDialog = (communication: Communication) => {
setSelectedCommunication(communication);
setOpenDeleteDialog(true);
};
const handleCloseDeleteDialog = () => {
setOpenDeleteDialog(false);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({ ...prev, communicationDate: new Date(e.target.value).toISOString() }));
};
const handleProjectChange = (value: string) => {
setFormData((prev) => ({
...prev,
projectId: value === "none" ? null : parseInt(value)
}));
};
const handleModeChange = (value: string) => {
setFormData((prev) => ({
...prev,
mode: value
}));
};
const handleCreateSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.stopPropagation();
// Validate required fields
if (!formData.projectId) {
toast.error('Please select a project');
return;
}
setSubmitLoading(true);
try {
const createData: CommunicationCreateRequest = {
topic: formData.topic || '',
details: formData.details || '',
decisionMade: formData.decisionMade || '',
mode: formData.mode || 'In-person',
participants: formData.participants || '',
communicationDate: formData.communicationDate || new Date().toISOString(),
projectId: formData.projectId,
};
console.log('Creating communication with data:', createData);
// Create new communication
await createCommunication(createData);
toast.success('Communication created successfully');
handleCloseCreateDialog();
fetchCommunications();
} catch (err: any) {
console.error('Error creating communication:', err);
const errorMessage = err.response?.data?.message || err.message || 'Failed to create communication';
toast.error(errorMessage);
} finally {
setSubmitLoading(false);
}
};
const handleEditSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.stopPropagation();
if (!selectedCommunication) return;
setSubmitLoading(true);
try {
const updateData: CommunicationUpdateRequest = {
communicationId: selectedCommunication.communicationId,
topic: formData.topic || '',
details: formData.details || '',
decisionMade: formData.decisionMade || '',
mode: formData.mode || 'In-person',
participants: formData.participants || '',
communicationDate: formData.communicationDate || new Date().toISOString(),
projectId: formData.projectId,
};
console.log('Updating communication with data:', updateData);
// Update existing communication
await updateCommunication(
selectedCommunication.communicationId,
updateData
);
toast.success('Communication updated successfully');
handleCloseEditDialog();
fetchCommunications();
} catch (err: any) {
console.error('Error updating communication:', err);
const errorMessage = err.response?.data?.message || err.message || 'Failed to update communication';
toast.error(errorMessage);
} finally {
setSubmitLoading(false);
}
};
const handleDelete = async () => {
if (!selectedCommunication) return;
try {
console.log(`Deleting communication with ID: ${selectedCommunication.communicationId}`);
const response = await deleteCommunication(selectedCommunication.communicationId);
if (response.success) {
toast.success('Communication deleted successfully');
handleCloseDeleteDialog();
fetchCommunications();
} else {
toast.error(response.message || 'Failed to delete communication');
}
} catch (err: any) {
console.error('Error deleting communication:', err);
const errorMessage = err.response?.data?.message || err.message || 'Failed to delete communication';
toast.error(errorMessage);
}
};
const formatDate = (dateString: string) => {
try {
return format(new Date(dateString), 'MMM dd, yyyy hh:mm a');
} catch (err) {
return 'Invalid date';
}
};
// Statistics for summary cards
const totalCommunications = communications.length;
const inPersonMeetings = communications.filter(c => c.mode.toLowerCase() === 'in-person').length;
const virtualMeetings = communications.filter(c => c.mode.toLowerCase() === 'virtual').length;
const otherCommunications = totalCommunications - inPersonMeetings - virtualMeetings;
// Get project name by ID
const getProjectName = (projectId: number | null | undefined): string => {
if (!projectId) return 'None';
const project = projects.find(p => p.id === projectId);
return project ? project.projectName : 'Unknown Project';
};
return (
<div className="flex min-h-screen flex-col">
<Header toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} />
<Sidebar isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} />
<main className={`flex-1 p-6 pt-20 md:p-10 md:pt-24 ${isSidebarOpen ? "md:ml-64" : ""} transition-all duration-300`}>
<div className="container mx-auto">
{notification && (
<Alert variant={notification.type === 'error' ? 'destructive' : 'default'} className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{notification.type === 'success' ? 'Success' : notification.type === 'error' ? 'Error' : 'Info'}</AlertTitle>
<AlertDescription>{notification.message}</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Communication Trail</h1>
<p className="text-muted-foreground">
Track and manage all communications within the organization
</p>
</div>
<Button onClick={() => handleOpenCreateDialog()}>
<PlusCircle className="mr-2 h-4 w-4" /> New Communication
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Communications
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-2xl font-bold">{totalCommunications}</div>
<LayoutDashboard className="h-4 w-4 text-muted-foreground" />
</div>
<p className="text-xs text-muted-foreground mt-1">All recorded communications</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
In-Person Meetings
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-2xl font-bold">{inPersonMeetings}</div>
<Building2 className="h-4 w-4 text-primary" />
</div>
<p className="text-xs text-muted-foreground mt-1">Face-to-face communications</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Virtual Meetings
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-2xl font-bold">{virtualMeetings}</div>
<Video className="h-4 w-4 text-purple-500" />
</div>
<p className="text-xs text-muted-foreground mt-1">Online video conferences</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Other Communications
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-2xl font-bold">{otherCommunications}</div>
<Phone className="h-4 w-4 text-orange-500" />
</div>
<p className="text-xs text-muted-foreground mt-1">Phone, email and other formats</p>
</CardContent>
</Card>
</div>
{/* Search and Filters */}
<div className="mt-6 mb-4">
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center mb-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search communications..."
className="pl-10"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex gap-2 flex-wrap sm:flex-nowrap">
{/* Date filter - custom dropdown */}
<div className="relative inline-block">
<Button
variant="outline"
className="flex gap-1 items-center h-10 pr-10"
onClick={(e) => {
e.stopPropagation();
const id = 'date-filter-dropdown';
const dropdown = document.getElementById(id);
// Close all other dropdowns first
['mode-filter-dropdown', 'project-filter-dropdown'].forEach(dropdownId => {
if (dropdownId !== id) {
document.getElementById(dropdownId)?.classList.add('hidden');
}
});
if (dropdown) {
dropdown.classList.toggle('hidden');
// Remove any existing listeners first to prevent duplicates
if (dateFilterClickHandlerRef.current) {
document.removeEventListener('click', dateFilterClickHandlerRef.current);
}
// Add click outside handler
dateFilterClickHandlerRef.current = (evt: MouseEvent) => {
if (!dropdown.contains(evt.target as Node) &&
!(e.target as HTMLElement).contains(evt.target as Node)) {
dropdown.classList.add('hidden');
if (dateFilterClickHandlerRef.current) {
document.removeEventListener('click', dateFilterClickHandlerRef.current);
}
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
if (dateFilterClickHandlerRef.current) {
document.addEventListener('click', dateFilterClickHandlerRef.current);
}
}, 0);
}
}}
>
<CalendarIcon className="h-4 w-4" />
{dateFilter ? format(dateFilter, 'MMM dd, yyyy') : "Filter by Date"}
<ChevronDown className="h-3 w-3 ml-1 opacity-50 absolute right-3" />
</Button>
<div
id="date-filter-dropdown"
className="absolute right-0 mt-2 z-10 bg-white shadow-lg border rounded-md p-2 w-[300px] hidden"
>
<div className="flex flex-col space-y-1 w-full">
<div className="grid grid-cols-7 gap-1 text-center text-xs font-medium text-gray-500">
<div>S</div>
<div>M</div>
<div>T</div>
<div>W</div>
<div>T</div>
<div>F</div>
<div>S</div>
</div>
<div className="grid grid-cols-7 gap-1">
{Array.from({ length: 31 }, (_, i) => {
const day = i + 1;
const date = new Date();
date.setDate(day);
// Calculate if this is the selected date
const isSelected = dateFilter ?
date.getDate() === dateFilter.getDate() &&
date.getMonth() === dateFilter.getMonth() : false;
return (
<button
key={day}
className={`h-8 w-8 rounded-md text-sm flex items-center justify-center ${
isSelected
? 'bg-primary text-white font-medium'
: 'hover:bg-slate-100'
}`}
onClick={(e) => {
e.stopPropagation();
const newDate = new Date();
newDate.setDate(day);
setDateFilter(newDate);
document.getElementById('date-filter-dropdown')?.classList.add('hidden');
}}
>
{day}
</button>
);
})}
</div>
{dateFilter && (
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={(e) => {
e.stopPropagation();
setDateFilter(null);
document.getElementById('date-filter-dropdown')?.classList.add('hidden');
}}
>
Clear Date
</Button>
)}
</div>
</div>
</div>
{/* Mode filter - custom dropdown */}
<div className="relative inline-block">
<Button
variant="outline"
className="flex gap-1 items-center h-10 pr-10"
onClick={(e) => {
e.stopPropagation();
const id = 'mode-filter-dropdown';
const dropdown = document.getElementById(id);
// Close all other dropdowns first
['date-filter-dropdown', 'project-filter-dropdown'].forEach(dropdownId => {
if (dropdownId !== id) {
document.getElementById(dropdownId)?.classList.add('hidden');
}
});
if (dropdown) {
dropdown.classList.toggle('hidden');
// Remove any existing listeners first to prevent duplicates
if (modeFilterClickHandlerRef.current) {
document.removeEventListener('click', modeFilterClickHandlerRef.current);
}
// Add click outside handler
modeFilterClickHandlerRef.current = (evt: MouseEvent) => {
if (!dropdown.contains(evt.target as Node) &&
!(e.target as HTMLElement).contains(evt.target as Node)) {
dropdown.classList.add('hidden');
if (modeFilterClickHandlerRef.current) {
document.removeEventListener('click', modeFilterClickHandlerRef.current);
}
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
if (modeFilterClickHandlerRef.current) {
document.addEventListener('click', modeFilterClickHandlerRef.current);
}
}, 0);
}
}}
>
<Filter className="h-4 w-4" />
{modeFilter ? `Mode: ${modeFilter}` : "Filter by Mode"}
<ChevronDown className="h-3 w-3 ml-1 opacity-50 absolute right-3" />
</Button>
<div
id="mode-filter-dropdown"
className="absolute right-0 mt-2 z-10 bg-white shadow-lg border rounded-md p-2 min-w-[180px] hidden"
>
<div className="py-1">
<div className="px-2 text-xs font-medium text-gray-500 mb-1">Communication Mode</div>
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setModeFilter('In-person');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
<Building2 className="h-4 w-4 mr-2 text-primary" />
<span>In-person</span>
</button>
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setModeFilter('Virtual');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
<Video className="h-4 w-4 mr-2 text-purple-500" />
<span>Virtual</span>
</button>
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setModeFilter('Phone');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
<Phone className="h-4 w-4 mr-2 text-orange-500" />
<span>Phone</span>
</button>
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setModeFilter('Email');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
<MessageSquare className="h-4 w-4 mr-2 text-blue-500" />
<span>Email</span>
</button>
</div>
{modeFilter && (
<div className="pt-1 mt-1 border-t">
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setModeFilter(null);
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
<span>Clear Mode Filter</span>
</button>
</div>
)}
</div>
</div>
{/* Project filter - custom dropdown */}
{projects.length > 0 && (
<div className="relative inline-block">
<Button
variant="outline"
className="flex gap-1 items-center h-10 pr-10"
onClick={(e) => {
e.stopPropagation();
const id = 'project-filter-dropdown';
const dropdown = document.getElementById(id);
// Close all other dropdowns first
['date-filter-dropdown', 'mode-filter-dropdown'].forEach(dropdownId => {
if (dropdownId !== id) {
document.getElementById(dropdownId)?.classList.add('hidden');
}
});
if (dropdown) {
dropdown.classList.toggle('hidden');
// Remove any existing listeners first to prevent duplicates
if (projectFilterClickHandlerRef.current) {
document.removeEventListener('click', projectFilterClickHandlerRef.current);
}
// Add click outside handler
projectFilterClickHandlerRef.current = (evt: MouseEvent) => {
if (!dropdown.contains(evt.target as Node) &&
!(e.target as HTMLElement).contains(evt.target as Node)) {
dropdown.classList.add('hidden');
if (projectFilterClickHandlerRef.current) {
document.removeEventListener('click', projectFilterClickHandlerRef.current);
}
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
if (projectFilterClickHandlerRef.current) {
document.addEventListener('click', projectFilterClickHandlerRef.current);
}
}, 0);
}
}}
>
<Briefcase className="h-4 w-4" />
{projectFilter !== null
? `Project: ${getProjectName(projectFilter)}`
: "Filter by Project"}
<ChevronDown className="h-3 w-3 ml-1 opacity-50 absolute right-3" />
</Button>
<div
id="project-filter-dropdown"
className="absolute right-0 mt-2 z-10 bg-white shadow-lg border rounded-md p-2 min-w-[200px] max-h-[300px] overflow-y-auto hidden"
>
<div className="py-1">
<div className="px-2 text-xs font-medium text-gray-500 mb-1">Select Project</div>
{projects.map((project) => (
<button
key={project.id}
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setProjectFilter(project.id);
document.getElementById('project-filter-dropdown')?.classList.add('hidden');
}}
>
<span>{project.projectName}</span>
</button>
))}
</div>
{projectFilter !== null && (
<div className="pt-1 mt-1 border-t">
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
setProjectFilter(null);
document.getElementById('project-filter-dropdown')?.classList.add('hidden');
}}
>
<span>Clear Project Filter</span>
</button>
</div>
)}
</div>
</div>
)}
{(searchQuery || modeFilter || dateFilter || projectFilter !== null) && (
<Button
variant="ghost"
onClick={clearAllFilters}
size="sm"
className="h-10"
>
Clear All
</Button>
)}
</div>
</div>
{/* Active Filters */}
{(modeFilter || dateFilter || projectFilter !== null) && (
<div className="flex gap-2 flex-wrap mb-3">
{modeFilter && (
<Badge variant="secondary" className="flex items-center gap-1">
{getModeIcon(modeFilter)}
Mode: {modeFilter}
<button
className="ml-1 h-4 w-4 rounded-full inline-flex items-center justify-center hover:bg-slate-200"
onClick={() => setModeFilter(null)}
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{dateFilter && (
<Badge variant="secondary" className="flex items-center gap-1">
<CalendarIcon className="h-3 w-3" />
Date: {format(dateFilter, 'MMM dd, yyyy')}
<button
className="ml-1 h-4 w-4 rounded-full inline-flex items-center justify-center hover:bg-slate-200"
onClick={() => setDateFilter(null)}
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{projectFilter !== null && (
<Badge variant="secondary" className="flex items-center gap-1">
<Briefcase className="h-3 w-3" />
Project: {getProjectName(projectFilter)}
<button
className="ml-1 h-4 w-4 rounded-full inline-flex items-center justify-center hover:bg-slate-200"
onClick={() => setProjectFilter(null)}
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
</div>
)}
</div>
{/* Communications Table/Cards */}
{loading ? (
<div className="flex justify-center items-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : error ? (
<Alert variant="destructive" className="m-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : (
<>
{/* Desktop View - Table */}
<div className="hidden md:block">
<Card>
<CardContent className="p-0">
<div className="relative overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Topic</TableHead>
<TableHead>Date & Time</TableHead>
<TableHead>Mode</TableHead>
<TableHead>Participants</TableHead>
<TableHead>Project</TableHead>
<TableHead>Decision Made</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCommunications.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
No communications found
</TableCell>
</TableRow>
) : (
filteredCommunications.map((communication) => (
<TableRow key={communication.communicationId}>
<TableCell className="font-medium">{communication.topic}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
{formatDate(communication.communicationDate)}
</div>
</TableCell>
<TableCell>
<Badge
variant={getModeBadgeVariant(communication.mode)}
className="flex items-center gap-1"
>
{getModeIcon(communication.mode)}
{communication.mode}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Users className="h-3.5 w-3.5 text-muted-foreground" />
<span className="truncate max-w-[200px]" title={communication.participants}>
{communication.participants.split(',').length > 2
? `${communication.participants.split(',').slice(0, 2).join(', ')} +${
communication.participants.split(',').length - 2
}`
: communication.participants}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Briefcase className="h-3.5 w-3.5 text-muted-foreground" />
{getProjectName(communication.projectId)}
</div>
</TableCell>
<TableCell>
<div className="truncate max-w-[200px]" title={communication.decisionMade || ''}>
{communication.decisionMade && (
<HtmlContent html={communication.decisionMade} />
)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenEditDialog(communication)}
className="h-8 w-8 p-0"
>
<Edit className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenDeleteDialog(communication)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive/90 hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
{/* Mobile View - Cards */}
<div className="md:hidden space-y-4">
{filteredCommunications.length === 0 ? (
<Card>
<CardContent className="text-center py-8 text-muted-foreground">
No communications found
</CardContent>
</Card>
) : (
filteredCommunications.map((communication) => (
<Card key={communication.communicationId}>
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-lg">{communication.topic}</CardTitle>
<div className="relative">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => {
const id = `mobile-actions-${communication.communicationId}`;
const menu = document.getElementById(id);
if (menu) menu.classList.toggle('hidden');
}}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
<div
id={`mobile-actions-${communication.communicationId}`}
className="absolute right-0 top-8 z-10 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 shadow-md hidden"
>
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={() => {
const id = `mobile-actions-${communication.communicationId}`;
const menu = document.getElementById(id);
if (menu) menu.classList.add('hidden');
handleOpenEditDialog(communication);
}}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</button>
<button
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-red-600"
onClick={() => {
const id = `mobile-actions-${communication.communicationId}`;
const menu = document.getElementById(id);
if (menu) menu.classList.add('hidden');
handleOpenDeleteDialog(communication);
}}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</button>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2 mt-2">
<Badge
variant={getModeBadgeVariant(communication.mode)}
className="flex items-center gap-1"
>
{getModeIcon(communication.mode)}
{communication.mode}
</Badge>
<Badge variant="outline" className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{formatDate(communication.communicationDate).split(',')[0]}
</Badge>
</div>
</CardHeader>
<CardContent className="pb-3 space-y-3">
<div>
<div className="text-sm font-medium text-muted-foreground mb-1">Participants:</div>
<div className="flex items-center gap-1 text-sm">
<Users className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
<span>{communication.participants}</span>
</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground mb-1">Project:</div>
<div className="flex items-center gap-1 text-sm">
<Briefcase className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
<span>{getProjectName(communication.projectId)}</span>
</div>
</div>
{communication.decisionMade && (
<div>
<div className="text-sm font-medium text-muted-foreground mb-1">Decision:</div>
<div className="text-sm">
<HtmlContent html={communication.decisionMade} />
</div>
</div>
)}
</CardContent>
</Card>
))
)}
</div>
</>
)}
</div>
{/* Create Communication Dialog */}
<CustomDialog
isOpen={isCreateDialogOpen}
onClose={handleCloseCreateDialog}
title="Add New Communication"
description="Create a new communication record. Fill out the form below."
isPending={submitLoading}
>
<form onSubmit={handleCreateSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="topic">Topic</Label>
<Input
id="topic"
name="topic"
placeholder="Enter communication topic"
value={formData.topic || ''}
onChange={handleInputChange}
required
/>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="details">Details</Label>
<SlateRichTextEditor
initialValue={formData.details || ''}
onChange={(value) => {
setFormData(prev => ({
...prev,
details: value
}));
}}
placeholder="Enter communication details"
minHeight="150px"
/>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="decisionMade">Decision Made</Label>
<SlateRichTextEditor
initialValue={formData.decisionMade || ''}
onChange={(value) => {
setFormData(prev => ({
...prev,
decisionMade: value
}));
}}
placeholder="Enter decisions made during this communication"
minHeight="150px"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="mode">Mode</Label>
<div className="relative">
<Button
type="button"
variant="outline"
className="w-full justify-between"
onClick={(e) => {
e.preventDefault();
const dropdown = document.getElementById('create-mode-dropdown');
if (dropdown) {
dropdown.classList.toggle('hidden');
// Add click outside handler
const clickHandler = (evt: any) => {
if (!dropdown.contains(evt.target) &&
!(e.target as HTMLElement).contains(evt.target)) {
dropdown.classList.add('hidden');
document.removeEventListener('click', clickHandler);
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
document.addEventListener('click', clickHandler);
}, 0);
}
}}
>
<span className="flex items-center gap-2">
{getModeIcon(formData.mode || 'In-person')}
{formData.mode || 'Select Mode'}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
<div
id="create-mode-dropdown"
className="absolute left-0 right-0 top-full mt-1 z-50 bg-white shadow-lg border rounded-md p-1 hidden"
>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('In-person');
document.getElementById('create-mode-dropdown')?.classList.add('hidden');
}}
>
<Building2 className="h-4 w-4 mr-2 text-primary" />
<span>In-person</span>
</button>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('Virtual');
document.getElementById('create-mode-dropdown')?.classList.add('hidden');
}}
>
<Video className="h-4 w-4 mr-2 text-purple-500" />
<span>Virtual</span>
</button>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('Phone');
document.getElementById('create-mode-dropdown')?.classList.add('hidden');
}}
>
<Phone className="h-4 w-4 mr-2 text-orange-500" />
<span>Phone</span>
</button>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('Email');
document.getElementById('create-mode-dropdown')?.classList.add('hidden');
}}
>
<MessageSquare className="h-4 w-4 mr-2 text-blue-500" />
<span>Email</span>
</button>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="communicationDate">Date</Label>
<Input
id="communicationDate"
name="communicationDate"
type="datetime-local"
value={formData.communicationDate ? new Date(formData.communicationDate).toISOString().slice(0, 16) : ''}
onChange={handleDateChange}
required
/>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="participants">Participants</Label>
<Input
id="participants"
name="participants"
placeholder="Enter participant names (comma separated)"
value={formData.participants || ''}
onChange={handleInputChange}
required
/>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="projectId">
Project <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Button
type="button"
variant="outline"
className={`w-full justify-between ${!formData.projectId ? 'border-destructive' : ''}`}
onClick={(e) => {
e.preventDefault();
const dropdown = document.getElementById('create-project-dropdown');
if (dropdown) {
dropdown.classList.toggle('hidden');
// Add click outside handler
const clickHandler = (evt: any) => {
if (!dropdown.contains(evt.target) &&
!(e.target as HTMLElement).contains(evt.target)) {
dropdown.classList.add('hidden');
document.removeEventListener('click', clickHandler);
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
document.addEventListener('click', clickHandler);
}, 0);
}
}}
>
<span className="flex items-center gap-2">
<Briefcase className="h-4 w-4" />
{formData.projectId ? getProjectName(formData.projectId) : 'Select Project'}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
{!formData.projectId && (
<p className="text-xs text-destructive mt-1">Project selection is required</p>
)}
<div
id="create-project-dropdown"
className="absolute left-0 right-0 top-full mt-1 z-50 bg-white shadow-lg border rounded-md p-1 max-h-[200px] overflow-y-auto hidden"
>
{projects.map((project) => (
<button
key={project.id}
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleProjectChange(project.id.toString());
document.getElementById('create-project-dropdown')?.classList.add('hidden');
}}
>
<span>{project.projectName}</span>
</button>
))}
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" onClick={handleCloseCreateDialog} disabled={submitLoading}>
Cancel
</Button>
<Button type="submit" disabled={submitLoading}>
{submitLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
'Create Communication'
)}
</Button>
</div>
</form>
</CustomDialog>
{/* Edit Communication Dialog */}
<CustomDialog
isOpen={isEditDialogOpen}
onClose={handleCloseEditDialog}
title="Edit Communication"
description="Update the communication record. Make changes to the form below."
isPending={submitLoading}
>
<form onSubmit={handleEditSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="topic">Topic</Label>
<Input
id="topic"
name="topic"
placeholder="Enter communication topic"
value={formData.topic || ''}
onChange={handleInputChange}
required
/>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="details">Details</Label>
<SlateRichTextEditor
initialValue={formData.details || ''}
onChange={(value) => {
setFormData(prev => ({
...prev,
details: value
}));
}}
placeholder="Enter communication details"
minHeight="150px"
/>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="decisionMade">Decision Made</Label>
<SlateRichTextEditor
initialValue={formData.decisionMade || ''}
onChange={(value) => {
setFormData(prev => ({
...prev,
decisionMade: value
}));
}}
placeholder="Enter decisions made during this communication"
minHeight="150px"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="mode">Mode</Label>
<div className="relative">
<Button
type="button"
variant="outline"
className="w-full justify-between"
onClick={(e) => {
e.preventDefault();
const dropdown = document.getElementById('edit-mode-dropdown');
if (dropdown) {
dropdown.classList.toggle('hidden');
// Add click outside handler
const clickHandler = (evt: any) => {
if (!dropdown.contains(evt.target) &&
!(e.target as HTMLElement).contains(evt.target)) {
dropdown.classList.add('hidden');
document.removeEventListener('click', clickHandler);
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
document.addEventListener('click', clickHandler);
}, 0);
}
}}
>
<span className="flex items-center gap-2">
{getModeIcon(formData.mode || 'In-person')}
{formData.mode || 'Select Mode'}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
<div
id="edit-mode-dropdown"
className="absolute left-0 right-0 top-full mt-1 z-50 bg-white shadow-lg border rounded-md p-1 hidden"
>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('In-person');
document.getElementById('edit-mode-dropdown')?.classList.add('hidden');
}}
>
<Building2 className="h-4 w-4 mr-2 text-primary" />
<span>In-person</span>
</button>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('Virtual');
document.getElementById('edit-mode-dropdown')?.classList.add('hidden');
}}
>
<Video className="h-4 w-4 mr-2 text-purple-500" />
<span>Virtual</span>
</button>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('Phone');
document.getElementById('edit-mode-dropdown')?.classList.add('hidden');
}}
>
<Phone className="h-4 w-4 mr-2 text-orange-500" />
<span>Phone</span>
</button>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleModeChange('Email');
document.getElementById('edit-mode-dropdown')?.classList.add('hidden');
}}
>
<MessageSquare className="h-4 w-4 mr-2 text-blue-500" />
<span>Email</span>
</button>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="communicationDate">Date</Label>
<Input
id="communicationDate"
name="communicationDate"
type="datetime-local"
value={formData.communicationDate ? new Date(formData.communicationDate).toISOString().slice(0, 16) : ''}
onChange={handleDateChange}
required
/>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="participants">Participants</Label>
<Input
id="participants"
name="participants"
placeholder="Enter participant names (comma separated)"
value={formData.participants || ''}
onChange={handleInputChange}
required
/>
</div>
<div className="grid grid-cols-1 gap-2">
<Label htmlFor="projectId">Project</Label>
<div className="relative">
<Button
type="button"
variant="outline"
className="w-full justify-between"
onClick={(e) => {
e.preventDefault();
const dropdown = document.getElementById('edit-project-dropdown');
if (dropdown) {
dropdown.classList.toggle('hidden');
// Add click outside handler
const clickHandler = (evt: any) => {
if (!dropdown.contains(evt.target) &&
!(e.target as HTMLElement).contains(evt.target)) {
dropdown.classList.add('hidden');
document.removeEventListener('click', clickHandler);
}
};
// Delay to avoid immediate triggering
setTimeout(() => {
document.addEventListener('click', clickHandler);
}, 0);
}
}}
>
<span className="flex items-center gap-2">
<Briefcase className="h-4 w-4" />
{formData.projectId ? getProjectName(formData.projectId) : 'Select Project'}
</span>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
<div
id="edit-project-dropdown"
className="absolute left-0 right-0 top-full mt-1 z-50 bg-white shadow-lg border rounded-md p-1 max-h-[200px] overflow-y-auto hidden"
>
<button
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleProjectChange('none');
document.getElementById('edit-project-dropdown')?.classList.add('hidden');
}}
>
<span>None</span>
</button>
{projects.map((project) => (
<button
key={project.id}
type="button"
className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-slate-100 text-slate-700"
onClick={(e) => {
e.preventDefault();
handleProjectChange(project.id.toString());
document.getElementById('edit-project-dropdown')?.classList.add('hidden');
}}
>
<span>{project.projectName}</span>
</button>
))}
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button type="button" variant="outline" onClick={handleCloseEditDialog} disabled={submitLoading}>
Cancel
</Button>
<Button type="submit" disabled={submitLoading}>
{submitLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : (
'Update Communication'
)}
</Button>
</div>
</form>
</CustomDialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={openDeleteDialog} onOpenChange={setOpenDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Communication</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this communication record?<br/>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCloseDeleteDialog}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</main>
</div>
);
};
export default Communications;