diff --git a/api/src/gateway/gateway.controller.ts b/api/src/gateway/gateway.controller.ts index 2d104ad..6bdf1e0 100644 --- a/api/src/gateway/gateway.controller.ts +++ b/api/src/gateway/gateway.controller.ts @@ -113,13 +113,20 @@ export class GatewayController { @ApiOperation({ summary: 'Get received SMS from a device' }) @ApiResponse({ status: 200, type: RetrieveSMSResponseDTO }) + @ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' }) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items per page (default: 50, max: 100)' }) @UseGuards(AuthGuard, CanModifyDevice) // deprecate getReceivedSMS route in favor of get-received-sms @Get(['/devices/:id/getReceivedSMS', '/devices/:id/get-received-sms']) async getReceivedSMS( @Param('id') deviceId: string, + @Request() req, ): Promise { - const data = await this.gatewayService.getReceivedSMS(deviceId) - return { data } + // Extract page and limit from query params, with defaults and max values + const page = req.query.page ? parseInt(req.query.page, 10) : 1; + const limit = req.query.limit ? Math.min(parseInt(req.query.limit, 10), 100) : 50; + + const result = await this.gatewayService.getReceivedSMS(deviceId, page, limit) + return result; } } diff --git a/api/src/gateway/gateway.dto.ts b/api/src/gateway/gateway.dto.ts index 6096ae7..dde4e23 100644 --- a/api/src/gateway/gateway.dto.ts +++ b/api/src/gateway/gateway.dto.ts @@ -195,6 +195,36 @@ export class RetrieveSMSDTO { updatedAt: Date } +export class PaginationMetaDTO { + @ApiProperty({ + type: Number, + required: true, + description: 'Current page number', + }) + page: number; + + @ApiProperty({ + type: Number, + required: true, + description: 'Number of items per page', + }) + limit: number; + + @ApiProperty({ + type: Number, + required: true, + description: 'Total number of items', + }) + total: number; + + @ApiProperty({ + type: Number, + required: true, + description: 'Total number of pages', + }) + totalPages: number; +} + export class RetrieveSMSResponseDTO { @ApiProperty({ type: [RetrieveSMSDTO], @@ -202,4 +232,11 @@ export class RetrieveSMSResponseDTO { description: 'The received SMS data', }) data: RetrieveSMSDTO[] + + @ApiProperty({ + type: PaginationMetaDTO, + required: true, + description: 'Pagination metadata', + }) + meta?: PaginationMetaDTO } diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index 87c763b..27bcb7a 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -437,7 +437,7 @@ export class GatewayService { return sms } - async getReceivedSMS(deviceId: string): Promise { + async getReceivedSMS(deviceId: string, page = 1, limit = 50): Promise<{ data: any[], meta: any }> { const device = await this.deviceModel.findById(deviceId) if (!device) { @@ -450,20 +450,47 @@ export class GatewayService { ) } + // Calculate skip value for pagination + const skip = (page - 1) * limit; + + // Get total count for pagination metadata + const total = await this.smsModel.countDocuments({ + device: device._id, + type: SMSType.RECEIVED, + }); + // @ts-ignore - return await this.smsModel + const data = await this.smsModel .find( { device: device._id, type: SMSType.RECEIVED, }, null, - { sort: { receivedAt: -1 }, limit: 200 }, + { + sort: { receivedAt: -1 }, + limit: limit, + skip: skip + }, ) .populate({ path: 'device', select: '_id brand model buildId enabled', }) + .lean() // Use lean() to return plain JavaScript objects instead of Mongoose documents + + // Calculate pagination metadata + const totalPages = Math.ceil(total / limit); + + return { + meta: { + page, + limit, + total, + totalPages, + }, + data, + }; } async getStatsForUser(user: User) { diff --git a/api/src/gateway/schemas/sms.schema.ts b/api/src/gateway/schemas/sms.schema.ts index 2600f4e..286c082 100644 --- a/api/src/gateway/schemas/sms.schema.ts +++ b/api/src/gateway/schemas/sms.schema.ts @@ -62,3 +62,6 @@ export class SMS { } export const SMSSchema = SchemaFactory.createForClass(SMS) + + +SMSSchema.index({ device: 1, type: 1, receivedAt: -1 }) diff --git a/web/app/(app)/dashboard/(components)/received-sms.tsx b/web/app/(app)/dashboard/(components)/received-sms.tsx index 3d4c5e6..aeac12f 100644 --- a/web/app/(app)/dashboard/(components)/received-sms.tsx +++ b/web/app/(app)/dashboard/(components)/received-sms.tsx @@ -4,8 +4,201 @@ import httpBrowserClient from '@/lib/httpBrowserClient' import { useQuery } from '@tanstack/react-query' import React, { useEffect, useState } from 'react' import { Card, CardContent } from '@/components/ui/card' -import { Clock } from 'lucide-react' +import { Clock, MessageSquare, Reply } from 'lucide-react' import { Skeleton } from '@/components/ui/skeleton' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +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 { Check, X } from 'lucide-react' + +function ReplyDialog({ sms, onClose }: { sms: any; onClose?: () => void }) { + const [open, setOpen] = useState(false) + + 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: () => { + setTimeout(() => { + setOpen(false) + if (onClose) onClose() + }, 1500) + }, + }) + + const { + register, + control, + handleSubmit, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(sendSmsSchema), + defaultValues: { + deviceId: sms?.device?._id, + recipients: [sms.sender], + message: '', + }, + }) + + const { data: devices, isLoading: isLoadingDevices } = useQuery({ + queryKey: ['devices'], + queryFn: () => + httpBrowserClient + .get(ApiEndpoints.gateway.listDevices()) + .then((res) => res.data), + }) + + useEffect(() => { + if (open) { + reset({ + deviceId: sms?.device?._id, + recipients: [sms.sender], + message: '', + }) + } + }, [open, sms, reset]) + + return ( + + + + + + + + + Reply to {sms.sender} + + + Send a reply message to this sender + + +
handleSubmit((data) => sendSms(data))(e)} + className='space-y-4 mt-4' + > +
+
+ ( + + )} + /> + {errors.deviceId && ( +

+ {errors.deviceId.message} +

+ )} +
+ +
+ + {errors.recipients?.[0] && ( +

+ {errors.recipients[0].message} +

+ )} +
+ +
+