362 lines
14 KiB
TypeScript
362 lines
14 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
|
import { Badge } from '../ui/badge';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
|
import { ScrollArea } from '../ui/scroll-area';
|
|
import {
|
|
Send,
|
|
Smile,
|
|
Paperclip,
|
|
Users,
|
|
FileText,
|
|
Download,
|
|
Eye,
|
|
MoreHorizontal
|
|
} from 'lucide-react';
|
|
|
|
interface Message {
|
|
id: string;
|
|
user: {
|
|
name: string;
|
|
avatar: string;
|
|
role: string;
|
|
};
|
|
content: string;
|
|
timestamp: string;
|
|
mentions?: string[];
|
|
isSystem?: boolean;
|
|
}
|
|
|
|
interface WorkNoteModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
requestId: string;
|
|
}
|
|
|
|
export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps) {
|
|
const [message, setMessage] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
const participants = [
|
|
{ name: 'Sarah Chen', avatar: 'SC', role: 'Initiator', status: 'online' },
|
|
{ name: 'Mike Johnson', avatar: 'MJ', role: 'Team Lead', status: 'online' },
|
|
{ name: 'Lisa Wong', avatar: 'LW', role: 'Finance Manager', status: 'away' },
|
|
{ name: 'Anna Smith', avatar: 'AS', role: 'Spectator', status: 'offline' },
|
|
{ name: 'John Doe', avatar: 'JD', role: 'Spectator', status: 'online' }
|
|
];
|
|
|
|
const documents = [
|
|
{
|
|
name: 'Q4_Marketing_Strategy.pdf',
|
|
size: '2.4 MB',
|
|
uploadedBy: 'Sarah Chen',
|
|
uploadedAt: '2024-10-05 14:32'
|
|
},
|
|
{
|
|
name: 'Budget_Breakdown.xlsx',
|
|
size: '1.1 MB',
|
|
uploadedBy: 'Sarah Chen',
|
|
uploadedAt: '2024-10-05 14:35'
|
|
},
|
|
{
|
|
name: 'Previous_Campaign_ROI.pdf',
|
|
size: '856 KB',
|
|
uploadedBy: 'Anna Smith',
|
|
uploadedAt: '2024-10-06 09:15'
|
|
}
|
|
];
|
|
|
|
const [messages, setMessages] = useState<Message[]>([
|
|
{
|
|
id: '1',
|
|
user: { name: 'Sarah Chen', avatar: 'SC', role: 'Initiator' },
|
|
content: 'Hi everyone! I\'ve submitted the marketing campaign budget request. Please review the attached documents.',
|
|
timestamp: '2024-10-05 14:30',
|
|
isSystem: false
|
|
},
|
|
{
|
|
id: '2',
|
|
user: { name: 'System', avatar: 'SY', role: 'System' },
|
|
content: 'Request RE-REQ-001 has been created and assigned to Mike Johnson for initial review.',
|
|
timestamp: '2024-10-05 14:31',
|
|
isSystem: true
|
|
},
|
|
{
|
|
id: '3',
|
|
user: { name: 'Anna Smith', avatar: 'AS', role: 'Spectator' },
|
|
content: 'I\'ve added the previous campaign ROI data to help with the decision. @Mike Johnson @Lisa Wong please check it out.',
|
|
timestamp: '2024-10-06 09:15',
|
|
mentions: ['Mike Johnson', 'Lisa Wong']
|
|
},
|
|
{
|
|
id: '4',
|
|
user: { name: 'Mike Johnson', avatar: 'MJ', role: 'Team Lead' },
|
|
content: 'Thanks @Anna Smith! The numbers look good. I\'m approving this and forwarding to Finance.',
|
|
timestamp: '2024-10-06 10:30',
|
|
mentions: ['Anna Smith']
|
|
},
|
|
{
|
|
id: '5',
|
|
user: { name: 'System', avatar: 'SY', role: 'System' },
|
|
content: 'Request approved by Mike Johnson and forwarded to Lisa Wong for finance review.',
|
|
timestamp: '2024-10-06 10:31',
|
|
isSystem: true
|
|
},
|
|
{
|
|
id: '6',
|
|
user: { name: 'Lisa Wong', avatar: 'LW', role: 'Finance Manager' },
|
|
content: 'I\'m reviewing the budget allocation. @Sarah Chen can you clarify the expected timeline for the LinkedIn ads campaign?',
|
|
timestamp: '2024-10-07 14:20',
|
|
mentions: ['Sarah Chen']
|
|
}
|
|
]);
|
|
|
|
const scrollToBottom = () => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
};
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [messages]);
|
|
|
|
const handleSendMessage = () => {
|
|
if (message.trim()) {
|
|
const newMessage: Message = {
|
|
id: Date.now().toString(),
|
|
user: { name: 'You', avatar: 'YO', role: 'Current User' },
|
|
content: message,
|
|
timestamp: new Date().toLocaleString(),
|
|
mentions: extractMentions(message)
|
|
};
|
|
setMessages([...messages, newMessage]);
|
|
setMessage('');
|
|
}
|
|
};
|
|
|
|
const extractMentions = (text: string): string[] => {
|
|
const mentionRegex = /@(\w+\s?\w+)/g;
|
|
const mentions = [];
|
|
let match;
|
|
while ((match = mentionRegex.exec(text)) !== null) {
|
|
mentions.push(match[1]);
|
|
}
|
|
return mentions;
|
|
};
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSendMessage();
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'online': return 'bg-green-500';
|
|
case 'away': return 'bg-yellow-500';
|
|
case 'offline': return 'bg-gray-400';
|
|
default: return 'bg-gray-400';
|
|
}
|
|
};
|
|
|
|
const formatMessage = (content: string) => {
|
|
// Simple mention highlighting
|
|
return content.replace(/@(\w+\s?\w+)/g, '<span class="text-blue-600 font-medium">@$1</span>');
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
|
|
<DialogHeader>
|
|
<DialogTitle>Work Notes - {requestId}</DialogTitle>
|
|
<DialogDescription>
|
|
Collaborate with all request participants
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 flex gap-4">
|
|
{/* Main Chat Area */}
|
|
<div className="flex-1 flex flex-col">
|
|
<Tabs defaultValue="chat" className="flex-1 flex flex-col">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="chat">Chat</TabsTrigger>
|
|
<TabsTrigger value="media">Media</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="chat" className="flex-1 flex flex-col">
|
|
<ScrollArea className="flex-1 p-4 border rounded-lg">
|
|
<div className="space-y-4">
|
|
{messages.map((msg) => (
|
|
<div key={msg.id} className={`flex gap-3 ${msg.isSystem ? 'justify-center' : ''}`}>
|
|
{!msg.isSystem && (
|
|
<Avatar className="h-8 w-8 flex-shrink-0">
|
|
<AvatarFallback className={`text-white text-xs ${
|
|
msg.user.role === 'Initiator' ? 'bg-re-green' :
|
|
msg.user.role === 'Current User' ? 'bg-blue-500' :
|
|
'bg-re-light-green'
|
|
}`}>
|
|
{msg.user.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
)}
|
|
|
|
<div className={`flex-1 ${msg.isSystem ? 'text-center' : ''}`}>
|
|
{msg.isSystem ? (
|
|
<div className="inline-flex items-center gap-2 px-3 py-1 bg-muted rounded-full text-sm text-muted-foreground">
|
|
{msg.content}
|
|
<span className="text-xs">{msg.timestamp}</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-medium text-sm">{msg.user.name}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{msg.user.role}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground">
|
|
{msg.timestamp}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className="text-sm bg-muted/30 p-3 rounded-lg"
|
|
dangerouslySetInnerHTML={{ __html: formatMessage(msg.content) }}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{isTyping && (
|
|
<div className="flex gap-3">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback className="bg-gray-400 text-white text-xs">
|
|
...
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<div className="flex gap-1">
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"></div>
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
|
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
</div>
|
|
Someone is typing...
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Message Input */}
|
|
<div className="mt-4 flex gap-2">
|
|
<div className="flex-1 relative">
|
|
<Input
|
|
placeholder="Type your message... Use @username to mention someone"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
onKeyPress={handleKeyPress}
|
|
className="pr-20"
|
|
/>
|
|
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex gap-1">
|
|
<Button variant="ghost" size="sm">
|
|
<Smile className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm">
|
|
<Paperclip className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<Button onClick={handleSendMessage} disabled={!message.trim()}>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="media" className="flex-1">
|
|
<div className="p-4 border rounded-lg h-full">
|
|
<h4 className="font-medium mb-4">Documents ({documents.length})</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{documents.map((doc, index) => (
|
|
<div key={index} className="border rounded-lg p-4 hover:bg-muted/30 transition-colors">
|
|
<div className="flex items-start gap-3">
|
|
<FileText className="h-8 w-8 text-muted-foreground flex-shrink-0 mt-1" />
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium truncate">{doc.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{doc.size} • by {doc.uploadedBy}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{doc.uploadedAt}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
<Button variant="ghost" size="sm">
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm">
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
|
|
{/* Participants Sidebar */}
|
|
<div className="w-72 border-l pl-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h4 className="font-medium">Participants</h4>
|
|
<Badge variant="outline">{participants.length}</Badge>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{participants.map((participant, index) => (
|
|
<div key={index} className="flex items-center gap-3">
|
|
<div className="relative">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarFallback className={`text-white text-xs ${
|
|
participant.role === 'Initiator' ? 'bg-re-green' : 'bg-re-light-green'
|
|
}`}>
|
|
{participant.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className={`absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-background ${getStatusColor(participant.status)}`}></div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-sm truncate">{participant.name}</p>
|
|
<p className="text-xs text-muted-foreground">{participant.role}</p>
|
|
</div>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<h4 className="font-medium mb-3">Quick Actions</h4>
|
|
<div className="space-y-2">
|
|
<Button variant="outline" size="sm" className="w-full justify-start">
|
|
<Users className="h-4 w-4 mr-2" />
|
|
Add Participant
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="w-full justify-start">
|
|
<Paperclip className="h-4 w-4 mr-2" />
|
|
Upload File
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |