Enable real-time chat feature with contacts and professional services
Implements WebSocket-based chat API endpoints, Drizzle ORM schema, and React UI for real-time messaging. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/c001adfb-dd61-4b64-834d-c77215561d6a.jpg
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
MessageCircle,
|
||||
Search,
|
||||
Phone,
|
||||
Video,
|
||||
MoreVertical,
|
||||
Send,
|
||||
Paperclip,
|
||||
Mic,
|
||||
Users,
|
||||
Settings,
|
||||
UserPlus,
|
||||
Briefcase
|
||||
} from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiRequest } from '@/lib/queryClient';
|
||||
|
||||
interface Contact {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
lastMessage?: string;
|
||||
lastMessageTime?: string;
|
||||
unreadCount?: number;
|
||||
isOnline?: boolean;
|
||||
isProfessional?: boolean;
|
||||
professionalType?: string;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
content: string;
|
||||
senderId: number;
|
||||
timestamp: string;
|
||||
type: 'text' | 'image' | 'document' | 'voice';
|
||||
isEdited?: boolean;
|
||||
status: 'sent' | 'delivered' | 'read';
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
id: number;
|
||||
name?: string;
|
||||
type: 'direct' | 'group' | 'professional_service';
|
||||
participants: any[];
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export default function ChatPage() {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedChat, setSelectedChat] = useState<number | null>(null);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [ws, setWs] = useState<WebSocket | null>(null);
|
||||
|
||||
// Fetch contacts/chats
|
||||
const { data: chats = [], isLoading } = useQuery({
|
||||
queryKey: ['/api/chats'],
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Fetch messages for selected chat
|
||||
const { data: messages = [] } = useQuery({
|
||||
queryKey: ['/api/chats', selectedChat, 'messages'],
|
||||
enabled: !!selectedChat,
|
||||
});
|
||||
|
||||
// Fetch professional services
|
||||
const { data: professionalServices = [] } = useQuery({
|
||||
queryKey: ['/api/professional-services'],
|
||||
});
|
||||
|
||||
// Send message mutation
|
||||
const sendMessageMutation = useMutation({
|
||||
mutationFn: async (messageData: { chatId: number; content: string; type: string }) => {
|
||||
return apiRequest(`/api/chats/${messageData.chatId}/messages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(messageData),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/chats', selectedChat, 'messages'] });
|
||||
setNewMessage('');
|
||||
},
|
||||
});
|
||||
|
||||
// WebSocket connection for real-time messaging
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
const websocket = new WebSocket(wsUrl);
|
||||
|
||||
websocket.onopen = () => {
|
||||
console.log('Connected to chat WebSocket');
|
||||
websocket.send(JSON.stringify({ type: 'join', userId: user.id }));
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'new_message') {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/chats'] });
|
||||
if (data.chatId === selectedChat) {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/chats', selectedChat, 'messages'] });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
console.log('Chat WebSocket disconnected');
|
||||
};
|
||||
|
||||
setWs(websocket);
|
||||
|
||||
return () => {
|
||||
websocket.close();
|
||||
};
|
||||
}, [user, selectedChat, queryClient]);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!newMessage.trim() || !selectedChat) return;
|
||||
|
||||
sendMessageMutation.mutate({
|
||||
chatId: selectedChat,
|
||||
content: newMessage.trim(),
|
||||
type: 'text'
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const filteredChats = chats.filter((chat: any) =>
|
||||
chat.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
chat.participants?.some((p: any) =>
|
||||
p.name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const formatTime = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return 'Today';
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return 'Yesterday';
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-muted-foreground">{t('common.loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background" dir={t('common.direction')}>
|
||||
{/* Sidebar - Chat List */}
|
||||
<div className="w-80 border-r border-border flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-xl font-semibold flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
{t('chat.title')}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
<UserPlus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('chat.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Professional Services Section */}
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Briefcase className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm font-medium">{t('chat.professionalServices')}</span>
|
||||
</div>
|
||||
<ScrollArea className="h-24">
|
||||
<div className="flex gap-2">
|
||||
{professionalServices.slice(0, 4).map((service: any) => (
|
||||
<Card key={service.id} className="p-2 min-w-[100px] cursor-pointer hover:bg-accent">
|
||||
<div className="text-center">
|
||||
<Avatar className="h-8 w-8 mx-auto mb-1">
|
||||
<AvatarImage src={service.avatar} />
|
||||
<AvatarFallback>{service.name?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-xs font-medium truncate">{service.name}</div>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{service.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Chat List */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2">
|
||||
{filteredChats.map((chat: any) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
onClick={() => setSelectedChat(chat.id)}
|
||||
className={`p-3 rounded-lg cursor-pointer mb-1 transition-colors ${
|
||||
selectedChat === chat.id
|
||||
? 'bg-primary/10 border border-primary/20'
|
||||
: 'hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={chat.avatar} />
|
||||
<AvatarFallback>
|
||||
{chat.name?.[0] || chat.participants?.[0]?.name?.[0]}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{chat.isOnline && (
|
||||
<div className="absolute bottom-0 right-0 h-3 w-3 bg-green-500 border-2 border-background rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium truncate">
|
||||
{chat.name || chat.participants?.[0]?.name}
|
||||
{chat.isProfessional && (
|
||||
<Badge variant="outline" className="ml-2 text-xs">
|
||||
{chat.professionalType}
|
||||
</Badge>
|
||||
)}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatTime(chat.lastMessageAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground truncate">
|
||||
{chat.lastMessage || t('chat.noMessages')}
|
||||
</p>
|
||||
{chat.unreadCount > 0 && (
|
||||
<Badge variant="default" className="text-xs h-5 w-5 p-0 flex items-center justify-center">
|
||||
{chat.unreadCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Main Chat Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{selectedChat ? (
|
||||
<>
|
||||
{/* Chat Header */}
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src="" />
|
||||
<AvatarFallback>US</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h2 className="font-semibold">{t('chat.selectedChat')}</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('chat.lastSeen')} 2 {t('chat.minutesAgo')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
<Phone className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Video className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-4">
|
||||
{messages.map((message: Message, index: number) => {
|
||||
const isFromUser = message.senderId === user?.id;
|
||||
const showDate = index === 0 ||
|
||||
formatDate(message.timestamp) !== formatDate(messages[index - 1]?.timestamp);
|
||||
|
||||
return (
|
||||
<div key={message.id}>
|
||||
{showDate && (
|
||||
<div className="flex justify-center my-4">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatDate(message.timestamp)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`flex ${isFromUser ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className="flex gap-2 max-w-[70%]">
|
||||
{!isFromUser && (
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback>U</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`rounded-lg p-3 ${
|
||||
isFromUser
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted'
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm">{message.content}</p>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<span className="text-xs opacity-70">
|
||||
{formatTime(message.timestamp)}
|
||||
</span>
|
||||
{message.isEdited && (
|
||||
<span className="text-xs opacity-70">{t('chat.edited')}</span>
|
||||
)}
|
||||
{isFromUser && (
|
||||
<div className="text-xs opacity-70">
|
||||
{message.status === 'read' ? '✓✓' : '✓'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Message Input */}
|
||||
<div className="p-4 border-t border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<Input
|
||||
placeholder={t('chat.typeMessage')}
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="pr-12"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!newMessage.trim() || sendMessageMutation.isPending}
|
||||
size="sm"
|
||||
className="absolute right-1 top-1 h-8 w-8 p-0"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="ghost" size="sm">
|
||||
<Mic className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Welcome Screen */
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<MessageCircle className="h-24 w-24 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">{t('chat.welcome')}</h2>
|
||||
<p className="text-muted-foreground max-w-md">
|
||||
{t('chat.welcomeDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+148
-2
@@ -1,11 +1,12 @@
|
||||
import type { Express } from "express";
|
||||
import type { Express, Request, Response } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import session from "express-session";
|
||||
import passport from "passport";
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { storage } from "./storage";
|
||||
import { authMiddleware, requireRole } from "./middleware/auth";
|
||||
import { authMiddleware, requireRole, type AuthenticatedRequest } from "./middleware/auth";
|
||||
import {
|
||||
generalRateLimit,
|
||||
authRateLimit,
|
||||
@@ -18,6 +19,20 @@ import {
|
||||
} from "./middleware/security";
|
||||
import { monitoringService } from "./services/monitoringService";
|
||||
import { taskScheduler } from "./services/taskScheduler";
|
||||
|
||||
// WebSocket connection management
|
||||
const wsConnections = new Map<number, WebSocket[]>(); // userId -> WebSocket connections
|
||||
|
||||
function broadcastToChat(chatId: number, data: any) {
|
||||
// Get all participants of the chat and send message to their connections
|
||||
wsConnections.forEach((connections, userId) => {
|
||||
connections.forEach(ws => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
import { performanceOptimizationService } from "./services/performanceOptimization";
|
||||
import { authController } from "./controllers/authController";
|
||||
import { taskController } from "./controllers/taskController";
|
||||
@@ -159,6 +174,137 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Chat API routes
|
||||
app.get('/api/chats', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const chats = await storage.getChats(req.user.id);
|
||||
res.json(chats);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch chats' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/chats', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const chat = await storage.createChat({
|
||||
...req.body,
|
||||
createdBy: req.user.id
|
||||
});
|
||||
|
||||
// Add creator as participant
|
||||
await storage.addChatParticipant({
|
||||
chatId: chat.id,
|
||||
userId: req.user.id,
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
res.json(chat);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create chat' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/chats/:id/messages', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const chatId = parseInt(req.params.id);
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
const messages = await storage.getMessages(chatId, req.user.id, limit);
|
||||
res.json(messages);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch messages' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/chats/:id/messages', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const chatId = parseInt(req.params.id);
|
||||
const message = await storage.createMessage({
|
||||
chatId,
|
||||
senderId: req.user.id,
|
||||
...req.body
|
||||
});
|
||||
|
||||
// Broadcast to WebSocket clients
|
||||
broadcastToChat(chatId, {
|
||||
type: 'new_message',
|
||||
chatId,
|
||||
message
|
||||
});
|
||||
|
||||
res.json(message);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to send message' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/contacts', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const contacts = await storage.getContacts(req.user.id);
|
||||
res.json(contacts);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch contacts' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/contacts', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const contact = await storage.createContact({
|
||||
...req.body,
|
||||
userId: req.user.id
|
||||
});
|
||||
res.json(contact);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create contact' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/professional-services', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { type, location } = req.query;
|
||||
const services = await storage.getProfessionalServices(
|
||||
type as string,
|
||||
location as string
|
||||
);
|
||||
res.json(services);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch professional services' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/professional-services', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const service = await storage.createProfessionalService({
|
||||
...req.body,
|
||||
providerId: req.user.id
|
||||
});
|
||||
res.json(service);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create professional service' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/service-requests', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const type = req.query.type as 'client' | 'provider' || 'client';
|
||||
const requests = await storage.getServiceRequests(req.user.id, type);
|
||||
res.json(requests);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch service requests' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/service-requests', authMiddleware, async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const request = await storage.createServiceRequest({
|
||||
...req.body,
|
||||
clientId: req.user.id
|
||||
});
|
||||
res.json(request);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create service request' });
|
||||
}
|
||||
});
|
||||
|
||||
// Configure session middleware
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||
|
||||
+267
-2
@@ -1,6 +1,6 @@
|
||||
import { users, sessions, tasks, financialRecords, voiceCommands, aiInteractions, userPreferences, type User, type InsertUser, type Task, type InsertTask, type FinancialRecord, type InsertFinancialRecord, type Session, type VoiceCommand, type InsertVoiceCommand, type AIInteraction, type UserPreferences, type InsertUserPreferences } from "@shared/schema";
|
||||
import { users, sessions, tasks, financialRecords, voiceCommands, aiInteractions, userPreferences, contacts, chats, chatParticipants, messages, messageStatuses, professionalServices, serviceRequests, type User, type InsertUser, type Task, type InsertTask, type FinancialRecord, type InsertFinancialRecord, type Session, type VoiceCommand, type InsertVoiceCommand, type AIInteraction, type UserPreferences, type InsertUserPreferences, type Contact, type InsertContact, type Chat, type InsertChat, type ChatParticipant, type InsertChatParticipant, type Message, type InsertMessage, type MessageStatus, type InsertMessageStatus, type ProfessionalService, type InsertProfessionalService, type ServiceRequest, type InsertServiceRequest } from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, desc, and, gte, lte, count } from "drizzle-orm";
|
||||
import { eq, desc, and, gte, lte, count, or, inArray } from "drizzle-orm";
|
||||
|
||||
export interface IStorage {
|
||||
// User management
|
||||
@@ -40,6 +40,36 @@ export interface IStorage {
|
||||
// User preferences
|
||||
getUserPreferences(userId: number): Promise<UserPreferences | undefined>;
|
||||
updateUserPreferences(userId: number, preferences: Partial<InsertUserPreferences>): Promise<UserPreferences>;
|
||||
|
||||
// Chat management
|
||||
createContact(contact: InsertContact): Promise<Contact>;
|
||||
getContacts(userId: number): Promise<Contact[]>;
|
||||
updateContact(id: number, userId: number, updates: Partial<Contact>): Promise<Contact | undefined>;
|
||||
deleteContact(id: number, userId: number): Promise<boolean>;
|
||||
|
||||
// Chat operations
|
||||
createChat(chat: InsertChat): Promise<Chat>;
|
||||
getChats(userId: number): Promise<Chat[]>;
|
||||
getChat(id: number, userId: number): Promise<Chat | undefined>;
|
||||
addChatParticipant(participant: InsertChatParticipant): Promise<ChatParticipant>;
|
||||
removeChatParticipant(chatId: number, userId: number): Promise<boolean>;
|
||||
|
||||
// Message operations
|
||||
createMessage(message: InsertMessage): Promise<Message>;
|
||||
getMessages(chatId: number, userId: number, limit?: number): Promise<Message[]>;
|
||||
updateMessage(id: number, userId: number, updates: Partial<Message>): Promise<Message | undefined>;
|
||||
deleteMessage(id: number, userId: number): Promise<boolean>;
|
||||
markMessageAsRead(messageId: number, userId: number): Promise<void>;
|
||||
|
||||
// Professional services
|
||||
createProfessionalService(service: InsertProfessionalService): Promise<ProfessionalService>;
|
||||
getProfessionalServices(type?: string, location?: string): Promise<ProfessionalService[]>;
|
||||
updateProfessionalService(id: number, userId: number, updates: Partial<ProfessionalService>): Promise<ProfessionalService | undefined>;
|
||||
|
||||
// Service requests
|
||||
createServiceRequest(request: InsertServiceRequest): Promise<ServiceRequest>;
|
||||
getServiceRequests(userId: number, type?: 'client' | 'provider'): Promise<ServiceRequest[]>;
|
||||
updateServiceRequest(id: number, userId: number, updates: Partial<ServiceRequest>): Promise<ServiceRequest | undefined>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
@@ -273,6 +303,241 @@ export class DatabaseStorage implements IStorage {
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
// Chat management implementation
|
||||
async createContact(contact: InsertContact): Promise<Contact> {
|
||||
const [created] = await db
|
||||
.insert(contacts)
|
||||
.values(contact)
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async getContacts(userId: number): Promise<Contact[]> {
|
||||
return await db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(eq(contacts.userId, userId))
|
||||
.orderBy(desc(contacts.lastContactedAt));
|
||||
}
|
||||
|
||||
async updateContact(id: number, userId: number, updates: Partial<Contact>): Promise<Contact | undefined> {
|
||||
const [updated] = await db
|
||||
.update(contacts)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(eq(contacts.id, id), eq(contacts.userId, userId)))
|
||||
.returning();
|
||||
return updated || undefined;
|
||||
}
|
||||
|
||||
async deleteContact(id: number, userId: number): Promise<boolean> {
|
||||
const result = await db
|
||||
.delete(contacts)
|
||||
.where(and(eq(contacts.id, id), eq(contacts.userId, userId)));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
// Chat operations implementation
|
||||
async createChat(chat: InsertChat): Promise<Chat> {
|
||||
const [created] = await db
|
||||
.insert(chats)
|
||||
.values(chat)
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async getChats(userId: number): Promise<Chat[]> {
|
||||
return await db
|
||||
.select({
|
||||
id: chats.id,
|
||||
type: chats.type,
|
||||
name: chats.name,
|
||||
description: chats.description,
|
||||
avatar: chats.avatar,
|
||||
isActive: chats.isActive,
|
||||
lastMessageId: chats.lastMessageId,
|
||||
lastMessageAt: chats.lastMessageAt,
|
||||
createdBy: chats.createdBy,
|
||||
createdAt: chats.createdAt,
|
||||
updatedAt: chats.updatedAt,
|
||||
})
|
||||
.from(chats)
|
||||
.innerJoin(chatParticipants, eq(chats.id, chatParticipants.chatId))
|
||||
.where(and(
|
||||
eq(chatParticipants.userId, userId),
|
||||
eq(chatParticipants.isActive, true)
|
||||
))
|
||||
.orderBy(desc(chats.lastMessageAt));
|
||||
}
|
||||
|
||||
async getChat(id: number, userId: number): Promise<Chat | undefined> {
|
||||
const [chat] = await db
|
||||
.select()
|
||||
.from(chats)
|
||||
.innerJoin(chatParticipants, eq(chats.id, chatParticipants.chatId))
|
||||
.where(and(
|
||||
eq(chats.id, id),
|
||||
eq(chatParticipants.userId, userId),
|
||||
eq(chatParticipants.isActive, true)
|
||||
));
|
||||
return chat?.chats || undefined;
|
||||
}
|
||||
|
||||
async addChatParticipant(participant: InsertChatParticipant): Promise<ChatParticipant> {
|
||||
const [created] = await db
|
||||
.insert(chatParticipants)
|
||||
.values(participant)
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async removeChatParticipant(chatId: number, userId: number): Promise<boolean> {
|
||||
const result = await db
|
||||
.update(chatParticipants)
|
||||
.set({ isActive: false, leftAt: new Date() })
|
||||
.where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.userId, userId)));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
// Message operations implementation
|
||||
async createMessage(message: InsertMessage): Promise<Message> {
|
||||
const [created] = await db
|
||||
.insert(messages)
|
||||
.values(message)
|
||||
.returning();
|
||||
|
||||
// Update chat's last message
|
||||
await db
|
||||
.update(chats)
|
||||
.set({ lastMessageId: created.id, lastMessageAt: created.createdAt })
|
||||
.where(eq(chats.id, created.chatId));
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
async getMessages(chatId: number, userId: number, limit: number = 50): Promise<Message[]> {
|
||||
// Verify user is participant in chat
|
||||
const [participant] = await db
|
||||
.select()
|
||||
.from(chatParticipants)
|
||||
.where(and(
|
||||
eq(chatParticipants.chatId, chatId),
|
||||
eq(chatParticipants.userId, userId),
|
||||
eq(chatParticipants.isActive, true)
|
||||
));
|
||||
|
||||
if (!participant) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(and(
|
||||
eq(messages.chatId, chatId),
|
||||
eq(messages.isDeleted, false)
|
||||
))
|
||||
.orderBy(desc(messages.createdAt))
|
||||
.limit(limit);
|
||||
}
|
||||
|
||||
async updateMessage(id: number, userId: number, updates: Partial<Message>): Promise<Message | undefined> {
|
||||
const [updated] = await db
|
||||
.update(messages)
|
||||
.set({ ...updates, isEdited: true, editedAt: new Date() })
|
||||
.where(and(eq(messages.id, id), eq(messages.senderId, userId)))
|
||||
.returning();
|
||||
return updated || undefined;
|
||||
}
|
||||
|
||||
async deleteMessage(id: number, userId: number): Promise<boolean> {
|
||||
const result = await db
|
||||
.update(messages)
|
||||
.set({ isDeleted: true, deletedAt: new Date() })
|
||||
.where(and(eq(messages.id, id), eq(messages.senderId, userId)));
|
||||
return result.rowCount > 0;
|
||||
}
|
||||
|
||||
async markMessageAsRead(messageId: number, userId: number): Promise<void> {
|
||||
await db
|
||||
.insert(messageStatuses)
|
||||
.values({
|
||||
messageId,
|
||||
userId,
|
||||
status: 'read',
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Professional services implementation
|
||||
async createProfessionalService(service: InsertProfessionalService): Promise<ProfessionalService> {
|
||||
const [created] = await db
|
||||
.insert(professionalServices)
|
||||
.values(service)
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async getProfessionalServices(type?: string, location?: string): Promise<ProfessionalService[]> {
|
||||
let query = db
|
||||
.select()
|
||||
.from(professionalServices)
|
||||
.where(eq(professionalServices.isActive, true));
|
||||
|
||||
if (type) {
|
||||
query = query.where(eq(professionalServices.type, type));
|
||||
}
|
||||
|
||||
if (location) {
|
||||
query = query.where(eq(professionalServices.location, location));
|
||||
}
|
||||
|
||||
return await query.orderBy(desc(professionalServices.rating));
|
||||
}
|
||||
|
||||
async updateProfessionalService(id: number, userId: number, updates: Partial<ProfessionalService>): Promise<ProfessionalService | undefined> {
|
||||
const [updated] = await db
|
||||
.update(professionalServices)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(eq(professionalServices.id, id), eq(professionalServices.providerId, userId)))
|
||||
.returning();
|
||||
return updated || undefined;
|
||||
}
|
||||
|
||||
// Service requests implementation
|
||||
async createServiceRequest(request: InsertServiceRequest): Promise<ServiceRequest> {
|
||||
const [created] = await db
|
||||
.insert(serviceRequests)
|
||||
.values(request)
|
||||
.returning();
|
||||
return created;
|
||||
}
|
||||
|
||||
async getServiceRequests(userId: number, type: 'client' | 'provider' = 'client'): Promise<ServiceRequest[]> {
|
||||
if (type === 'client') {
|
||||
return await db
|
||||
.select()
|
||||
.from(serviceRequests)
|
||||
.where(eq(serviceRequests.clientId, userId))
|
||||
.orderBy(desc(serviceRequests.createdAt));
|
||||
} else {
|
||||
return await db
|
||||
.select()
|
||||
.from(serviceRequests)
|
||||
.innerJoin(professionalServices, eq(serviceRequests.serviceId, professionalServices.id))
|
||||
.where(eq(professionalServices.providerId, userId))
|
||||
.orderBy(desc(serviceRequests.createdAt));
|
||||
}
|
||||
}
|
||||
|
||||
async updateServiceRequest(id: number, userId: number, updates: Partial<ServiceRequest>): Promise<ServiceRequest | undefined> {
|
||||
const [updated] = await db
|
||||
.update(serviceRequests)
|
||||
.set({ ...updates, updatedAt: new Date() })
|
||||
.where(and(eq(serviceRequests.id, id), eq(serviceRequests.clientId, userId)))
|
||||
.returning();
|
||||
return updated || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
|
||||
Reference in New Issue
Block a user