Browse Source

feat(web): allow auto-refreshing of message history in the dashboard

pull/58/head
isra el 12 months ago
parent
commit
b5e524aafd
  1. 307
      web/app/(app)/dashboard/(components)/message-history.tsx

307
web/app/(app)/dashboard/(components)/message-history.tsx

@ -1,11 +1,22 @@
'use client' 'use client'
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Clock, Reply, ArrowUpRight, ArrowDownLeft, MessageSquare, Check, X, Smartphone } from 'lucide-react'
import {
Clock,
Reply,
ArrowUpRight,
ArrowDownLeft,
MessageSquare,
Check,
X,
Smartphone,
RefreshCw,
Timer,
} from 'lucide-react'
import { import {
Select, Select,
SelectContent, SelectContent,
@ -31,7 +42,7 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Badge } from "@/components/ui/badge"
import { Badge } from '@/components/ui/badge'
function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) { function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -201,7 +212,13 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
) )
} }
function FollowUpDialog({ message, onClose }: { message: any; onClose?: () => void }) {
function FollowUpDialog({
message,
onClose,
}: {
message: any
onClose?: () => void
}) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { const {
@ -231,7 +248,11 @@ function FollowUpDialog({ message, onClose }: { message: any; onClose?: () => vo
resolver: zodResolver(sendSmsSchema), resolver: zodResolver(sendSmsSchema),
defaultValues: { defaultValues: {
deviceId: message?.device?._id, deviceId: message?.device?._id,
recipients: [message.recipient || (message.recipients && message.recipients[0]) || ''],
recipients: [
message.recipient ||
(message.recipients && message.recipients[0]) ||
'',
],
message: '', message: '',
}, },
}) })
@ -248,7 +269,11 @@ function FollowUpDialog({ message, onClose }: { message: any; onClose?: () => vo
if (open) { if (open) {
reset({ reset({
deviceId: message?.device?._id, deviceId: message?.device?._id,
recipients: [message.recipient || (message.recipients && message.recipients[0]) || ''],
recipients: [
message.recipient ||
(message.recipients && message.recipients[0]) ||
'',
],
message: '', message: '',
}) })
} }
@ -266,7 +291,10 @@ function FollowUpDialog({ message, onClose }: { message: any; onClose?: () => vo
<DialogHeader> <DialogHeader>
<DialogTitle className='flex items-center gap-2'> <DialogTitle className='flex items-center gap-2'>
<MessageSquare className='h-5 w-5' /> <MessageSquare className='h-5 w-5' />
Follow Up with {message.recipient || (message.recipients && message.recipients[0]) || 'Recipient'}
Follow Up with{' '}
{message.recipient ||
(message.recipients && message.recipients[0]) ||
'Recipient'}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
Send a follow-up message to this recipient Send a follow-up message to this recipient
@ -372,7 +400,9 @@ function FollowUpDialog({ message, onClose }: { message: any; onClose?: () => vo
function MessageCard({ message, type }) { function MessageCard({ message, type }) {
const isSent = type === 'sent' const isSent = type === 'sent'
const formattedDate = new Date((isSent ? message.requestedAt : message.receivedAt) || message.createdAt).toLocaleString('en-US', {
const formattedDate = new Date(
(isSent ? message.requestedAt : message.receivedAt) || message.createdAt
).toLocaleString('en-US', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
day: 'numeric', day: 'numeric',
@ -380,10 +410,14 @@ function MessageCard({ message, type }) {
year: 'numeric', year: 'numeric',
}) })
return ( return (
<Card className={`hover:bg-muted/50 transition-colors max-w-sm md:max-w-none ${isSent ? 'border-l-4 border-l-blue-500' : 'border-l-4 border-l-green-500'}`}>
<Card
className={`hover:bg-muted/50 transition-colors max-w-sm md:max-w-none ${
isSent
? 'border-l-4 border-l-blue-500'
: 'border-l-4 border-l-green-500'
}`}
>
<CardContent className='p-4'> <CardContent className='p-4'>
<div className='space-y-3'> <div className='space-y-3'>
<div className='flex justify-between items-start'> <div className='flex justify-between items-start'>
@ -391,7 +425,12 @@ function MessageCard({ message, type }) {
{isSent ? ( {isSent ? (
<div className='flex items-center text-blue-600 dark:text-blue-400 font-medium'> <div className='flex items-center text-blue-600 dark:text-blue-400 font-medium'>
<ArrowUpRight className='h-4 w-4 mr-1' /> <ArrowUpRight className='h-4 w-4 mr-1' />
<span>To: {message.recipient || (message.recipients && message.recipients[0]) || 'Unknown'}</span>
<span>
To:{' '}
{message.recipient ||
(message.recipients && message.recipients[0]) ||
'Unknown'}
</span>
</div> </div>
) : ( ) : (
<div className='flex items-center text-green-600 dark:text-green-400 font-medium'> <div className='flex items-center text-green-600 dark:text-green-400 font-medium'>
@ -415,7 +454,7 @@ function MessageCard({ message, type }) {
<ReplyDialog sms={message} /> <ReplyDialog sms={message} />
</div> </div>
)} )}
{isSent && ( {isSent && (
<div className='flex justify-end'> <div className='flex justify-end'>
<FollowUpDialog message={message} /> <FollowUpDialog message={message} />
@ -460,6 +499,9 @@ export default function MessageHistory() {
const [messageType, setMessageType] = useState('all') const [messageType, setMessageType] = useState('all')
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [limit, setLimit] = useState(20) const [limit, setLimit] = useState(20)
const [autoRefreshInterval, setAutoRefreshInterval] = useState(0) // 0 means no auto-refresh
const [isRefreshing, setIsRefreshing] = useState(false)
const refreshTimerRef = useRef(null)
useEffect(() => { useEffect(() => {
if (devices?.data?.length) { if (devices?.data?.length) {
@ -472,21 +514,57 @@ export default function MessageHistory() {
data: messagesResponse, data: messagesResponse,
isLoading: isLoadingMessages, isLoading: isLoadingMessages,
error: messagesError, error: messagesError,
refetch,
} = useQuery({ } = useQuery({
queryKey: ['messages-history', currentDevice, messageType, page, limit], queryKey: ['messages-history', currentDevice, messageType, page, limit],
enabled: !!currentDevice, enabled: !!currentDevice,
queryFn: () => queryFn: () =>
httpBrowserClient httpBrowserClient
.get( .get(
`${ApiEndpoints.gateway.getMessages(currentDevice)}?type=${messageType}&page=${page}&limit=${limit}`
`${ApiEndpoints.gateway.getMessages(
currentDevice
)}?type=${messageType}&page=${page}&limit=${limit}`
) )
.then((res) => res.data), .then((res) => res.data),
}) })
// Handle manual refresh
const handleRefresh = async () => {
if (!currentDevice) return // Don't refresh if no device is selected
setIsRefreshing(true)
await refetch()
setTimeout(() => setIsRefreshing(false), 500) // Show refresh animation for at least 500ms
}
// Setup auto-refresh timer
useEffect(() => {
// Clear any existing timer
if (refreshTimerRef.current) {
clearInterval(refreshTimerRef.current)
refreshTimerRef.current = null
}
// Set up new timer if interval > 0
if (autoRefreshInterval > 0 && currentDevice) {
refreshTimerRef.current = setInterval(() => {
refetch()
// Brief visual feedback that refresh happened
setIsRefreshing(true)
setTimeout(() => setIsRefreshing(false), 300)
}, autoRefreshInterval * 1000)
}
// Cleanup on unmount
return () => {
if (refreshTimerRef.current) {
clearInterval(refreshTimerRef.current)
}
}
}, [autoRefreshInterval, currentDevice, messageType, page, limit, refetch])
const messages = messagesResponse?.data || [] const messages = messagesResponse?.data || []
const pagination = messagesResponse?.meta || { const pagination = messagesResponse?.meta || {
page: 1, page: 1,
limit: 20, limit: 20,
@ -536,60 +614,131 @@ export default function MessageHistory() {
return ( return (
<div className='space-y-4'> <div className='space-y-4'>
<div className="bg-gradient-to-r from-blue-50 to-sky-50 dark:from-blue-950/30 dark:to-sky-950/30 rounded-lg shadow-sm border border-blue-100 dark:border-blue-800/50 p-4 mb-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1.5">
<Smartphone className="h-3.5 w-3.5 text-blue-500" />
<h3 className="text-sm font-medium text-foreground">Device</h3>
<div className='bg-gradient-to-r from-blue-50 to-sky-50 dark:from-blue-950/30 dark:to-sky-950/30 rounded-lg shadow-sm border border-blue-100 dark:border-blue-800/50 p-4 mb-4'>
<div className='flex flex-col gap-4'>
<div className='flex flex-col sm:flex-row sm:items-center gap-3'>
<div className='flex-1'>
<div className='flex items-center gap-2 mb-1.5'>
<Smartphone className='h-3.5 w-3.5 text-blue-500' />
<h3 className='text-sm font-medium text-foreground'>Device</h3>
</div>
<Select value={currentDevice} onValueChange={handleDeviceChange}>
<SelectTrigger className='w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-blue-200 dark:border-blue-800/70'>
<SelectValue placeholder='Select a device' />
</SelectTrigger>
<SelectContent>
{devices?.data?.map((device) => (
<SelectItem key={device._id} value={device._id}>
<div className='flex items-center gap-2'>
<span className='font-medium'>
{device.brand} {device.model}
</span>
{!device.enabled && (
<Badge
variant='outline'
className='ml-1 text-xs py-0 h-5'
>
Disabled
</Badge>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<Select value={currentDevice} onValueChange={handleDeviceChange}>
<SelectTrigger className="w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-blue-200 dark:border-blue-800/70">
<SelectValue placeholder='Select a device' />
</SelectTrigger>
<SelectContent>
{devices?.data?.map((device) => (
<SelectItem key={device._id} value={device._id}>
<div className="flex items-center gap-2">
<span className="font-medium">{device.brand} {device.model}</span>
{!device.enabled && <Badge variant="outline" className="ml-1 text-xs py-0 h-5">Disabled</Badge>}
<div className='w-full sm:w-44'>
<div className='flex items-center gap-2 mb-1.5'>
<MessageSquare className='h-3.5 w-3.5 text-blue-500' />
<h3 className='text-sm font-medium text-foreground'>
Message Type
</h3>
</div>
<Select
value={messageType}
onValueChange={handleMessageTypeChange}
>
<SelectTrigger className='w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-blue-200 dark:border-blue-800/70'>
<SelectValue placeholder='Message type' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>
<div className='flex items-center gap-1.5'>
<div className='h-1.5 w-1.5 rounded-full bg-gray-500'></div>
All Messages
</div> </div>
</SelectItem> </SelectItem>
))}
</SelectContent>
</Select>
<SelectItem value='received'>
<div className='flex items-center gap-1.5'>
<div className='h-1.5 w-1.5 rounded-full bg-green-500'></div>
Received
</div>
</SelectItem>
<SelectItem value='sent'>
<div className='flex items-center gap-1.5'>
<div className='h-1.5 w-1.5 rounded-full bg-blue-500'></div>
Sent
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
<div className="w-full sm:w-44">
<div className="flex items-center gap-2 mb-1.5">
<MessageSquare className="h-3.5 w-3.5 text-blue-500" />
<h3 className="text-sm font-medium text-foreground">Message Type</h3>
{/* Refresh Controls */}
<div className='flex items-center justify-between gap-2 pt-2 mt-2 border-t border-blue-100 dark:border-blue-800/50'>
<div className='flex items-center gap-1.5'>
<Button
onClick={handleRefresh}
variant='ghost'
size='sm'
disabled={!currentDevice}
className='h-7 px-2 text-xs text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/30'
>
<RefreshCw
className={`h-3.5 w-3.5 mr-1 ${
isRefreshing ? 'animate-spin' : ''
}`}
/>
Refresh Now
</Button>
{/* {messagesResponse && (
<span className='text-xs text-muted-foreground hidden sm:inline-block'>
Updated: {new Date().toLocaleTimeString()}
</span>
)} */}
</div>
<div className='flex items-center gap-1.5'>
<Timer className='h-3 w-3 text-blue-500' />
<span className='text-xs font-medium mr-1'>Auto Refresh:</span>
<div className='flex'>
{[
{ value: 0, label: 'Off' },
{ value: 15, label: '15s' },
{ value: 30, label: '30s' },
{ value: 60, label: '60s' },
].map((interval) => (
<Button
key={interval.value}
size='sm'
variant='ghost'
disabled={!currentDevice && interval.value > 0}
className={`h-6 px-1.5 text-xs ${
autoRefreshInterval === interval.value
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium'
: 'text-muted-foreground hover:bg-blue-50 dark:hover:bg-blue-900/20'
}`}
onClick={() => setAutoRefreshInterval(interval.value)}
>
{interval.label}
</Button>
))}
</div>
</div> </div>
<Select value={messageType} onValueChange={handleMessageTypeChange}>
<SelectTrigger className="w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-blue-200 dark:border-blue-800/70">
<SelectValue placeholder='Message type' />
</SelectTrigger>
<SelectContent>
<SelectItem value='all'>
<div className="flex items-center gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-gray-500"></div>
All Messages
</div>
</SelectItem>
<SelectItem value='received'>
<div className="flex items-center gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-green-500"></div>
Received
</div>
</SelectItem>
<SelectItem value='sent'>
<div className="flex items-center gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-blue-500"></div>
Sent
</div>
</SelectItem>
</SelectContent>
</Select>
</div> </div>
</div> </div>
</div> </div>
@ -608,7 +757,7 @@ export default function MessageHistory() {
</div> </div>
)} )}
{(!isLoadingDevices && !messages) && (
{!isLoadingDevices && !messages && (
<div className='flex justify-center items-center h-full py-10'> <div className='flex justify-center items-center h-full py-10'>
No messages found No messages found
</div> </div>
@ -616,10 +765,10 @@ export default function MessageHistory() {
<div className='space-y-4'> <div className='space-y-4'>
{messages?.map((message) => ( {messages?.map((message) => (
<MessageCard
key={message._id}
message={message}
type={message.sender ? 'received' : 'sent'}
<MessageCard
key={message._id}
message={message}
type={message.sender ? 'received' : 'sent'}
/> />
))} ))}
</div> </div>
@ -677,10 +826,7 @@ export default function MessageHistory() {
} }
// Ensure page is within bounds and not the first or last page // Ensure page is within bounds and not the first or last page
if (
pageToShow > 1 &&
pageToShow < pagination.totalPages
) {
if (pageToShow > 1 && pageToShow < pagination.totalPages) {
return ( return (
<Button <Button
key={pageToShow} key={pageToShow}
@ -702,18 +848,15 @@ export default function MessageHistory() {
)} )}
{/* Ellipsis if needed */} {/* Ellipsis if needed */}
{page < pagination.totalPages - 3 &&
pagination.totalPages > 7 && (
<span className='px-1'>...</span>
)}
{page < pagination.totalPages - 3 && pagination.totalPages > 7 && (
<span className='px-1'>...</span>
)}
{/* Last page */} {/* Last page */}
{pagination.totalPages > 1 && ( {pagination.totalPages > 1 && (
<Button <Button
onClick={() => handlePageChange(pagination.totalPages)} onClick={() => handlePageChange(pagination.totalPages)}
variant={
page === pagination.totalPages ? 'default' : 'ghost'
}
variant={page === pagination.totalPages ? 'default' : 'ghost'}
size='icon' size='icon'
className={`h-8 w-8 rounded-full ${ className={`h-8 w-8 rounded-full ${
page === pagination.totalPages page === pagination.totalPages
@ -739,4 +882,4 @@ export default function MessageHistory() {
)} )}
</div> </div>
) )
}
}
Loading…
Cancel
Save