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 ;
case 'virtual':
return ;
case 'phone':
return ;
case 'email':
return ;
default:
return ;
}
};
// 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 (
);
};
const Communications: React.FC = () => {
// State management
const isMobile = useIsMobile();
const [isSidebarOpen, setIsSidebarOpen] = useState(!isMobile);
const [communications, setCommunications] = useState([]);
const [filteredCommunications, setFilteredCommunications] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [projects, setProjects] = useState([]);
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(null);
const [dateFilter, setDateFilter] = useState(null);
const [projectFilter, setProjectFilter] = useState(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(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>({
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) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleDateChange = (e: React.ChangeEvent) => {
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 (
{notification && (
{notification.type === 'success' ? 'Success' : notification.type === 'error' ? 'Error' : 'Info'}
{notification.message}
)}
{error && (
Error
{error}
)}
Communication Trail
Track and manage all communications within the organization
handleOpenCreateDialog()}>
New Communication
{/* Stats Cards */}
Total Communications
All recorded communications
In-Person Meetings
Face-to-face communications
Virtual Meetings
Online video conferences
Other Communications
Phone, email and other formats
{/* Search and Filters */}
setSearchQuery(e.target.value)}
/>
{/* Date filter - custom dropdown */}
{
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);
}
}}
>
{dateFilter ? format(dateFilter, 'MMM dd, yyyy') : "Filter by Date"}
{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 (
{
e.stopPropagation();
const newDate = new Date();
newDate.setDate(day);
setDateFilter(newDate);
document.getElementById('date-filter-dropdown')?.classList.add('hidden');
}}
>
{day}
);
})}
{dateFilter && (
{
e.stopPropagation();
setDateFilter(null);
document.getElementById('date-filter-dropdown')?.classList.add('hidden');
}}
>
Clear Date
)}
{/* Mode filter - custom dropdown */}
{
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);
}
}}
>
{modeFilter ? `Mode: ${modeFilter}` : "Filter by Mode"}
Communication Mode
{
setModeFilter('In-person');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
In-person
{
setModeFilter('Virtual');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
Virtual
{
setModeFilter('Phone');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
Phone
{
setModeFilter('Email');
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
Email
{modeFilter && (
{
setModeFilter(null);
document.getElementById('mode-filter-dropdown')?.classList.add('hidden');
}}
>
Clear Mode Filter
)}
{/* Project filter - custom dropdown */}
{projects.length > 0 && (
{
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);
}
}}
>
{projectFilter !== null
? `Project: ${getProjectName(projectFilter)}`
: "Filter by Project"}
Select Project
{projects.map((project) => (
{
setProjectFilter(project.id);
document.getElementById('project-filter-dropdown')?.classList.add('hidden');
}}
>
{project.projectName}
))}
{projectFilter !== null && (
{
setProjectFilter(null);
document.getElementById('project-filter-dropdown')?.classList.add('hidden');
}}
>
Clear Project Filter
)}
)}
{(searchQuery || modeFilter || dateFilter || projectFilter !== null) && (
Clear All
)}
{/* Active Filters */}
{(modeFilter || dateFilter || projectFilter !== null) && (
{modeFilter && (
{getModeIcon(modeFilter)}
Mode: {modeFilter}
setModeFilter(null)}
>
)}
{dateFilter && (
Date: {format(dateFilter, 'MMM dd, yyyy')}
setDateFilter(null)}
>
)}
{projectFilter !== null && (
Project: {getProjectName(projectFilter)}
setProjectFilter(null)}
>
)}
)}
{/* Communications Table/Cards */}
{loading ? (
) : error ? (
Error
{error}
) : (
<>
{/* Desktop View - Table */}
Topic
Date & Time
Mode
Participants
Project
Decision Made
Actions
{filteredCommunications.length === 0 ? (
No communications found
) : (
filteredCommunications.map((communication) => (
{communication.topic}
{formatDate(communication.communicationDate)}
{getModeIcon(communication.mode)}
{communication.mode}
{communication.participants.split(',').length > 2
? `${communication.participants.split(',').slice(0, 2).join(', ')} +${
communication.participants.split(',').length - 2
}`
: communication.participants}
{getProjectName(communication.projectId)}
{communication.decisionMade && (
)}
handleOpenEditDialog(communication)}
className="h-8 w-8 p-0"
>
Edit
handleOpenDeleteDialog(communication)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive/90 hover:bg-destructive/10"
>
Delete
))
)}
{/* Mobile View - Cards */}
{filteredCommunications.length === 0 ? (
No communications found
) : (
filteredCommunications.map((communication) => (
{communication.topic}
{
const id = `mobile-actions-${communication.communicationId}`;
const menu = document.getElementById(id);
if (menu) menu.classList.toggle('hidden');
}}
>
{
const id = `mobile-actions-${communication.communicationId}`;
const menu = document.getElementById(id);
if (menu) menu.classList.add('hidden');
handleOpenEditDialog(communication);
}}
>
Edit
{
const id = `mobile-actions-${communication.communicationId}`;
const menu = document.getElementById(id);
if (menu) menu.classList.add('hidden');
handleOpenDeleteDialog(communication);
}}
>
Delete
{getModeIcon(communication.mode)}
{communication.mode}
{formatDate(communication.communicationDate).split(',')[0]}
Participants:
{communication.participants}
Project:
{getProjectName(communication.projectId)}
{communication.decisionMade && (
)}
))
)}
>
)}
{/* Create Communication Dialog */}
{/* Edit Communication Dialog */}
{/* Delete Confirmation Dialog */}
Delete Communication
Are you sure you want to delete this communication record?
This action cannot be undone.
Cancel
Delete
);
};
export default Communications;