wired the api for rest mock data
This commit is contained in:
@@ -40,7 +40,14 @@ import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||
|
||||
import Logo from '@/components/Logo';
|
||||
import { getStaff, getHubContext, clearSession } from '@/auth/session';
|
||||
import { getNotifications, markNotificationRead } from '@/api/hub';
|
||||
import {
|
||||
getNotifications,
|
||||
markNotificationRead,
|
||||
getConversations,
|
||||
getConversation,
|
||||
sendMessage,
|
||||
markConversationRead
|
||||
} from '@/api/hub';
|
||||
|
||||
const RED = '#C01227';
|
||||
|
||||
@@ -63,42 +70,27 @@ const toInitials = (name) =>
|
||||
.join('')
|
||||
.toUpperCase() || 'HB';
|
||||
|
||||
const MESSAGES = [
|
||||
{ id: 1, name: 'Devendra (Gate Supervisor)', text: 'Jaipur vehicle is backing into Bay 4 now.', time: '3 min ago', initials: 'DS' },
|
||||
{ id: 2, name: 'Suresh (Sorter Zone A)', text: 'Completed sorting for South Delhi batch. Ready to verify.', time: '12 min ago', initials: 'SS' },
|
||||
{ id: 3, name: 'Neha (Last-mile Dispatcher)', text: 'We need 2 more milers for South Delhi route.', time: '45 min ago', initials: 'ND' }
|
||||
];
|
||||
|
||||
const INITIAL_CHATS = {
|
||||
1: {
|
||||
name: 'Devendra (Gate Supervisor)',
|
||||
initials: 'DS',
|
||||
chat: [
|
||||
{ sender: 'them', text: 'Jaipur vehicle is backing into Bay 4 now. Sorters are ready.', time: '11:20 AM' },
|
||||
{ sender: 'me', text: 'Excellent. Please verify temperature logs for the cold container immediately upon opening.', time: '11:21 AM' },
|
||||
{ sender: 'them', text: 'Got it, Rajesh. Handled by sorter Devendra.', time: '11:22 AM' }
|
||||
]
|
||||
},
|
||||
2: {
|
||||
name: 'Suresh (Sorter Zone A)',
|
||||
initials: 'SS',
|
||||
chat: [
|
||||
{ sender: 'them', text: 'Completed sorting for South Delhi batch. Ready to verify.', time: '11:10 AM' },
|
||||
{ sender: 'me', text: 'Have all exceptions been routed to Zone D?', time: '11:12 AM' },
|
||||
{ sender: 'them', text: 'Yes, 3 damaged boxes are in Zone D. Rest are loaded in the manifest bags.', time: '11:15 AM' }
|
||||
]
|
||||
},
|
||||
3: {
|
||||
name: 'Neha (Last-mile Dispatcher)',
|
||||
initials: 'ND',
|
||||
chat: [
|
||||
{ sender: 'them', text: 'We need 2 more milers for South Delhi route. Surge in fashion orders.', time: '10:40 AM' },
|
||||
{ sender: 'me', text: 'Let me ping Deepak and Yashpal to see if they can start early shift.', time: '10:45 AM' },
|
||||
{ sender: 'them', text: 'Deepak just logged online. Still need one more.', time: '11:05 AM' }
|
||||
]
|
||||
}
|
||||
// Normalise a conversation summary from GET /hub/messages. Be tolerant of a few
|
||||
// backend field-name variants so the list still renders if a key is named differently.
|
||||
const toConversation = (c) => {
|
||||
const name = c.name || c.milername || c.displayname || `Miler ${c.mileruserid ?? c.id}`;
|
||||
return {
|
||||
id: c.id ?? c.conversationid ?? c.mileruserid,
|
||||
name,
|
||||
initials: toInitials(name),
|
||||
lastMessage: c.lastmessage ?? c.last_message ?? c.preview ?? '',
|
||||
time: c.time ?? c.lasttime ?? c.updatedat ?? '',
|
||||
unread: Number(c.unread ?? c.unreadcount ?? 0) || 0
|
||||
};
|
||||
};
|
||||
|
||||
// Normalise one message in a thread from GET /hub/messages/:id.
|
||||
const toMessage = (m) => ({
|
||||
sender: m.sender === 'me' ? 'me' : 'them',
|
||||
text: m.text ?? m.body ?? m.message ?? '',
|
||||
time: m.time ?? m.createdat ?? m.sentat ?? ''
|
||||
});
|
||||
|
||||
export default function Header({ onToggle }) {
|
||||
const navigate = useNavigate();
|
||||
const staff = getStaff();
|
||||
@@ -117,10 +109,14 @@ export default function Header({ onToggle }) {
|
||||
|
||||
// Dialog State
|
||||
const [selectedNotif, setSelectedNotif] = useState(null);
|
||||
const [selectedChatId, setSelectedChatId] = useState(null);
|
||||
const [chats, setChats] = useState(INITIAL_CHATS);
|
||||
const [conversations, setConversations] = useState([]);
|
||||
const [activeChat, setActiveChat] = useState(null); // { id, name, initials, messages: [] }
|
||||
const [chatLoading, setChatLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [typedMessage, setTypedMessage] = useState('');
|
||||
|
||||
const unreadMessages = conversations.reduce((sum, c) => sum + c.unread, 0);
|
||||
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
@@ -152,9 +148,28 @@ export default function Header({ onToggle }) {
|
||||
return () => clearInterval(t);
|
||||
}, [loadNotifications]);
|
||||
|
||||
// Load conversations (one per miler at the hub) for the messages dropdown.
|
||||
const loadConversations = useCallback(async () => {
|
||||
try {
|
||||
const res = await getConversations();
|
||||
setConversations((res?.data || []).map(toConversation));
|
||||
} catch {
|
||||
// Non-fatal: leave the messages list empty if it can't load.
|
||||
setConversations([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadConversations();
|
||||
const t = setInterval(loadConversations, 60000); // refresh every 60s
|
||||
return () => clearInterval(t);
|
||||
}, [loadConversations]);
|
||||
|
||||
const openNotif = (e) => { setNotifAnchor(e.currentTarget); loadNotifications(); };
|
||||
const closeNotif = () => setNotifAnchor(null);
|
||||
|
||||
const openMessages = (e) => { setMsgAnchor(e.currentTarget); loadConversations(); };
|
||||
|
||||
const markAllRead = async () => {
|
||||
const unreadIds = notifications.filter((n) => !n.read).map((n) => n.id);
|
||||
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||
@@ -171,30 +186,61 @@ export default function Header({ onToggle }) {
|
||||
}
|
||||
};
|
||||
|
||||
const onMessageClick = (mId) => {
|
||||
// Open a conversation: show the header immediately, then load the thread and
|
||||
// mark the other party's messages as read (which clears the unread badge).
|
||||
const onMessageClick = async (conv) => {
|
||||
setMsgAnchor(null);
|
||||
setSelectedChatId(mId);
|
||||
setTypedMessage('');
|
||||
setActiveChat({ id: conv.id, name: conv.name, initials: conv.initials, messages: [] });
|
||||
setChatLoading(true);
|
||||
try {
|
||||
const res = await getConversation(conv.id);
|
||||
const thread = res?.data || {};
|
||||
setActiveChat({
|
||||
id: conv.id,
|
||||
name: thread.name || conv.name,
|
||||
initials: toInitials(thread.name || conv.name),
|
||||
messages: (thread.messages || thread.chat || []).map(toMessage)
|
||||
});
|
||||
if (conv.unread > 0) {
|
||||
await markConversationRead(conv.id).catch(() => {});
|
||||
setConversations((prev) => prev.map((c) => (c.id === conv.id ? { ...c, unread: 0 } : c)));
|
||||
}
|
||||
} catch {
|
||||
// Leave the (empty) thread open; the header still shows who it's with.
|
||||
} finally {
|
||||
setChatLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!typedMessage.trim() || !selectedChatId) return;
|
||||
const closeChat = () => { setActiveChat(null); setTypedMessage(''); };
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const text = typedMessage.trim();
|
||||
if (!text || !activeChat || sending) return;
|
||||
setSending(true);
|
||||
// Optimistically append; reconcile with the server's stored copy on success.
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
setChats((prev) => {
|
||||
const activeChat = prev[selectedChatId];
|
||||
return {
|
||||
...prev,
|
||||
[selectedChatId]: {
|
||||
...activeChat,
|
||||
chat: [
|
||||
...activeChat.chat,
|
||||
{ sender: 'me', text: typedMessage, time: timeStr }
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
setActiveChat((c) => ({ ...c, messages: [...c.messages, { sender: 'me', text, time: timeStr }] }));
|
||||
setTypedMessage('');
|
||||
try {
|
||||
await sendMessage(activeChat.id, text);
|
||||
// Refresh the thread so the persisted message (and its real timestamp) shows.
|
||||
const res = await getConversation(activeChat.id);
|
||||
const thread = res?.data || {};
|
||||
setActiveChat((c) => c && { ...c, messages: (thread.messages || thread.chat || []).map(toMessage) });
|
||||
// Keep the dropdown preview in sync.
|
||||
setConversations((prev) =>
|
||||
prev.map((c) => (c.id === activeChat.id ? { ...c, lastMessage: text, time: timeStr } : c))
|
||||
);
|
||||
} catch {
|
||||
// On failure, drop the optimistic bubble and restore the draft to retry.
|
||||
setActiveChat((c) => c && { ...c, messages: c.messages.filter((m) => !(m.sender === 'me' && m.text === text && m.time === timeStr)) });
|
||||
setTypedMessage(text);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitSearch = (e) => {
|
||||
@@ -258,8 +304,8 @@ export default function Header({ onToggle }) {
|
||||
</Box>
|
||||
|
||||
<Tooltip title="Messages">
|
||||
<IconButton color="inherit" onClick={(e) => setMsgAnchor(e.currentTarget)}>
|
||||
<Badge badgeContent={MESSAGES.length} color="error">
|
||||
<IconButton color="inherit" onClick={openMessages}>
|
||||
<Badge badgeContent={unreadMessages} color="error">
|
||||
<ChatIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
@@ -354,8 +400,13 @@ export default function Header({ onToggle }) {
|
||||
Messages
|
||||
</Typography>
|
||||
<Divider />
|
||||
{MESSAGES.map((m) => (
|
||||
<MenuItem key={m.id} onClick={() => onMessageClick(m.id)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
|
||||
{conversations.length === 0 && (
|
||||
<MenuItem disabled>
|
||||
<ListItemText primary="No messages" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{conversations.map((m) => (
|
||||
<MenuItem key={m.id} onClick={() => onMessageClick(m)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
|
||||
<ListItemIcon sx={{ mt: 0.25 }}>
|
||||
<Avatar sx={{ width: 34, height: 34, bgcolor: alpha(RED, 0.12), color: RED, fontWeight: 700, fontSize: '0.8rem' }}>
|
||||
{m.initials}
|
||||
@@ -363,13 +414,18 @@ export default function Header({ onToggle }) {
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={m.name}
|
||||
secondary={m.text}
|
||||
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 700 }}
|
||||
secondaryTypographyProps={{ fontSize: '0.8rem' }}
|
||||
secondary={m.lastMessage || 'No messages yet'}
|
||||
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: m.unread > 0 ? 700 : 600 }}
|
||||
secondaryTypographyProps={{ fontSize: '0.8rem', noWrap: true }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, mt: 0.5, flexShrink: 0 }}>
|
||||
{m.time}
|
||||
</Typography>
|
||||
<Stack alignItems="flex-end" sx={{ ml: 1, flexShrink: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
{m.time}
|
||||
</Typography>
|
||||
{m.unread > 0 && (
|
||||
<Badge badgeContent={m.unread} color="error" sx={{ mt: 1, mr: 0.75 }} />
|
||||
)}
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
@@ -439,21 +495,31 @@ export default function Header({ onToggle }) {
|
||||
</Dialog>
|
||||
|
||||
{/* High Fidelity Chat Message Dialog */}
|
||||
<Dialog open={Boolean(selectedChatId)} onClose={() => setSelectedChatId(null)} fullWidth maxWidth="xs">
|
||||
{selectedChatId && chats[selectedChatId] && (
|
||||
<Dialog open={Boolean(activeChat)} onClose={closeChat} fullWidth maxWidth="xs">
|
||||
{activeChat && (
|
||||
<>
|
||||
<DialogTitle sx={{ fontWeight: 700, py: 2, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Avatar sx={{ bgcolor: alpha(RED, 0.12), color: RED, fontWeight: 700, width: 34, height: 34 }}>
|
||||
{chats[selectedChatId].initials}
|
||||
{activeChat.initials}
|
||||
</Avatar>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||
{chats[selectedChatId].name}
|
||||
{activeChat.name}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent sx={{ p: 2, bgcolor: 'grey.50', height: 280, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<Stack spacing={2} sx={{ overflowY: 'auto', pr: 0.5, flexGrow: 1, mb: 2 }}>
|
||||
{chats[selectedChatId].chat.map((msg, idx) => (
|
||||
{chatLoading && activeChat.messages.length === 0 && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mt: 2 }}>
|
||||
Loading…
|
||||
</Typography>
|
||||
)}
|
||||
{!chatLoading && activeChat.messages.length === 0 && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mt: 2 }}>
|
||||
No messages yet. Say hello.
|
||||
</Typography>
|
||||
)}
|
||||
{activeChat.messages.map((msg, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
sx={{
|
||||
@@ -503,7 +569,7 @@ export default function Header({ onToggle }) {
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleSendMessage} size="small" color="primary">
|
||||
<IconButton onClick={handleSendMessage} size="small" color="primary" disabled={sending || !typedMessage.trim()}>
|
||||
<SendIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
|
||||
Reference in New Issue
Block a user