api for operator get actvity
This commit is contained in:
@@ -88,3 +88,21 @@ operatorVerifyPassword:
|
||||
- httpApi:
|
||||
path: /verify-password
|
||||
method: post
|
||||
|
||||
operatorGetActivitiesByDate:
|
||||
handler: src/modules/host/handlers/operator/getActivitiesByDate.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/operator/**'
|
||||
- 'src/modules/host/services/operatorActivity.service.ts'
|
||||
- 'src/modules/host/dto/operator.activity.dto.ts'
|
||||
- 'src/common/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /activities-by-date
|
||||
method: get
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import httpStatus from 'http-status';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
import { ROLE } from '@/common/utils/constants/common.constant';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import httpStatus from 'http-status';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import config from '../../../config/config';
|
||||
import { prisma } from '../../database/prisma.client';
|
||||
import ApiError from '../../utils/helper/ApiError';
|
||||
|
||||
interface DecodedToken {
|
||||
id?: number;
|
||||
@@ -29,6 +29,68 @@ declare module 'express-serve-static-core' {
|
||||
* Core authentication function - verifies JWT and validates Host user
|
||||
* Can be used by both Express middleware and Lambda handlers
|
||||
*/
|
||||
/**
|
||||
* Verifies JWT and validates Operator user (role_xid = 5)
|
||||
*/
|
||||
export async function verifyOperatorToken(token: string): Promise<{ id: number; role?: string }> {
|
||||
if (!token) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.jwt.secret) as unknown as DecodedToken;
|
||||
|
||||
const userId = decoded.id ?? (decoded.sub ? Number(decoded.sub) : null);
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Invalid token payload');
|
||||
}
|
||||
|
||||
// ✅ Fetch user from Prisma (Operator user only)
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
const latestToken = await prisma.token.findFirst({
|
||||
where: {
|
||||
userXid: userId
|
||||
},
|
||||
orderBy: { id: 'desc' }
|
||||
})
|
||||
|
||||
if (latestToken?.isBlackListed == true) {
|
||||
throw new ApiError(401, "This session is expired. Please login.")
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'User not found');
|
||||
}
|
||||
|
||||
// ✅ Check if user is active
|
||||
if (user.isActive === false) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Your account is deactivated by admin.');
|
||||
}
|
||||
|
||||
// ✅ Check Operator role (role_xid = 5)
|
||||
if (user.roleXid !== ROLE.OPERATOR) {
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Access denied.');
|
||||
}
|
||||
|
||||
return { id: user.id, role: user.role?.roleName };
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Your session has expired. Please log in again.');
|
||||
}
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApiError(httpStatus.FORBIDDEN, 'Invalid or expired authentication token.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyHostToken(token: string): Promise<{ id: number; role?: string }> {
|
||||
if (!token) {
|
||||
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
|
||||
|
||||
26
src/modules/host/dto/operator.activity.dto.ts
Normal file
26
src/modules/host/dto/operator.activity.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export class GetActivitiesByDateRequestDTO {
|
||||
activityDate?: string; // ISO date format: YYYY-MM-DD (optional, defaults to today)
|
||||
}
|
||||
|
||||
export class DateBreakdownDTO {
|
||||
date: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class ActivitySummaryDTO {
|
||||
activityName: string;
|
||||
activityImage: string | null;
|
||||
activityImagePreSignedUrl: string | null;
|
||||
count: number;
|
||||
dateBreakdown: DateBreakdownDTO[]; // Booking count for each scheduled date
|
||||
}
|
||||
|
||||
export class GetActivitiesByDateResponseDTO {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
date: string;
|
||||
activities: ActivitySummaryDTO[];
|
||||
totalCount: number;
|
||||
};
|
||||
}
|
||||
59
src/modules/host/handlers/operator/getActivitiesByDate.ts
Normal file
59
src/modules/host/handlers/operator/getActivitiesByDate.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { GetActivitiesByDateRequestDTO } from '../../dto/operator.activity.dto';
|
||||
import { OperatorActivityService } from '../../services/operatorActivity.service';
|
||||
|
||||
const operatorActivityService = new OperatorActivityService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
try {
|
||||
// Extract token from headers
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) {
|
||||
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
|
||||
// Verify token and get operator info
|
||||
const operatorInfo = await verifyOperatorToken(token);
|
||||
const operatorId = Number(operatorInfo.id);
|
||||
|
||||
if (!operatorId || isNaN(operatorId)) {
|
||||
throw new ApiError(400, 'Invalid operator ID');
|
||||
}
|
||||
|
||||
// Get activityDate from query parameters
|
||||
const { activityDate } = event.queryStringParameters || {};
|
||||
|
||||
const requestDTO: GetActivitiesByDateRequestDTO = {
|
||||
activityDate: activityDate?.trim(),
|
||||
};
|
||||
|
||||
// Fetch activities by date and operator
|
||||
const result = await operatorActivityService.getActivitiesByDate(
|
||||
operatorId,
|
||||
requestDTO.activityDate,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Activities fetched successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
// Error will be handled by safeHandler
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
274
src/modules/host/services/operatorActivity.service.ts
Normal file
274
src/modules/host/services/operatorActivity.service.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import config from '../../../config/config';
|
||||
import { ActivitySummaryDTO } from '../dto/operator.activity.dto';
|
||||
|
||||
@Injectable()
|
||||
export class OperatorActivityService {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async getActivitiesByDate(
|
||||
operatorId: number,
|
||||
activityDate?: string,
|
||||
): Promise<{
|
||||
date: string;
|
||||
activities: ActivitySummaryDTO[];
|
||||
totalCount: number;
|
||||
}> {
|
||||
try {
|
||||
// Get operator's assigned hosts
|
||||
const hostMembers = await this.prisma.hostMembers.findMany({
|
||||
where: {
|
||||
userXid: operatorId,
|
||||
isActive: true,
|
||||
memberStatus: 'accepted', // Only accepted memberships
|
||||
},
|
||||
select: {
|
||||
hostXid: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (hostMembers.length === 0) {
|
||||
return {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
activities: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const hostXids = hostMembers.map((m) => m.hostXid);
|
||||
|
||||
// Use today's date if not provided
|
||||
const queryDate = activityDate
|
||||
? new Date(activityDate)
|
||||
: new Date();
|
||||
|
||||
// Validate date format
|
||||
if (isNaN(queryDate.getTime())) {
|
||||
throw new ApiError(400, 'Invalid date format. Use YYYY-MM-DD format');
|
||||
}
|
||||
|
||||
// Set time to start of day (UTC)
|
||||
queryDate.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
// Set end of day
|
||||
const endOfDay = new Date(queryDate);
|
||||
endOfDay.setUTCHours(23, 59, 59, 999);
|
||||
|
||||
// Get all schedule occurrences from today onwards (not just query date)
|
||||
// This includes future dates to show all scheduled dates
|
||||
const allScheduleOccurrences = await this.prisma.scheduleOccurences.findMany({
|
||||
where: {
|
||||
occurenceDate: {
|
||||
gte: queryDate,
|
||||
},
|
||||
isActive: true,
|
||||
scheduleHeader: {
|
||||
activity: {
|
||||
hostXid: {
|
||||
in: hostXids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
scheduleHeader: {
|
||||
include: {
|
||||
activity: {
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
isActive: true,
|
||||
ActivitiesMedia: {
|
||||
where: {
|
||||
isCoverImage: true,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
mediaFileName: true,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
occurenceDate: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
if (allScheduleOccurrences.length === 0) {
|
||||
return {
|
||||
date: queryDate.toISOString().split('T')[0],
|
||||
activities: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Group activities by activity ID with all scheduled dates
|
||||
const activityMap = new Map<
|
||||
number,
|
||||
{
|
||||
activityTitle: string;
|
||||
coverImage: string | null;
|
||||
coverImageUrl: string | null;
|
||||
activityId: number;
|
||||
scheduledDates: Date[];
|
||||
}
|
||||
>();
|
||||
|
||||
// Process all schedule occurrences to build activity list with all scheduled dates
|
||||
for (const occurrence of allScheduleOccurrences) {
|
||||
const activity = occurrence.scheduleHeader.activity;
|
||||
|
||||
if (!activityMap.has(activity.id)) {
|
||||
let coverImage: string | null = null;
|
||||
let coverImageUrl: string | null = null;
|
||||
|
||||
if (activity.ActivitiesMedia.length > 0) {
|
||||
coverImage = activity.ActivitiesMedia[0].mediaFileName;
|
||||
try {
|
||||
// Generate presigned URL for the image
|
||||
coverImageUrl = await getPresignedUrl(
|
||||
config.aws.bucketName,
|
||||
coverImage,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to generate presigned URL for ${coverImage}:`,
|
||||
error,
|
||||
);
|
||||
coverImageUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
activityMap.set(activity.id, {
|
||||
activityTitle: activity.activityTitle || 'Unknown Activity',
|
||||
coverImage,
|
||||
coverImageUrl,
|
||||
activityId: activity.id,
|
||||
scheduledDates: [new Date(occurrence.occurenceDate)],
|
||||
});
|
||||
} else {
|
||||
// Add to scheduled dates if not already present
|
||||
const existingActivity = activityMap.get(activity.id)!;
|
||||
const occurrenceDateStr = new Date(occurrence.occurenceDate).toISOString().split('T')[0];
|
||||
const dateExists = existingActivity.scheduledDates.some(
|
||||
(d) => d.toISOString().split('T')[0] === occurrenceDateStr
|
||||
);
|
||||
if (!dateExists) {
|
||||
existingActivity.scheduledDates.push(new Date(occurrence.occurenceDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all unique scheduled dates for all activities
|
||||
const allScheduledDates = new Set<string>();
|
||||
activityMap.forEach((activity) => {
|
||||
activity.scheduledDates.forEach((date) => {
|
||||
allScheduledDates.add(date.toISOString().split('T')[0]);
|
||||
});
|
||||
});
|
||||
|
||||
// Get all activity IDs
|
||||
const activityIds = Array.from(activityMap.keys());
|
||||
|
||||
// Query all bookings for these activities from query date onwards
|
||||
const allBookings = await this.prisma.itineraryActivities.findMany({
|
||||
where: {
|
||||
activityXid: {
|
||||
in: activityIds,
|
||||
},
|
||||
occurenceDate: {
|
||||
gte: queryDate,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
itineraryHeaderXid: true,
|
||||
occurenceDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Count bookings by activity and date
|
||||
const bookingsByActivityAndDate = new Map<
|
||||
number,
|
||||
Map<string, Set<number>>
|
||||
>();
|
||||
|
||||
allBookings.forEach((booking) => {
|
||||
if (booking.activityXid) {
|
||||
if (!bookingsByActivityAndDate.has(booking.activityXid)) {
|
||||
bookingsByActivityAndDate.set(booking.activityXid, new Map());
|
||||
}
|
||||
|
||||
const dateStr = new Date(booking.occurenceDate)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
const dateMap = bookingsByActivityAndDate.get(booking.activityXid)!;
|
||||
|
||||
if (!dateMap.has(dateStr)) {
|
||||
dateMap.set(dateStr, new Set());
|
||||
}
|
||||
|
||||
dateMap.get(dateStr)!.add(booking.itineraryHeaderXid);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert map to array format with date breakdown
|
||||
const activities: ActivitySummaryDTO[] = Array.from(
|
||||
activityMap.values(),
|
||||
).map((activity) => {
|
||||
const activityBookings = bookingsByActivityAndDate.get(activity.activityId);
|
||||
|
||||
// Get bookings for each scheduled date
|
||||
const dateBreakdown = Array.from(allScheduledDates)
|
||||
.sort()
|
||||
.map((dateStr) => {
|
||||
const count = activityBookings?.get(dateStr)?.size || 0;
|
||||
return {
|
||||
date: dateStr,
|
||||
count,
|
||||
};
|
||||
});
|
||||
|
||||
// Count for the query date only
|
||||
const queryDateStr = queryDate.toISOString().split('T')[0];
|
||||
const countForQueryDate = activityBookings?.get(queryDateStr)?.size || 0;
|
||||
|
||||
return {
|
||||
activityName: activity.activityTitle,
|
||||
activityImage: activity.coverImage,
|
||||
activityImagePreSignedUrl: activity.coverImageUrl,
|
||||
count: countForQueryDate,
|
||||
dateBreakdown,
|
||||
};
|
||||
});
|
||||
|
||||
// Total count is bookings for the requested date only
|
||||
const queryDateStr = queryDate.toISOString().split('T')[0];
|
||||
const totalCount = allBookings.filter(
|
||||
(b) => new Date(b.occurenceDate).toISOString().split('T')[0] === queryDateStr
|
||||
).length;
|
||||
|
||||
return {
|
||||
date: queryDateStr,
|
||||
activities,
|
||||
totalCount,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiError(
|
||||
500,
|
||||
error instanceof Error ? error.message : 'Error fetching activities',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user