mirror of
https://github.com/vernu/textbee.git
synced 2026-05-19 22:10:58 -04:00
@@ -7,6 +7,7 @@ import {
|
||||
Controller,
|
||||
Get,
|
||||
UseGuards,
|
||||
Query,
|
||||
} from '@nestjs/common'
|
||||
import { WebhookService } from './webhook.service'
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'
|
||||
@@ -27,7 +28,30 @@ export class WebhookController {
|
||||
})
|
||||
return { data }
|
||||
}
|
||||
|
||||
@Get('notifications')
|
||||
@UseGuards(AuthGuard)
|
||||
async getWebhookNotifications(
|
||||
@Request() req,
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('status') status?: string,
|
||||
@Query('eventType') eventType?: string,
|
||||
@Query('deviceId') deviceId?: string,
|
||||
@Query('start') start?: Date,
|
||||
@Query('end') end?: Date,
|
||||
) {
|
||||
const data = await this.webhookService.findWebhookNotificationsForUser({
|
||||
user: req.user,
|
||||
page,
|
||||
limit,
|
||||
eventType,
|
||||
status,
|
||||
start,
|
||||
end,
|
||||
deviceId,
|
||||
})
|
||||
return { data }
|
||||
}
|
||||
@Get(':webhookId')
|
||||
@UseGuards(AuthGuard)
|
||||
async getWebhook(@Request() req, @Param('webhookId') webhookId: string) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { Cron } from '@nestjs/schedule'
|
||||
import { CronExpression } from '@nestjs/schedule'
|
||||
import * as crypto from 'crypto'
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
@Injectable()
|
||||
export class WebhookService {
|
||||
@@ -40,7 +41,181 @@ export class WebhookService {
|
||||
async findWebhooksForUser({ user }) {
|
||||
return await this.webhookSubscriptionModel.find({ user: user._id })
|
||||
}
|
||||
async findWebhookNotificationsForUser({
|
||||
user,
|
||||
page = 1,
|
||||
limit = 10,
|
||||
eventType,
|
||||
status,
|
||||
start,
|
||||
end,
|
||||
deviceId,
|
||||
}): Promise<{ data: any[]; meta: any }> {
|
||||
const userWebhookSubscription = await this.webhookSubscriptionModel.findOne(
|
||||
{
|
||||
user: user._id,
|
||||
},
|
||||
)
|
||||
|
||||
if (!userWebhookSubscription) {
|
||||
return {
|
||||
data: [],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const matchStage: any = { webhookSubscription: userWebhookSubscription._id }
|
||||
|
||||
if (eventType) {
|
||||
matchStage.event = eventType
|
||||
}
|
||||
|
||||
if (
|
||||
start &&
|
||||
end &&
|
||||
!Number.isNaN(new Date(start).getTime()) &&
|
||||
!Number.isNaN(new Date(end).getTime())
|
||||
) {
|
||||
matchStage.createdAt = { $gte: new Date(start), $lte: new Date(end) }
|
||||
}
|
||||
|
||||
|
||||
const pageNum = Math.max(1, Number.parseInt(page.toString()) || 1)
|
||||
const limitNum = Math.max(1, Number.parseInt(limit.toString()) || 10)
|
||||
const skip = (pageNum - 1) * limitNum
|
||||
|
||||
const commonPipeline: any[] = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'sms',
|
||||
localField: 'sms',
|
||||
foreignField: '_id',
|
||||
as: 'smsData',
|
||||
},
|
||||
},
|
||||
{ $unwind: { path: '$smsData', preserveNullAndEmptyArrays: true } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'devices',
|
||||
localField: 'smsData.device',
|
||||
foreignField: '_id',
|
||||
as: 'deviceData',
|
||||
},
|
||||
},
|
||||
{ $unwind: { path: '$deviceData', preserveNullAndEmptyArrays: true } },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'webhooksubscriptions',
|
||||
localField: 'webhookSubscription',
|
||||
foreignField: '_id',
|
||||
as: 'webhookSubscriptionData',
|
||||
},
|
||||
},
|
||||
{
|
||||
$unwind: {
|
||||
path: '$webhookSubscriptionData',
|
||||
preserveNullAndEmptyArrays: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
computedStatus: {
|
||||
$cond: {
|
||||
if: { $ne: ['$deliveredAt', null] },
|
||||
then: 'delivered',
|
||||
else: {
|
||||
$cond: {
|
||||
if: {
|
||||
$or: [
|
||||
{ $ne: ['$deliveryAttemptAbortedAt', null] },
|
||||
{ $gte: ['$deliveryAttemptCount', 10] }
|
||||
]
|
||||
},
|
||||
then: 'failed',
|
||||
else: {
|
||||
$cond: {
|
||||
if: {
|
||||
$and: [
|
||||
{ $eq: ['$deliveredAt', null] },
|
||||
{ $eq: ['$deliveryAttemptAbortedAt', null] },
|
||||
{ $gt: ['$deliveryAttemptCount', 0] },
|
||||
{ $lt: ['$deliveryAttemptCount', 10] }
|
||||
]
|
||||
},
|
||||
then: 'retrying',
|
||||
else: 'pending'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// Apply status filter
|
||||
if (status) {
|
||||
commonPipeline.push({
|
||||
$match: {
|
||||
computedStatus: status
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (deviceId) {
|
||||
commonPipeline.push({
|
||||
$match: {
|
||||
'deviceData._id': new mongoose.Types.ObjectId(deviceId),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const facetPipeline = [
|
||||
...commonPipeline,
|
||||
{
|
||||
$facet: {
|
||||
totalCount: [{ $count: 'count' }],
|
||||
data: [
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $skip: skip },
|
||||
{ $limit: limitNum },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
data: 1,
|
||||
totalCount: {
|
||||
$ifNull: [{ $arrayElemAt: ['$totalCount.count', 0] }, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const [result] =
|
||||
await this.webhookNotificationModel.aggregate(facetPipeline)
|
||||
|
||||
const total = result?.totalCount || 0
|
||||
const data = result?.data || []
|
||||
const totalPages = Math.ceil(total / limitNum)
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
totalPages,
|
||||
},
|
||||
}
|
||||
}
|
||||
async create({ user, createWebhookDto }) {
|
||||
const { events, deliveryUrl, signingSecret } = createWebhookDto
|
||||
|
||||
@@ -164,9 +339,7 @@ export class WebhookService {
|
||||
)
|
||||
|
||||
if (!webhookSubscription) {
|
||||
console.log(
|
||||
`Webhook subscription not found for ${webhookSubscriptionId}`,
|
||||
)
|
||||
console.log(`Webhook subscription not found for ${webhookSubscriptionId}`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -217,7 +390,6 @@ export class WebhookService {
|
||||
|
||||
webhookSubscription.deliveryFailureCount += 1
|
||||
webhookSubscription.lastDeliveryFailureAt = now
|
||||
|
||||
} finally {
|
||||
webhookSubscription.deliveryAttemptCount += 1
|
||||
await webhookSubscription.save()
|
||||
@@ -250,7 +422,7 @@ export class WebhookService {
|
||||
|
||||
// Check for notifications that need to be delivered every 3 minutes
|
||||
@Cron('0 */3 * * * *', {
|
||||
disabled: process.env.NODE_ENV !== 'production'
|
||||
disabled: process.env.NODE_ENV !== 'production',
|
||||
})
|
||||
async checkForNotificationsToDeliver() {
|
||||
const now = new Date()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
Smartphone,
|
||||
RefreshCw,
|
||||
Timer,
|
||||
Copy,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Select,
|
||||
@@ -30,38 +32,94 @@ import { useForm, Controller } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { sendSmsSchema } from '@/lib/schemas'
|
||||
import type { SendSmsFormData } from '@/lib/schemas'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Helper function to format timestamps
|
||||
const formatTimestamp = (timestamp: string | null | undefined) => {
|
||||
if (!timestamp) return 'N/A'
|
||||
return new Date(timestamp).toLocaleString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
// Helper to get status color and icon
|
||||
const getStatusBadge = (status: string) => {
|
||||
const normalizedStatus = status?.toLowerCase() || 'pending'
|
||||
switch (normalizedStatus) {
|
||||
case 'pending':
|
||||
return {
|
||||
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
icon: <Timer className='h-3 w-3' />,
|
||||
label: 'Pending',
|
||||
}
|
||||
case 'sent':
|
||||
return {
|
||||
color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
icon: <Check className='h-3 w-3' />,
|
||||
label: 'Sent',
|
||||
}
|
||||
case 'delivered':
|
||||
return {
|
||||
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
icon: <Check className='h-3 w-3' />,
|
||||
label: 'Delivered',
|
||||
}
|
||||
case 'failed':
|
||||
return {
|
||||
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
icon: <X className='h-3 w-3' />,
|
||||
label: 'Failed',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300',
|
||||
icon: <Timer className='h-3 w-3' />,
|
||||
label: normalizedStatus,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ReplyDialog({ sms, onClose, open, onOpenChange }: { sms: any; onClose?: () => void; open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
|
||||
const {
|
||||
mutate: sendSms,
|
||||
isPending: isSendingSms,
|
||||
error: sendSmsError,
|
||||
isSuccess: isSendSmsSuccess,
|
||||
} = useMutation({
|
||||
mutationKey: ['send-sms'],
|
||||
mutationFn: (data: SendSmsFormData) =>
|
||||
httpBrowserClient.post(ApiEndpoints.gateway.sendSMS(data.deviceId), data),
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'SMS sent successfully!',
|
||||
})
|
||||
setTimeout(() => {
|
||||
setOpen(false)
|
||||
onOpenChange(false)
|
||||
if (onClose) onClose()
|
||||
}, 1500)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: 'Failed to send SMS.',
|
||||
description: error.response?.data?.message || 'Please try again.',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -79,12 +137,9 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
},
|
||||
})
|
||||
|
||||
const { data: devices, isLoading: isLoadingDevices } = useQuery({
|
||||
const { data: devices } = useQuery({
|
||||
queryKey: ['devices'],
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(ApiEndpoints.gateway.listDevices())
|
||||
.then((res) => res.data),
|
||||
queryFn: () => httpBrowserClient.get(ApiEndpoints.gateway.listDevices()).then((res) => res.data),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,25 +153,19 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
}, [open, sms, reset])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='ghost' size='sm' className='gap-1'>
|
||||
<Reply className='h-3.5 w-3.5' />
|
||||
Reply
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className='sm:max-w-[500px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<MessageSquare className='h-5 w-5' />
|
||||
<Reply className='h-5 w-5' />
|
||||
Reply to {sms.sender}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Send a reply message to this sender
|
||||
Send a reply message to this sender.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(e) => handleSubmit((data) => sendSms(data))(e)}
|
||||
onSubmit={handleSubmit((data) => sendSms(data))}
|
||||
className='space-y-4 mt-4'
|
||||
>
|
||||
<div className='space-y-4'>
|
||||
@@ -134,7 +183,7 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
<SelectValue placeholder='Select a device' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{devices?.data?.map((device) => (
|
||||
{devices?.data?.map((device: any) => (
|
||||
<SelectItem key={device._id} value={device._id}>
|
||||
{device.brand} {device.model}{' '}
|
||||
{device.enabled ? '' : '(disabled)'}
|
||||
@@ -150,7 +199,6 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type='tel'
|
||||
@@ -163,7 +211,6 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Textarea
|
||||
placeholder='Message'
|
||||
@@ -177,25 +224,11 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{sendSmsError && (
|
||||
<div className='flex items-center gap-2 text-destructive'>
|
||||
<p>Error sending SMS: {sendSmsError.message}</p>
|
||||
<X className='h-5 w-5' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSendSmsSuccess && (
|
||||
<div className='flex items-center gap-2 text-green-600'>
|
||||
<p>SMS sent successfully!</p>
|
||||
<Check className='h-5 w-5' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -212,30 +245,30 @@ function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) {
|
||||
)
|
||||
}
|
||||
|
||||
function FollowUpDialog({
|
||||
message,
|
||||
onClose,
|
||||
}: {
|
||||
message: any
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
function FollowUpDialog({ message, onClose, open, onOpenChange }: { message: any; onClose?: () => void; open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const {
|
||||
mutate: sendSms,
|
||||
isPending: isSendingSms,
|
||||
error: sendSmsError,
|
||||
isSuccess: isSendSmsSuccess,
|
||||
} = useMutation({
|
||||
mutationKey: ['send-sms'],
|
||||
mutationFn: (data: SendSmsFormData) =>
|
||||
httpBrowserClient.post(ApiEndpoints.gateway.sendSMS(data.deviceId), data),
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Follow-up SMS sent successfully!',
|
||||
})
|
||||
setTimeout(() => {
|
||||
setOpen(false)
|
||||
onOpenChange(false)
|
||||
if (onClose) onClose()
|
||||
}, 1500)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: 'Failed to send follow-up SMS.',
|
||||
description: error.response?.data?.message || 'Please try again.',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -250,19 +283,16 @@ function FollowUpDialog({
|
||||
deviceId: message?.device?._id,
|
||||
recipients: [
|
||||
message.recipient ||
|
||||
(message.recipients && message.recipients[0]) ||
|
||||
'',
|
||||
(message.recipients && message.recipients[0]) ||
|
||||
'',
|
||||
],
|
||||
message: '',
|
||||
},
|
||||
})
|
||||
|
||||
const { data: devices, isLoading: isLoadingDevices } = useQuery({
|
||||
const { data: devices } = useQuery({
|
||||
queryKey: ['devices'],
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(ApiEndpoints.gateway.listDevices())
|
||||
.then((res) => res.data),
|
||||
queryFn: () => httpBrowserClient.get(ApiEndpoints.gateway.listDevices()).then((res) => res.data),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -271,8 +301,8 @@ function FollowUpDialog({
|
||||
deviceId: message?.device?._id,
|
||||
recipients: [
|
||||
message.recipient ||
|
||||
(message.recipients && message.recipients[0]) ||
|
||||
'',
|
||||
(message.recipients && message.recipients[0]) ||
|
||||
'',
|
||||
],
|
||||
message: '',
|
||||
})
|
||||
@@ -280,13 +310,7 @@ function FollowUpDialog({
|
||||
}, [open, message, reset])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='ghost' size='sm' className='gap-1'>
|
||||
<MessageSquare className='h-3.5 w-3.5' />
|
||||
Follow Up
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className='sm:max-w-[500px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
@@ -297,11 +321,11 @@ function FollowUpDialog({
|
||||
'Recipient'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Send a follow-up message to this recipient
|
||||
Send a follow-up message to this recipient.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(e) => handleSubmit((data) => sendSms(data))(e)}
|
||||
onSubmit={handleSubmit((data) => sendSms(data))}
|
||||
className='space-y-4 mt-4'
|
||||
>
|
||||
<div className='space-y-4'>
|
||||
@@ -319,7 +343,7 @@ function FollowUpDialog({
|
||||
<SelectValue placeholder='Select a device' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{devices?.data?.map((device) => (
|
||||
{devices?.data?.map((device: any) => (
|
||||
<SelectItem key={device._id} value={device._id}>
|
||||
{device.brand} {device.model}{' '}
|
||||
{device.enabled ? '' : '(disabled)'}
|
||||
@@ -335,7 +359,6 @@ function FollowUpDialog({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type='tel'
|
||||
@@ -348,7 +371,6 @@ function FollowUpDialog({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Textarea
|
||||
placeholder='Message'
|
||||
@@ -362,25 +384,11 @@ function FollowUpDialog({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{sendSmsError && (
|
||||
<div className='flex items-center gap-2 text-destructive'>
|
||||
<p>Error sending SMS: {sendSmsError.message}</p>
|
||||
<X className='h-5 w-5' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSendSmsSuccess && (
|
||||
<div className='flex items-center gap-2 text-green-600'>
|
||||
<p>SMS sent successfully!</p>
|
||||
<Check className='h-5 w-5' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -397,164 +405,169 @@ function FollowUpDialog({
|
||||
)
|
||||
}
|
||||
|
||||
function StatusDetailsDialog({ message }: { message: any }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Format timestamps for display
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return 'N/A';
|
||||
return new Date(timestamp).toLocaleString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
// Get status badge color and icon based on message status
|
||||
const getStatusBadge = () => {
|
||||
const status = message.status?.toLowerCase() || 'pending';
|
||||
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return {
|
||||
color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
icon: <Timer className="h-3 w-3 mr-1" />,
|
||||
label: 'Pending'
|
||||
};
|
||||
case 'sent':
|
||||
return {
|
||||
color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
icon: <Check className="h-3 w-3 mr-1" />,
|
||||
label: 'Sent'
|
||||
};
|
||||
case 'delivered':
|
||||
return {
|
||||
color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
icon: <Check className="h-3 w-3 mr-1" />,
|
||||
label: 'Delivered'
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
icon: <X className="h-3 w-3 mr-1" />,
|
||||
label: 'Failed'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300',
|
||||
icon: <Timer className="h-3 w-3 mr-1" />,
|
||||
label: status
|
||||
};
|
||||
}
|
||||
};
|
||||
// New component for full SMS details
|
||||
function SmsDetailsDialog({
|
||||
message,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
message: any
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}) {
|
||||
const [isReplyOpen, setIsReplyOpen] = useState(false)
|
||||
const [isFollowUpOpen, setIsFollowUpOpen] = useState(false)
|
||||
|
||||
const statusBadge = getStatusBadge(message?.status)
|
||||
const isSent = !!message?.recipient || (message?.recipients && message.recipients.length > 0)
|
||||
|
||||
const handleCopyMessage = () => {
|
||||
if (message?.message) {
|
||||
navigator.clipboard.writeText(message.message)
|
||||
toast({
|
||||
title: 'Message copied to clipboard!',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleReplyClick = () => {
|
||||
onOpenChange(false)
|
||||
setIsReplyOpen(true)
|
||||
}
|
||||
|
||||
const handleFollowUpClick = () => {
|
||||
onOpenChange(false)
|
||||
setIsFollowUpOpen(true)
|
||||
}
|
||||
|
||||
const handleReplyDialogClose = () => setIsReplyOpen(false)
|
||||
const handleFollowUpDialogClose = () => setIsFollowUpOpen(false)
|
||||
|
||||
const statusBadge = getStatusBadge();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Badge variant="outline" className={`${statusBadge.color} flex items-center text-xs cursor-pointer`}>
|
||||
{statusBadge.icon}
|
||||
{statusBadge.label}
|
||||
</Badge>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
SMS Status Details
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detailed information about this SMS message
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="font-medium">Status</div>
|
||||
<div>{message.status || 'pending'}</div>
|
||||
|
||||
<div className="font-medium">Requested At</div>
|
||||
<div>{formatTimestamp(message.requestedAt)}</div>
|
||||
|
||||
<div className="font-medium">Sent At</div>
|
||||
<div>{formatTimestamp(message.sentAt)}</div>
|
||||
|
||||
<div className="font-medium">Delivered At</div>
|
||||
<div>{formatTimestamp(message.deliveredAt)}</div>
|
||||
|
||||
{message.status?.toLowerCase() === 'failed' && (
|
||||
<>
|
||||
<div className="font-medium">Failed At</div>
|
||||
<div>{formatTimestamp(message.failedAt)}</div>
|
||||
|
||||
{message.errorCode && (
|
||||
<>
|
||||
<div className="font-medium">Error Code</div>
|
||||
<div className="">{message.errorCode}</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{message.errorMessage && (
|
||||
<>
|
||||
<div className="font-medium">Error Message</div>
|
||||
<div className="">{message.errorMessage}</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(!message.errorCode && !message.errorMessage && message.error) && (
|
||||
<>
|
||||
<div className="font-medium">Error</div>
|
||||
<div className="text-destructive">{message.error || 'Unknown error'}</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="font-medium">Recipient</div>
|
||||
<div>{message.recipient || (message.recipients && message.recipients[0]) || 'Unknown'}</div>
|
||||
|
||||
<div className="font-medium">Message ID</div>
|
||||
<div className="font-mono text-xs">{message._id}</div>
|
||||
|
||||
{message.smsBatch && (
|
||||
<>
|
||||
<div className="font-medium">Batch ID</div>
|
||||
<div className="font-mono text-xs">{message.smsBatch}</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[550px] p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-lg font-semibold">
|
||||
<MessageSquare className="h-5 w-5 text-brand-500" />
|
||||
SMS Details
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detailed information about this SMS message.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4 space-y-4 text-sm">
|
||||
{/* Info Grid */}
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
|
||||
<div className="font-medium text-muted-foreground">Direction</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{isSent ? <ArrowUpRight className="h-4 w-4 text-brand-500" /> :
|
||||
<ArrowDownLeft className="h-4 w-4 text-green-500" />}
|
||||
<span className="capitalize">{isSent ? 'Sent' : 'Received'}</span>
|
||||
</div>
|
||||
|
||||
<div className="font-medium text-muted-foreground">Number</div>
|
||||
<div>
|
||||
{isSent ? message.recipient || message.recipients?.[0] || 'Unknown'
|
||||
: message.sender || 'Unknown'}
|
||||
</div>
|
||||
|
||||
<div className="font-medium text-muted-foreground">Status</div>
|
||||
<div>
|
||||
<Badge variant="outline" className={`${statusBadge.color} flex items-center text-xs`}>
|
||||
{statusBadge.icon}
|
||||
{statusBadge.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="font-medium text-muted-foreground">Date & Time</div>
|
||||
<div>{formatTimestamp(isSent ? message.requestedAt : message.receivedAt)}</div>
|
||||
|
||||
<div className="font-medium text-muted-foreground">Device</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Smartphone className="h-3 w-3" />
|
||||
{message.device?.brand || 'N/A'} {message.device?.model || ''}
|
||||
</div>
|
||||
|
||||
{message.gatewayMessageId && (
|
||||
<>
|
||||
<div className="font-medium text-muted-foreground">Gateway ID</div>
|
||||
<div className="font-mono text-xs break-all">{message.gatewayMessageId}</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{message.errorMessage && (
|
||||
<>
|
||||
<div className="font-medium text-muted-foreground">Error</div>
|
||||
<div className="text-destructive text-sm">{message.errorMessage}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message Body */}
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h4 className="font-medium text-sm text-muted-foreground mb-1">Message Body</h4>
|
||||
<div className="max-h-48 overflow-y-auto p-2 bg-gray-50 dark:bg-gray-900 rounded-md text-sm break-words">
|
||||
{message.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-2 mt-4 pt-2 border-t border-border">
|
||||
{!isSent && (
|
||||
<Button variant="ghost" size="sm" className="gap-1" onClick={handleReplyClick}>
|
||||
<Reply className="h-4 w-4" />
|
||||
Reply
|
||||
</Button>
|
||||
)}
|
||||
{isSent && (
|
||||
<Button variant="ghost" size="sm" className="gap-1" onClick={handleFollowUpClick}>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Follow Up
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" className="gap-1" onClick={handleCopyMessage}>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
</Button>
|
||||
{/* Optional Delete */}
|
||||
{/* <Button variant="ghost" size="sm" className="gap-1 text-destructive hover:bg-destructive/10">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{message && isReplyOpen && (
|
||||
<ReplyDialog sms={message} open={isReplyOpen} onOpenChange={handleReplyDialogClose} />
|
||||
)}
|
||||
{message && isFollowUpOpen && (
|
||||
<FollowUpDialog message={message} open={isFollowUpOpen} onOpenChange={handleFollowUpDialogClose} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function MessageCard({ message, type, device }) {
|
||||
function MessageCard({ message, type, device, onSelectMessage }) {
|
||||
const isSent = type === 'sent'
|
||||
|
||||
const formattedDate = new Date(
|
||||
const formattedDate = formatTimestamp(
|
||||
(isSent ? message.requestedAt : message.receivedAt) || message.createdAt
|
||||
).toLocaleString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})
|
||||
)
|
||||
const statusBadge = getStatusBadge(message.status)
|
||||
|
||||
const shouldShowStatus = device?.appVersionCode >= 14 && new Date(message?.createdAt) > new Date('2025-06-05')
|
||||
// Condition to show status badge based on device app version and message date
|
||||
const shouldShowStatus = device?.appVersionCode >= 14 && new Date(message?.createdAt) > new Date('2025-06-05')
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`hover:bg-muted/50 transition-colors max-w-sm md:max-w-none ${
|
||||
isSent
|
||||
? 'border-l-4 border-l-brand-500'
|
||||
: 'border-l-4 border-l-green-500'
|
||||
className={`hover:bg-muted/50 transition-colors cursor-pointer max-w-sm md:max-w-none ${
|
||||
isSent ? 'border-l-4 border-l-brand-500' : 'border-l-4 border-l-green-500'
|
||||
}`}
|
||||
onClick={() => onSelectMessage(message)}
|
||||
>
|
||||
<CardContent className='p-4'>
|
||||
<div className='space-y-3'>
|
||||
@@ -584,20 +597,16 @@ function MessageCard({ message, type, device }) {
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<p className='text-sm max-w-sm md:max-w-none'>{message.message}</p>
|
||||
<p className='text-sm max-w-sm md:max-w-none line-clamp-2'>{message.message}</p>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center'>
|
||||
{isSent && shouldShowStatus && (
|
||||
<div className='flex items-center'>
|
||||
<StatusDetailsDialog message={message} />
|
||||
</div>
|
||||
<Badge variant='outline' className={`${statusBadge.color} flex items-center text-xs`}>
|
||||
{statusBadge.icon}
|
||||
{statusBadge.label}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<div className='flex justify-end ml-auto'>
|
||||
{!isSent && <ReplyDialog sms={message} />}
|
||||
{isSent && <FollowUpDialog message={message} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -622,33 +631,37 @@ function MessageCardSkeleton() {
|
||||
}
|
||||
|
||||
export default function MessageHistory() {
|
||||
const [selectedMessage, setSelectedMessage] = useState(null)
|
||||
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false)
|
||||
|
||||
const handleSelectMessage = (message: any) => {
|
||||
setSelectedMessage(message)
|
||||
setIsDetailsDialogOpen(true)
|
||||
}
|
||||
|
||||
const {
|
||||
data: devices,
|
||||
isLoading: isLoadingDevices,
|
||||
error: devicesError,
|
||||
} = useQuery({
|
||||
queryKey: ['devices'],
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(ApiEndpoints.gateway.listDevices())
|
||||
.then((res) => res.data),
|
||||
queryFn: () => httpBrowserClient.get(ApiEndpoints.gateway.listDevices()).then((res) => res.data),
|
||||
})
|
||||
|
||||
const [currentDevice, setCurrentDevice] = useState('')
|
||||
const [messageType, setMessageType] = useState('all')
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [autoRefreshInterval, setAutoRefreshInterval] = useState(0) // 0 means no auto-refresh
|
||||
const [autoRefreshInterval, setAutoRefreshInterval] = useState(0)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const refreshTimerRef = useRef(null)
|
||||
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (devices?.data?.length) {
|
||||
setCurrentDevice(devices?.data?.[0]?._id)
|
||||
if (devices?.data?.length && !currentDevice) {
|
||||
setCurrentDevice(devices.data[0]._id)
|
||||
}
|
||||
}, [devices])
|
||||
}, [devices, currentDevice])
|
||||
|
||||
// Query for messages with type filter
|
||||
const {
|
||||
data: messagesResponse,
|
||||
isLoading: isLoadingMessages,
|
||||
@@ -667,34 +680,27 @@ export default function MessageHistory() {
|
||||
.then((res) => res.data),
|
||||
})
|
||||
|
||||
// Handle manual refresh
|
||||
const handleRefresh = async () => {
|
||||
if (!currentDevice) return // Don't refresh if no device is selected
|
||||
|
||||
if (!currentDevice) return
|
||||
setIsRefreshing(true)
|
||||
await refetch()
|
||||
setTimeout(() => setIsRefreshing(false), 500) // Show refresh animation for at least 500ms
|
||||
setTimeout(() => setIsRefreshing(false), 500)
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -711,17 +717,17 @@ export default function MessageHistory() {
|
||||
totalPages: 1,
|
||||
}
|
||||
|
||||
const handleDeviceChange = (deviceId) => {
|
||||
const handleDeviceChange = (deviceId: string) => {
|
||||
setCurrentDevice(deviceId)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const handleMessageTypeChange = (type) => {
|
||||
const handleMessageTypeChange = (type: string) => {
|
||||
setMessageType(type)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const handlePageChange = (newPage) => {
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
|
||||
@@ -766,7 +772,7 @@ export default function MessageHistory() {
|
||||
<SelectValue placeholder='Select a device' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{devices?.data?.map((device) => (
|
||||
{devices?.data?.map((device: any) => (
|
||||
<SelectItem key={device._id} value={device._id}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium'>
|
||||
@@ -825,7 +831,6 @@ export default function MessageHistory() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refresh Controls */}
|
||||
<div className='flex items-center justify-between gap-2 pt-2 mt-2 border-t border-brand-100 dark:border-brand-800/50'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Button
|
||||
@@ -842,18 +847,11 @@ export default function MessageHistory() {
|
||||
/>
|
||||
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-brand-500' />
|
||||
<span className='text-xs font-medium mr-1'>Auto Refresh:</span>
|
||||
|
||||
<div className='flex'>
|
||||
{[
|
||||
{ value: 0, label: 'Off' },
|
||||
@@ -896,19 +894,20 @@ export default function MessageHistory() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingDevices && !messages && (
|
||||
{!isLoadingMessages && messages.length === 0 && (
|
||||
<div className='flex justify-center items-center h-full py-10'>
|
||||
No messages found
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='space-y-4'>
|
||||
{messages?.map((message) => (
|
||||
{messages?.map((message: any) => (
|
||||
<MessageCard
|
||||
key={message._id}
|
||||
message={message}
|
||||
type={message.sender ? 'received' : 'sent'}
|
||||
device={devices?.data?.find((device) => device._id === currentDevice)}
|
||||
device={devices?.data?.find((device: any) => device._id === currentDevice)}
|
||||
onSelectMessage={handleSelectMessage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -924,7 +923,6 @@ export default function MessageHistory() {
|
||||
</Button>
|
||||
|
||||
<div className='flex flex-wrap items-center gap-2 justify-center sm:justify-start'>
|
||||
{/* First page */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<Button
|
||||
onClick={() => handlePageChange(1)}
|
||||
@@ -940,32 +938,24 @@ export default function MessageHistory() {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Ellipsis if needed */}
|
||||
{page > 4 && pagination.totalPages > 7 && (
|
||||
<span className='px-1'>...</span>
|
||||
)}
|
||||
|
||||
{/* Middle pages */}
|
||||
{Array.from(
|
||||
{ length: Math.min(6, pagination.totalPages - 2) },
|
||||
(_, i) => {
|
||||
let pageToShow
|
||||
|
||||
if (pagination.totalPages <= 8) {
|
||||
// If we have 8 or fewer pages, show pages 2 through 7 (or fewer)
|
||||
pageToShow = i + 2
|
||||
} else if (page <= 4) {
|
||||
// Near the start
|
||||
pageToShow = i + 2
|
||||
} else if (page >= pagination.totalPages - 3) {
|
||||
// Near the end
|
||||
pageToShow = pagination.totalPages - 7 + i
|
||||
} else {
|
||||
// Middle - center around current page
|
||||
pageToShow = page - 2 + i
|
||||
}
|
||||
|
||||
// Ensure page is within bounds and not the first or last page
|
||||
if (pageToShow > 1 && pageToShow < pagination.totalPages) {
|
||||
return (
|
||||
<Button
|
||||
@@ -987,12 +977,10 @@ export default function MessageHistory() {
|
||||
}
|
||||
)}
|
||||
|
||||
{/* Ellipsis if needed */}
|
||||
{page < pagination.totalPages - 3 && pagination.totalPages > 7 && (
|
||||
<span className='px-1'>...</span>
|
||||
)}
|
||||
|
||||
{/* Last page */}
|
||||
{pagination.totalPages > 1 && (
|
||||
<Button
|
||||
onClick={() => handlePageChange(pagination.totalPages)}
|
||||
@@ -1008,11 +996,8 @@ export default function MessageHistory() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
handlePageChange(Math.min(pagination.totalPages, page + 1))
|
||||
}
|
||||
onClick={() => handlePageChange(Math.min(pagination.totalPages, page + 1))}
|
||||
disabled={page === pagination.totalPages}
|
||||
variant={page === pagination.totalPages ? 'ghost' : 'default'}
|
||||
>
|
||||
@@ -1020,6 +1005,13 @@ export default function MessageHistory() {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{selectedMessage && (
|
||||
<SmsDetailsDialog
|
||||
message={selectedMessage}
|
||||
open={isDetailsDialogOpen}
|
||||
onOpenChange={setIsDetailsDialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
518
web/app/(app)/dashboard/(components)/webhooks-history.tsx
Normal file
518
web/app/(app)/dashboard/(components)/webhooks-history.tsx
Normal file
@@ -0,0 +1,518 @@
|
||||
'use client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { ApiEndpoints } from '@/config/api'
|
||||
import httpBrowserClient from '@/lib/httpBrowserClient'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
ArrowDownLeft,
|
||||
ArrowUpRight,
|
||||
Clock,
|
||||
Ellipsis,
|
||||
MessageSquare,
|
||||
Smartphone,
|
||||
} from 'lucide-react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import ProductClient from '../webhooks/(components)/webhook-table'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Calendar } from 'lucide-react'
|
||||
import { truncate } from 'fs'
|
||||
|
||||
const WebhooksHistory = () => {
|
||||
const {
|
||||
data: devices,
|
||||
isLoading: isLoadingDevices,
|
||||
error: devicesError,
|
||||
} = useQuery({
|
||||
queryKey: ['devices'],
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(ApiEndpoints.gateway.listDevices())
|
||||
.then((res) => res.data),
|
||||
})
|
||||
|
||||
const [currentDevice, setCurrentDevice] = useState('all')
|
||||
const [eventType, setEventType] = useState('all')
|
||||
const [status, setStatus] = useState('all')
|
||||
const [dateRange, setDateRange] = useState<any>('90')
|
||||
const [openCal, setOpenCal] = useState(false)
|
||||
const [dateQuery, setDateQuery] = useState<{ start: string; end: string }>({
|
||||
start: '',
|
||||
end: '',
|
||||
})
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(10)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (devices?.data?.length && currentDevice === 'all') {
|
||||
}
|
||||
}, [devices, currentDevice])
|
||||
|
||||
const {
|
||||
data: webhookNotifications,
|
||||
isLoading: isLoadingNotifications,
|
||||
error: webhookNotificationsError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [
|
||||
'webhook-notification',
|
||||
eventType,
|
||||
page,
|
||||
limit,
|
||||
currentDevice,
|
||||
status,
|
||||
],
|
||||
enabled: true,
|
||||
queryFn: () =>
|
||||
httpBrowserClient
|
||||
.get(
|
||||
`${ApiEndpoints.gateway.getWebhookNotifications()}?eventType=${eventType === 'all' ? '' : eventType}&page=${page}&limit=${limit}&status=${status === 'all' ? '' : status}&start=${
|
||||
dateQuery.start
|
||||
}&end=${dateQuery.end}&deviceId=${
|
||||
currentDevice === 'all' ? '' : currentDevice
|
||||
}`
|
||||
)
|
||||
.then((res) => res.data),
|
||||
})
|
||||
|
||||
const totalPages = webhookNotifications?.data?.meta?.totalPages
|
||||
const handlePageChange = (currentPage: number) => {
|
||||
setPage(currentPage)
|
||||
}
|
||||
const handleMessageTypeChange = (type: string) => {
|
||||
setEventType(type)
|
||||
setPage(1)
|
||||
}
|
||||
const handleStatusChange = (status: string) => {
|
||||
setStatus(status)
|
||||
setPage(1)
|
||||
}
|
||||
const handleDateRangeChange = (range: string) => {
|
||||
const end = new Date() // today
|
||||
const start = new Date()
|
||||
|
||||
switch (range) {
|
||||
case '7':
|
||||
start.setDate(end.getDate() - 7)
|
||||
break
|
||||
case '30':
|
||||
start.setDate(end.getDate() - 30)
|
||||
break
|
||||
case '90':
|
||||
start.setDate(end.getDate() - 90)
|
||||
break
|
||||
case '90_months':
|
||||
start.setMonth(end.getMonth() - 3)
|
||||
break
|
||||
case '180':
|
||||
start.setMonth(end.getMonth() - 6)
|
||||
break
|
||||
case '365':
|
||||
start.setMonth(end.getMonth() - 12)
|
||||
break
|
||||
case 'custom':
|
||||
setDateQuery({ start: '', end: '' })
|
||||
setDateRange('custom')
|
||||
setOpenCal(true)
|
||||
return
|
||||
}
|
||||
|
||||
setDateQuery({ start: start.toISOString(), end: end.toISOString() })
|
||||
setDateRange(range)
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const handleDeviceChange = (deviceId: string) => {
|
||||
setCurrentDevice(deviceId)
|
||||
setPage(1)
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="bg-gradient-to-r from-brand-50 to-sky-50 dark:from-brand-950/30 dark:to-sky-950/30 rounded-lg shadow-sm border border-brand-100 dark:border-brand-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="">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Smartphone className="h-3.5 w-3.5 text-brand-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-brand-200 dark:border-brand-800/70">
|
||||
<SelectValue placeholder="Select a device" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem key="all" value="all">
|
||||
All devices
|
||||
</SelectItem>
|
||||
{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 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-brand-500" />
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Event Type
|
||||
</h3>
|
||||
</div>
|
||||
<Select value={eventType} onValueChange={handleMessageTypeChange}>
|
||||
<SelectTrigger className="w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-brand-200 dark:border-brand-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" />
|
||||
All Events
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="MESSAGE_RECEIVED">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
MESSAGE_RECEIVED
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="SMS_STATUS_UPDATED">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-brand-500" />
|
||||
SMS_STATUS_UPDATED
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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-brand-500" />
|
||||
<h3 className="text-sm font-medium text-foreground">Status</h3>
|
||||
</div>
|
||||
{/* status(delivered, pending, failed, retrying) */}
|
||||
<Select value={status} onValueChange={handleStatusChange}>
|
||||
<SelectTrigger className="w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-brand-200 dark:border-brand-800/70">
|
||||
<SelectValue placeholder="Status" />
|
||||
</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" />
|
||||
All
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="delivered">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
Delivered
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="pending">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-brand-500" />
|
||||
Pending
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="failed">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-red-500" />
|
||||
Failed
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="retrying">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-gray-500" />
|
||||
Retrying
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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-brand-500" />
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Date Range
|
||||
</h3>{' '}
|
||||
</div>
|
||||
<Select value={dateRange} onValueChange={handleDateRangeChange}>
|
||||
<SelectTrigger className="w-full bg-white/80 dark:bg-black/20 h-9 text-sm border-brand-200 dark:border-brand-800/70">
|
||||
<SelectValue placeholder="Date Range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7">
|
||||
<div className="flex items-center gap-1.5">Last 7 Days</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="30">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 30 Days
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="90">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 90 Days
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="90_months">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 3 Months
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="180">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 6 Months
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="365">
|
||||
<div className="flex items-center gap-1.5">
|
||||
Last 12 Months
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
<div className="flex items-center gap-1.5">Custom</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popover open={openCal} onOpenChange={setOpenCal}>
|
||||
<PopoverContent className="p-4 w-64">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs mb-1"
|
||||
htmlFor="start-date"
|
||||
>
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
id="start-date"
|
||||
type="date"
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
value={dateQuery.start ? dateQuery.start.split('T')[0] : ''}
|
||||
onChange={(e) =>
|
||||
setDateQuery({
|
||||
...dateQuery,
|
||||
start: e.target.value ? new Date(e.target.value).toISOString() : '',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="block text-xs mb-1"
|
||||
htmlFor="end-date"
|
||||
>
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
id="end-date"
|
||||
type="date"
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
value={dateQuery.end ? dateQuery.end.split('T')[0] : ''}
|
||||
onChange={(e) =>
|
||||
setDateQuery({
|
||||
...dateQuery,
|
||||
end: e.target.value ? new Date(e.target.value).toISOString() : '',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpenCal(false)
|
||||
setDateRange('90')
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(end.getDate() - 90)
|
||||
setDateQuery({ start: start.toISOString(), end: end.toISOString() })
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setPage(1)
|
||||
setOpenCal(false)
|
||||
refetch()
|
||||
}}
|
||||
disabled={!dateQuery.start || !dateQuery.end}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{isLoadingNotifications || isLoading ? (
|
||||
<ProductClient data={[]} isLoading={true} />
|
||||
) : (
|
||||
<ProductClient
|
||||
data={webhookNotifications?.data?.data || []}
|
||||
isLoading={false}
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
<div className="flex justify-end gap-x-3">
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
<Button
|
||||
onClick={() => handlePageChange(Math.max(1, page - 1))}
|
||||
disabled={page === 1 || isLoading || isLoadingNotifications}
|
||||
variant={page === 1 ? 'ghost' : 'default'}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 justify-center sm:justify-start">
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Button
|
||||
onClick={() => handlePageChange(1)}
|
||||
variant={page === 1 ? 'default' : 'ghost'}
|
||||
size="icon"
|
||||
className={`h-8 w-8 rounded-full ${
|
||||
page === 1
|
||||
? 'bg-primary text-brand-foreground hover:bg-primary/90'
|
||||
: 'hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
1
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Ellipsis if needed */}
|
||||
{page > 4 && totalPages > 7 && (
|
||||
<span className="px-1">...</span>
|
||||
)}
|
||||
|
||||
|
||||
{Array.from(
|
||||
{
|
||||
length: Math.min(6, totalPages - 2),
|
||||
},
|
||||
(_, i) => {
|
||||
let pageToShow: number
|
||||
|
||||
if (totalPages <= 8) {
|
||||
// If we have 8 or fewer pages, show pages 2 through 7 (or fewer)
|
||||
pageToShow = i + 2
|
||||
} else if (page <= 4) {
|
||||
// Near the start
|
||||
pageToShow = i + 2
|
||||
} else if (page >= totalPages - 3) {
|
||||
// Near the end
|
||||
pageToShow = totalPages - 7 + i
|
||||
} else {
|
||||
// Middle - center around current page
|
||||
pageToShow = page - 2 + i
|
||||
}
|
||||
|
||||
// Ensure page is within bounds and not the first or last page
|
||||
if (pageToShow > 1 && pageToShow < totalPages) {
|
||||
return (
|
||||
<Button
|
||||
key={pageToShow}
|
||||
onClick={() => handlePageChange(pageToShow)}
|
||||
variant={page === pageToShow ? 'default' : 'ghost'}
|
||||
size="icon"
|
||||
className={`h-8 w-8 rounded-full ${
|
||||
page === pageToShow
|
||||
? 'bg-primary text-brand-foreground hover:bg-primary/90'
|
||||
: 'hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
{pageToShow}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
)}
|
||||
|
||||
{/* Ellipsis if needed */}
|
||||
{page < totalPages - 3 && totalPages > 7 && (
|
||||
<span className="px-1">...</span>
|
||||
)}
|
||||
|
||||
{/* Last page */}
|
||||
{totalPages > 1 && (
|
||||
<Button
|
||||
onClick={() => handlePageChange(totalPages)}
|
||||
variant={page === totalPages ? 'default' : 'ghost'}
|
||||
size="icon"
|
||||
className={`h-8 w-8 rounded-full ${
|
||||
page === totalPages
|
||||
? 'bg-primary text-brand-foreground hover:bg-primary/90'
|
||||
: 'hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
{totalPages}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
handlePageChange(Math.min(totalPages, page + 1))
|
||||
}
|
||||
disabled={
|
||||
page === totalPages || isLoading || isLoadingNotifications
|
||||
}
|
||||
variant={page === totalPages ? 'ghost' : 'default'}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebhooksHistory
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PlusCircle, Webhook } from 'lucide-react'
|
||||
import { Bell, PlusCircle, Webhook } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { WebhookData } from '@/lib/types'
|
||||
import { WebhookCard } from './webhook-card'
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
function WebhookCardSkeleton() {
|
||||
return (
|
||||
@@ -57,8 +59,9 @@ function WebhookCardSkeleton() {
|
||||
export default function WebhooksSection() {
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const navigator = useRouter()
|
||||
const [selectedWebhook, setSelectedWebhook] = useState<WebhookData | null>(
|
||||
null
|
||||
null,
|
||||
)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -95,31 +98,41 @@ export default function WebhooksSection() {
|
||||
Manage webhook notifications for your SMS events
|
||||
</p>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleCreateClick}
|
||||
disabled={webhooks?.data?.length > 0 || isLoading}
|
||||
variant='default'
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
<PlusCircle className='mr-2 h-4 w-4' />
|
||||
Create Webhook
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{webhooks?.data?.length > 0 && (
|
||||
<TooltipContent>
|
||||
<p>
|
||||
You already have an active webhook subscription. You can edit
|
||||
or manage the existing webhook instead.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className='flex gap-x-4'>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleCreateClick}
|
||||
disabled={webhooks?.data?.length > 0 || isLoading}
|
||||
variant='default'
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
<PlusCircle className='mr-2 h-4 w-4' />
|
||||
Create Webhook
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{webhooks?.data?.length > 0 && (
|
||||
<TooltipContent>
|
||||
<p>
|
||||
You already have an active webhook subscription. You can
|
||||
edit or manage the existing webhook instead.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
onClick={() => navigator.push('/dashboard/webhooks')}
|
||||
variant='default'
|
||||
className='w-full sm:w-auto'
|
||||
>
|
||||
<Bell className='mr-2 h-4 w-4' />
|
||||
Notification Deliveries
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-8'>
|
||||
|
||||
148
web/app/(app)/dashboard/webhooks/(components)/data-table.tsx
Normal file
148
web/app/(app)/dashboard/webhooks/(components)/data-table.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
type ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
} from '@tanstack/react-table'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import React from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { WebhookPayloadModal } from './webhook-payload-modal' // Import your modal
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
searchKey: string
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
searchKey,
|
||||
isLoading,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
)
|
||||
const [selectedSms, setSelectedSms] = React.useState<any>(null)
|
||||
const [selectedPayload, setSelectedPayload] = React.useState<any>(null)
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false)
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
columnFilters,
|
||||
},
|
||||
})
|
||||
|
||||
const handleRowClick = (row: any) => {
|
||||
// Assuming your row data has smsData property
|
||||
if (row.original.smsData) {
|
||||
setSelectedSms(row.original.smsData)
|
||||
setSelectedPayload(row.original.payload)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false)
|
||||
setSelectedSms(null)
|
||||
setSelectedPayload(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
{isLoading ? (
|
||||
<TableBody>
|
||||
{Array.from({ length: 10 }, (_, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell colSpan={6}>
|
||||
<Skeleton className="w-full h-8" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
) : (
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
onClick={() => handleRowClick(row)}
|
||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="h-16">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-36 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
)}
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Webhook Payload Modal */}
|
||||
<WebhookPayloadModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
smsData={selectedSms}
|
||||
payload={selectedPayload}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Copy,
|
||||
} from 'lucide-react'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
|
||||
interface SmsData {
|
||||
_id: string
|
||||
createdAt: string
|
||||
device: string
|
||||
encrypted: boolean
|
||||
message: string
|
||||
receivedAt: string
|
||||
sender: string
|
||||
status: string
|
||||
type: string
|
||||
updatedAt: string
|
||||
__v: number
|
||||
}
|
||||
|
||||
interface WebhookPayloadModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
smsData: SmsData | null
|
||||
payload?: any
|
||||
}
|
||||
|
||||
export function WebhookPayloadModal({ isOpen, onClose, smsData, payload }: WebhookPayloadModalProps) {
|
||||
const { toast } = useToast()
|
||||
|
||||
if (!smsData) return null
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
const copyPayloadToClipboard = async () => {
|
||||
if (!payload) return
|
||||
try {
|
||||
const jsonString = JSON.stringify(payload, null, 2)
|
||||
await navigator.clipboard.writeText(jsonString)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Payload copied to clipboard",
|
||||
duration: 2000,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to copy payload:', err)
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = JSON.stringify(payload, null, 2)
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Payload copied to clipboard",
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Webhook Notification
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{payload && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Payload</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyPayloadToClipboard}
|
||||
className="h-8"
|
||||
>
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-md border p-4 overflow-auto max-h-64">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap text-gray-800 dark:text-gray-200">
|
||||
{JSON.stringify(payload, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Sender Information */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm flex items-center gap-2">
|
||||
Sender
|
||||
</h3>
|
||||
<p className="text-sm bg-muted/30 rounded-md p-2">
|
||||
{smsData.sender}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Message Type */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">Message Type</h3>
|
||||
<p className="text-sm bg-muted/30 rounded-md p-2 capitalize">
|
||||
{smsData.type.toLowerCase()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm">Status</h3>
|
||||
<p className="text-sm bg-muted/30 rounded-md p-2 capitalize">
|
||||
{smsData.status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm flex items-center gap-x-2">
|
||||
Created At
|
||||
</h3>
|
||||
<p className="text-sm bg-muted/30 rounded-md p-2 capitalize">
|
||||
{new Date(smsData.createdAt).toLocaleDateString('en-GB')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
106
web/app/(app)/dashboard/webhooks/(components)/webhook-table.tsx
Normal file
106
web/app/(app)/dashboard/webhooks/(components)/webhook-table.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DataTable } from './data-table'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { format } from 'date-fns'
|
||||
import { Eye } from 'lucide-react'
|
||||
|
||||
interface ProductClientProps {
|
||||
data: ProductColumns[]
|
||||
}
|
||||
|
||||
export type ProductColumns = {
|
||||
event?: string
|
||||
deviceName?: string
|
||||
webhookEvent?: string
|
||||
deliveryUrl?: string
|
||||
webhookSubscription?: {
|
||||
deliveryUrl: string
|
||||
}
|
||||
createdAt?: string
|
||||
status: string
|
||||
computedStatus?: string
|
||||
payload?: any
|
||||
}
|
||||
|
||||
|
||||
export const columns: ColumnDef<ProductColumns>[] = [
|
||||
{
|
||||
accessorKey: 'event',
|
||||
header: 'Event',
|
||||
},
|
||||
{
|
||||
accessorKey: 'deviceName',
|
||||
header: 'Device',
|
||||
cell: ({ row }) => {
|
||||
const deviceName = row.original.deviceName
|
||||
// If deviceName is an array with two lines
|
||||
if (Array.isArray(deviceName) && deviceName.length === 2) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{deviceName[0]}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{deviceName[1]}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Fallback for single line
|
||||
return <span>{deviceName}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
},
|
||||
{
|
||||
accessorKey: 'webhookSubscriptionData.deliveryUrl',
|
||||
header: 'Delivery Url',
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Created At',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<Button variant="ghost" size="sm" disabled={!row.original.payload}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
const formatDate = (dateString: string) => {
|
||||
return format(new Date(dateString), 'MMM dd, yyyy h:mm a')
|
||||
}
|
||||
const ProductClient = ({ data, isLoading, status = 'delivered' }) => {
|
||||
const { storeId } = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
const formatted = data.map((d) => ({
|
||||
...d,
|
||||
deviceName: [
|
||||
`${d.deviceData.brand} ${d.deviceData.model}`,
|
||||
` ${d.smsData._id}`,
|
||||
],
|
||||
createdAt: formatDate(d.createdAt.toString()),
|
||||
status: d.computedStatus || status,
|
||||
payload: d.payload,
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
searchKey="event"
|
||||
columns={columns}
|
||||
data={formatted}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductClient
|
||||
23
web/app/(app)/dashboard/webhooks/page.tsx
Normal file
23
web/app/(app)/dashboard/webhooks/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { MessageSquareTextIcon } from 'lucide-react'
|
||||
import WebhooksHistory from '../(components)/webhooks-history'
|
||||
|
||||
export default function MessagingPage() {
|
||||
return (
|
||||
<div className="flex-1 p-6 md:p-8">
|
||||
<div className="space-y-1 mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageSquareTextIcon className="h-6 w-6 text-primary" />
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
Webhook Notification Delivery History
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<WebhooksHistory />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
213
web/components/ui/calendar.tsx
Normal file
213
web/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"relative flex flex-col gap-4 md:flex-row",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"bg-popover absolute inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"w-[--cell-size] select-none",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-muted-foreground select-none text-[0.8rem]",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"bg-accent rounded-l-md",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-[--cell-size] items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
33
web/components/ui/popover.tsx
Normal file
33
web/components/ui/popover.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
120
web/components/ui/table.tsx
Normal file
120
web/components/ui/table.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export const ApiEndpoints = {
|
||||
getMessages: (id: string) => `/gateway/devices/${id}/messages`,
|
||||
|
||||
getWebhooks: () => '/webhooks',
|
||||
getWebhookNotifications: () => '/webhooks/notifications',
|
||||
createWebhook: () => '/webhooks',
|
||||
updateWebhook: (id: string) => `/webhooks/${id}`,
|
||||
getStats: () => '/gateway/stats',
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
@@ -31,9 +33,11 @@
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@sentry/nextjs": "^9",
|
||||
"@tanstack/react-query": "^5.61.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"axios": "^1.8.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.453.0",
|
||||
"next": "14.2.26",
|
||||
"next-auth": "^4.24.10",
|
||||
@@ -41,6 +45,7 @@
|
||||
"papaparse": "^5.4.1",
|
||||
"prisma": "^5.22.0",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^9.9.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-hook-form": "^7.53.1",
|
||||
|
||||
416
web/pnpm-lock.yaml
generated
416
web/pnpm-lock.yaml
generated
@@ -41,12 +41,18 @@ importers:
|
||||
'@radix-ui/react-navigation-menu':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-scroll-area':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.1.7
|
||||
version: 1.1.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -71,6 +77,9 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.61.0
|
||||
version: 5.61.0(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
axios:
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.2
|
||||
@@ -80,6 +89,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
lucide-react:
|
||||
specifier: ^0.453.0
|
||||
version: 0.453.0(react@18.2.0)
|
||||
@@ -101,6 +113,9 @@ importers:
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
react-day-picker:
|
||||
specifier: ^9.9.0
|
||||
version: 9.9.0(react@18.2.0)
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
@@ -247,6 +262,9 @@ packages:
|
||||
resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
|
||||
|
||||
@@ -771,6 +789,9 @@ packages:
|
||||
'@radix-ui/primitive@1.1.2':
|
||||
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
||||
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
'@radix-ui/react-accordion@1.2.1':
|
||||
resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==}
|
||||
peerDependencies:
|
||||
@@ -823,6 +844,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7':
|
||||
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-avatar@1.1.1':
|
||||
resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==}
|
||||
peerDependencies:
|
||||
@@ -990,6 +1024,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.11':
|
||||
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.3':
|
||||
resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==}
|
||||
peerDependencies:
|
||||
@@ -1025,6 +1072,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.3':
|
||||
resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.0':
|
||||
resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==}
|
||||
peerDependencies:
|
||||
@@ -1051,6 +1107,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.7':
|
||||
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-icons@1.3.0':
|
||||
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
|
||||
peerDependencies:
|
||||
@@ -1113,6 +1182,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popover@1.1.15':
|
||||
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popper@1.2.0':
|
||||
resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==}
|
||||
peerDependencies:
|
||||
@@ -1139,6 +1221,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-popper@1.2.8':
|
||||
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-portal@1.1.2':
|
||||
resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==}
|
||||
peerDependencies:
|
||||
@@ -1165,6 +1260,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-portal@1.1.9':
|
||||
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-presence@1.1.1':
|
||||
resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
|
||||
peerDependencies:
|
||||
@@ -1204,6 +1312,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-presence@1.1.5':
|
||||
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.0.0':
|
||||
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
||||
peerDependencies:
|
||||
@@ -1282,6 +1403,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.7':
|
||||
resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.1.0':
|
||||
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
|
||||
peerDependencies:
|
||||
@@ -1370,6 +1504,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.1.0':
|
||||
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
|
||||
peerDependencies:
|
||||
@@ -1406,6 +1549,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.1.1':
|
||||
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.0':
|
||||
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
|
||||
peerDependencies:
|
||||
@@ -1442,6 +1594,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1':
|
||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-size@1.1.0':
|
||||
resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
|
||||
peerDependencies:
|
||||
@@ -1451,6 +1612,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1':
|
||||
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.1.0':
|
||||
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
||||
peerDependencies:
|
||||
@@ -1480,6 +1650,9 @@ packages:
|
||||
'@radix-ui/rect@1.1.0':
|
||||
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
||||
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@react-oauth/google@0.12.1':
|
||||
resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==}
|
||||
peerDependencies:
|
||||
@@ -1636,6 +1809,17 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-table@8.21.3':
|
||||
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
'@tanstack/table-core@8.21.3':
|
||||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@types/connect@3.4.36':
|
||||
resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==}
|
||||
|
||||
@@ -1889,6 +2073,10 @@ packages:
|
||||
resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
aria-query@5.3.0:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
|
||||
@@ -2101,6 +2289,12 @@ packages:
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
date-fns-jalali@4.1.0-0:
|
||||
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@@ -3239,6 +3433,12 @@ packages:
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
react-day-picker@9.9.0:
|
||||
resolution: {integrity: sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
react-dom@18.2.0:
|
||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||
peerDependencies:
|
||||
@@ -3308,6 +3508,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-remove-scroll@2.7.1:
|
||||
resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-style-singleton@2.2.1:
|
||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3712,6 +3922,16 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sidecar@1.1.3:
|
||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -3919,6 +4139,8 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -4467,6 +4689,8 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.1.2': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-accordion@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
@@ -4516,6 +4740,15 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-arrow@1.1.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-avatar@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -4687,6 +4920,19 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@@ -4721,6 +4967,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-focus-guards@1.1.3(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -4743,6 +4995,17 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-icons@1.3.0(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -4818,6 +5081,29 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-popover@1.1.15(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-focus-guards': 1.1.3(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-popper': 1.2.8(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.2.48)(react@18.2.0)
|
||||
aria-hidden: 1.2.6
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-remove-scroll: 2.7.1(@types/react@18.2.48)(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-popper@1.2.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -4854,6 +5140,24 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-popper@1.2.8(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-use-rect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-use-size': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/rect': 1.1.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-portal@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -4874,6 +5178,16 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-portal@1.1.9(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-presence@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -4904,6 +5218,16 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-presence@1.1.5(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.2.48)(react@18.2.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -4994,6 +5318,15 @@ snapshots:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-separator@1.1.7(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
'@types/react-dom': 18.2.18
|
||||
|
||||
'@radix-ui/react-slot@1.1.0(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -5092,6 +5425,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -5121,6 +5460,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -5146,6 +5492,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.1(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.1
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-size@1.1.0(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0)
|
||||
@@ -5153,6 +5506,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-use-size@1.1.1(@types/react@18.2.48)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.48)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -5173,6 +5533,8 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.0': {}
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@react-oauth/google@0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
@@ -5396,6 +5758,14 @@ snapshots:
|
||||
'@tanstack/query-core': 5.60.6
|
||||
react: 18.2.0
|
||||
|
||||
'@tanstack/react-table@8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@tanstack/table-core': 8.21.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@tanstack/table-core@8.21.3': {}
|
||||
|
||||
'@types/connect@3.4.36':
|
||||
dependencies:
|
||||
'@types/node': 20.11.5
|
||||
@@ -5700,6 +6070,10 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
aria-query@5.3.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
@@ -5930,6 +6304,10 @@ snapshots:
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
date-fns-jalali@4.1.0-0: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -6103,7 +6481,7 @@ snapshots:
|
||||
'@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
|
||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0)
|
||||
eslint-plugin-react: 7.33.2(eslint@8.56.0)
|
||||
@@ -6122,12 +6500,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0):
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0):
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
enhanced-resolve: 5.15.0
|
||||
eslint: 8.56.0
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.2
|
||||
@@ -6139,14 +6517,14 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0):
|
||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6160,7 +6538,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.56.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
|
||||
hasown: 2.0.0
|
||||
is-core-module: 2.13.1
|
||||
is-glob: 4.0.3
|
||||
@@ -7152,6 +7530,13 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
react-day-picker@9.9.0(react@18.2.0):
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.4.1
|
||||
date-fns: 4.1.0
|
||||
date-fns-jalali: 4.1.0-0
|
||||
react: 18.2.0
|
||||
|
||||
react-dom@18.2.0(react@18.2.0):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -7215,6 +7600,17 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
react-remove-scroll@2.7.1(@types/react@18.2.48)(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-remove-scroll-bar: 2.3.8(@types/react@18.2.48)(react@18.2.0)
|
||||
react-style-singleton: 2.2.3(@types/react@18.2.48)(react@18.2.0)
|
||||
tslib: 2.8.1
|
||||
use-callback-ref: 1.3.3(@types/react@18.2.48)(react@18.2.0)
|
||||
use-sidecar: 1.1.3(@types/react@18.2.48)(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
react-style-singleton@2.2.1(@types/react@18.2.48)(react@18.2.0):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
@@ -7685,6 +8081,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
use-sidecar@1.1.3(@types/react@18.2.48)(react@18.2.0):
|
||||
dependencies:
|
||||
detect-node-es: 1.1.0
|
||||
react: 18.2.0
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.48
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@11.0.3: {}
|
||||
|
||||
Reference in New Issue
Block a user