Files
karibeo_backend_admin/src/pages/dashboard/Messages.tsx
2025-10-11 16:44:26 +00:00

400 lines
16 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useChat } from '@/hooks/useChat';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from 'sonner';
import {
MessageCircle,
Car,
UtensilsCrossed,
Hotel as HotelIcon,
Shield,
MapPin,
Send,
Search,
Plus,
Phone,
Video,
Paperclip,
Smile,
Circle,
HeadphonesIcon
} from 'lucide-react';
type ServiceType = 'all' | 'taxi' | 'restaurant' | 'hotel' | 'guide' | 'politur' | 'support';
const serviceIcons = {
taxi: Car,
restaurant: UtensilsCrossed,
hotel: HotelIcon,
guide: MapPin,
politur: Shield,
support: HeadphonesIcon
};
const serviceColors = {
taxi: 'bg-yellow-500/10 text-yellow-700 border-yellow-200',
restaurant: 'bg-orange-500/10 text-orange-700 border-orange-200',
hotel: 'bg-blue-500/10 text-blue-700 border-blue-200',
guide: 'bg-green-500/10 text-green-700 border-green-200',
politur: 'bg-red-500/10 text-red-700 border-red-200',
support: 'bg-purple-500/10 text-purple-700 border-purple-200'
};
const Messages = () => {
const { user } = useAuth();
const [selectedChatId, setSelectedChatId] = useState<string | null>(null);
const [messageText, setMessageText] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [serviceFilter, setServiceFilter] = useState<ServiceType>('all');
const messagesEndRef = useRef<HTMLDivElement>(null);
const {
chats,
messages,
onlineUsers,
loading,
error,
loadMessages,
sendMessage,
searchUsers,
createChat,
getChatById,
clearError
} = useChat();
const selectedChat = selectedChatId ? getChatById(selectedChatId) : null;
// Filter chats by service type
const filteredChats = chats.filter(chat => {
const matchesService = serviceFilter === 'all' || chat.serviceType === serviceFilter;
const matchesSearch = !searchQuery ||
chat.name.toLowerCase().includes(searchQuery.toLowerCase());
return matchesService && matchesSearch;
});
// Auto-scroll to bottom of messages
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// Load messages when chat is selected
useEffect(() => {
if (selectedChatId) {
loadMessages(selectedChatId);
}
}, [selectedChatId, loadMessages]);
// Auto-select first chat
useEffect(() => {
if (chats.length > 0 && !selectedChatId) {
setSelectedChatId(chats[0].id);
}
}, [chats, selectedChatId]);
// Handle send message
const handleSendMessage = async () => {
if (!messageText.trim() || !selectedChatId) return;
try {
await sendMessage(selectedChatId, messageText);
setMessageText('');
} catch (err) {
toast.error('No se pudo enviar el mensaje');
}
};
// Format time
const formatTime = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (diffInHours < 168) {
return date.toLocaleDateString([], { weekday: 'short' });
} else {
return date.toLocaleDateString();
}
};
const getServiceIcon = (serviceType?: string) => {
if (!serviceType) return MessageCircle;
return serviceIcons[serviceType as keyof typeof serviceIcons] || MessageCircle;
};
const getServiceBadgeClass = (serviceType?: string) => {
if (!serviceType) return 'bg-muted';
return serviceColors[serviceType as keyof typeof serviceColors] || 'bg-muted';
};
return (
<div className="flex h-screen bg-background overflow-hidden">
<div className="chat-container m-0 overflow-hidden relative rounded-lg flex w-full">
{/* Chat List Sidebar */}
<div className="chat-list__sidebar p-0 w-80 bg-background border-r border-border flex flex-col">
{/* Header */}
<div className="p-4 border-b border-border">
<h2 className="text-xl font-bold mb-2">Mensajes</h2>
<p className="text-sm text-muted-foreground">Comunícate con servicios turísticos</p>
</div>
{/* Search */}
<div className="p-4 border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="search"
className="pl-9"
placeholder="Buscar conversaciones..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
{/* Service Filter Tabs */}
<div className="px-4 py-3 border-b border-border">
<Tabs value={serviceFilter} onValueChange={(v) => setServiceFilter(v as ServiceType)}>
<TabsList className="w-full grid grid-cols-4 h-auto gap-1">
<TabsTrigger value="all" className="text-xs py-1">
Todos
</TabsTrigger>
<TabsTrigger value="taxi" className="text-xs py-1">
<Car className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="restaurant" className="text-xs py-1">
<UtensilsCrossed className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="hotel" className="text-xs py-1">
<HotelIcon className="h-3 w-3" />
</TabsTrigger>
</TabsList>
<TabsList className="w-full grid grid-cols-3 h-auto gap-1 mt-1">
<TabsTrigger value="guide" className="text-xs py-1">
<MapPin className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="politur" className="text-xs py-1">
<Shield className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="support" className="text-xs py-1">
<HeadphonesIcon className="h-3 w-3" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Chat List */}
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
{loading && (
<div className="p-4 text-center text-sm text-muted-foreground">
Cargando conversaciones...
</div>
)}
{!loading && filteredChats.length === 0 && (
<div className="p-4 text-center text-sm text-muted-foreground">
No hay conversaciones
</div>
)}
{filteredChats.map((chat) => {
const ServiceIcon = getServiceIcon(chat.serviceType);
return (
<div
key={chat.id}
className={`flex items-start p-3 rounded-lg cursor-pointer transition-colors ${
selectedChatId === chat.id
? 'bg-primary/10 border border-primary/20'
: 'hover:bg-accent'
}`}
onClick={() => setSelectedChatId(chat.id)}
>
<div className="relative mr-3">
<Avatar className="h-12 w-12">
<AvatarImage src={chat.avatar} />
<AvatarFallback>
<ServiceIcon className="h-6 w-6" />
</AvatarFallback>
</Avatar>
{chat.online && (
<div className="absolute bottom-0 right-0 h-3 w-3 bg-green-500 rounded-full border-2 border-background" />
)}
{chat.unreadCount > 0 && (
<div className="absolute -top-1 -left-1 h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
{chat.unreadCount}
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h5 className="font-semibold text-sm truncate">{chat.name}</h5>
<span className="text-xs text-muted-foreground">
{chat.lastMessage ? formatTime(chat.lastMessage.timestamp) : ''}
</span>
</div>
{chat.serviceType && (
<Badge variant="outline" className={`mb-1 text-xs ${getServiceBadgeClass(chat.serviceType)}`}>
<ServiceIcon className="h-3 w-3 mr-1" />
{chat.serviceType.charAt(0).toUpperCase() + chat.serviceType.slice(1)}
</Badge>
)}
<p className="text-xs text-muted-foreground truncate">
{chat.lastMessage?.content || 'No hay mensajes'}
</p>
</div>
</div>
);
})}
</div>
</ScrollArea>
</div>
{/* Chat Area */}
<div className="flex-1 flex flex-col">
{selectedChat ? (
<>
{/* Chat Header */}
<div className="p-4 border-b border-border flex items-center justify-between bg-card">
<div className="flex items-center">
<Avatar className="h-10 w-10 mr-3">
<AvatarImage src={selectedChat.avatar} />
<AvatarFallback>
{React.createElement(getServiceIcon(selectedChat.serviceType), { className: 'h-5 w-5' })}
</AvatarFallback>
</Avatar>
<div>
<h2 className="font-semibold flex items-center gap-2">
{selectedChat.name}
{selectedChat.serviceType && (
<Badge variant="outline" className={`text-xs ${getServiceBadgeClass(selectedChat.serviceType)}`}>
{selectedChat.serviceType}
</Badge>
)}
</h2>
<div className="flex items-center text-sm text-muted-foreground">
<Circle className={`h-2 w-2 mr-1 ${selectedChat.online ? 'fill-green-500 text-green-500' : 'fill-gray-400 text-gray-400'}`} />
{selectedChat.online ? 'En línea' : 'Desconectado'}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" title="Llamar">
<Phone className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" title="Video llamada">
<Video className="h-4 w-4" />
</Button>
</div>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-4">
<div className="space-y-4">
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-8">
<div className="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center mb-4">
<MessageCircle className="h-12 w-12 text-primary" />
</div>
<h3 className="text-lg font-medium mb-2">Inicia la conversación</h3>
<p className="text-muted-foreground text-sm">
Envía el primer mensaje a {selectedChat.name}
</p>
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`flex items-start ${message.isOwn ? 'justify-end' : 'justify-start'}`}
>
{!message.isOwn && (
<Avatar className="h-8 w-8 mr-2">
<AvatarImage src={message.senderAvatar} />
<AvatarFallback>{message.senderName?.charAt(0)}</AvatarFallback>
</Avatar>
)}
<div className={`flex flex-col ${message.isOwn ? 'items-end' : 'items-start'} max-w-md`}>
{!message.isOwn && (
<span className="text-xs text-muted-foreground mb-1">{message.senderName}</span>
)}
<div
className={`px-4 py-2 rounded-lg ${
message.isOwn
? 'bg-primary text-primary-foreground'
: 'bg-muted'
}`}
>
<p className="text-sm">{message.content}</p>
</div>
<span className="text-xs text-muted-foreground mt-1">{message.timestamp}</span>
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
</ScrollArea>
{/* Message Input */}
<div className="p-4 border-t border-border bg-card">
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" title="Adjuntar archivo">
<Paperclip className="h-4 w-4" />
</Button>
<div className="flex-1">
<Input
placeholder="Escribe un mensaje..."
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
/>
</div>
<Button size="sm" variant="outline" title="Emoji">
<Smile className="h-4 w-4" />
</Button>
<Button
size="sm"
onClick={handleSendMessage}
disabled={!messageText.trim() || loading}
>
<Send className="h-4 w-4" />
</Button>
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="w-32 h-32 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
<MessageCircle className="h-16 w-16 text-primary" />
</div>
<h3 className="text-lg font-medium mb-2">Selecciona una conversación</h3>
<p className="text-muted-foreground text-sm max-w-sm">
Elige un servicio para comunicarte: taxis, restaurantes, hoteles, guías turísticos o POLITUR
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default Messages;