feat: implement rating summary service methods and integrate rating display components into the organization header and pending ratings page
This commit is contained in:
@@ -1,10 +1,33 @@
|
||||
import { Document, Model, Schema, Types, model, models } from "mongoose";
|
||||
import { FindRatingsParams, IRating, IRatingsAdapter, RATING_COMMENT_MAX_LENGTH, RatingTargetType } from "./Ratings.Interface";
|
||||
import getAvatar from "../../helpers/getAvatar";
|
||||
import { joinStrings } from "../../helpers/String";
|
||||
import {
|
||||
FindRatingsParams,
|
||||
IRating,
|
||||
IRatingsAdapter,
|
||||
RATING_COMMENT_MAX_LENGTH,
|
||||
RatingTargetSummaryParams,
|
||||
RatingTargetSummaryResult,
|
||||
RatingTargetReviewItem,
|
||||
RatingTargetReviewsParams,
|
||||
RatingTargetType,
|
||||
} from "./Ratings.Interface";
|
||||
|
||||
export interface IRatingDocument extends Omit<IRating, "id">, Document {
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
type RatingReviewDocument = IRatingDocument & {
|
||||
userId?: {
|
||||
_id: Types.ObjectId;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const GENERIC_REVIEW_AUTHOR = "Cliente verificado";
|
||||
|
||||
export class RatingsAdapterMongoose implements IRatingsAdapter {
|
||||
schema: Schema;
|
||||
ratingsList: Model<IRatingDocument>;
|
||||
@@ -44,6 +67,97 @@ export class RatingsAdapterMongoose implements IRatingsAdapter {
|
||||
return rating ? this.mapRating(rating) : null;
|
||||
}
|
||||
|
||||
public async count(filters: FindRatingsParams): Promise<number> {
|
||||
return this.ratingsList.countDocuments(filters).exec();
|
||||
}
|
||||
|
||||
public async summaryByTarget(filters: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult> {
|
||||
const [summary] = await this.ratingsList
|
||||
.aggregate<{ averageScore: number; totalCount: number }>([
|
||||
{
|
||||
$match: {
|
||||
targetType: filters.targetType,
|
||||
targetId: new Types.ObjectId(filters.targetId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
averageScore: { $avg: "$score" },
|
||||
totalCount: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
])
|
||||
.exec();
|
||||
|
||||
return {
|
||||
averageScore: summary?.averageScore || 0,
|
||||
totalCount: summary?.totalCount || 0,
|
||||
};
|
||||
}
|
||||
|
||||
public async summaryByTargets(filters: { targetType: RatingTargetType; targetIds: string[] }): Promise<RatingTargetSummaryResult> {
|
||||
const targetIds = filters.targetIds.filter((targetId) => Types.ObjectId.isValid(targetId));
|
||||
|
||||
if (targetIds.length === 0) {
|
||||
return { averageScore: 0, totalCount: 0 };
|
||||
}
|
||||
|
||||
const [summary] = await this.ratingsList
|
||||
.aggregate<{ averageScore: number; totalCount: number }>([
|
||||
{
|
||||
$match: {
|
||||
targetType: filters.targetType,
|
||||
targetId: { $in: targetIds.map((targetId) => new Types.ObjectId(targetId)) },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
averageScore: { $avg: "$score" },
|
||||
totalCount: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
])
|
||||
.exec();
|
||||
|
||||
return {
|
||||
averageScore: summary?.averageScore || 0,
|
||||
totalCount: summary?.totalCount || 0,
|
||||
};
|
||||
}
|
||||
|
||||
public async reviewsByTarget(filters: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]> {
|
||||
const ratings = await this.ratingsList
|
||||
.find({
|
||||
targetType: filters.targetType,
|
||||
targetId: new Types.ObjectId(filters.targetId),
|
||||
comment: { $regex: /\S/ },
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(filters.limit || 20)
|
||||
.select("score comment createdAt userId")
|
||||
.populate("userId", "firstName lastName avatar")
|
||||
.exec();
|
||||
|
||||
return (ratings as unknown as RatingReviewDocument[]).map((rating) => {
|
||||
const user = rating.userId;
|
||||
const displayName = user ? joinStrings([user.firstName, user.lastName], " ") : "";
|
||||
const authorName = displayName || GENERIC_REVIEW_AUTHOR;
|
||||
|
||||
return {
|
||||
id: rating._id.toString(),
|
||||
score: rating.score,
|
||||
comment: rating.comment || "",
|
||||
createdAt: rating.createdAt,
|
||||
author: {
|
||||
displayName: authorName,
|
||||
avatarUrl: user ? getAvatar(user._id.toString(), user.avatar, "") : "",
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private mapRating(rating: IRatingDocument): IRating {
|
||||
return {
|
||||
id: rating._id.toString(),
|
||||
|
||||
@@ -30,6 +30,43 @@ export type PendingRatingsByUserParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type CountRatingsByUserParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type CountRatingsByUserResult = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type ProfessionalRatingSummaryByUserParams = {
|
||||
sessionUser: string;
|
||||
};
|
||||
|
||||
export type RatingTargetSummaryParams = {
|
||||
targetType: RatingTargetType;
|
||||
targetId: string;
|
||||
};
|
||||
|
||||
export type RatingTargetSummaryResult = {
|
||||
averageScore: number;
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export type RatingTargetReviewsParams = RatingTargetSummaryParams & {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type RatingTargetReviewItem = {
|
||||
id: string;
|
||||
score: number;
|
||||
comment: string;
|
||||
createdAt: Date;
|
||||
author: {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface IRating {
|
||||
id?: string;
|
||||
appointmentId: string;
|
||||
@@ -61,10 +98,18 @@ export interface IRatingsAdapter {
|
||||
create(data: Omit<IRating, "id" | "createdAt" | "updatedAt">): Promise<IRating>;
|
||||
find(filters: FindRatingsParams): Promise<IRating[]>;
|
||||
findOne(filters: FindRatingsParams): Promise<IRating | null>;
|
||||
count(filters: FindRatingsParams): Promise<number>;
|
||||
summaryByTarget(filters: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult>;
|
||||
summaryByTargets(filters: { targetType: RatingTargetType; targetIds: string[] }): Promise<RatingTargetSummaryResult>;
|
||||
reviewsByTarget(filters: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]>;
|
||||
}
|
||||
|
||||
export interface IRatingsManager {
|
||||
ratings: IRatingsAdapter;
|
||||
createRating(data: CreateRatingParams): Promise<IRating>;
|
||||
pendingByUser(data: PendingRatingsByUserParams): Promise<PendingRatingItem[]>;
|
||||
countByUser(data: CountRatingsByUserParams): Promise<CountRatingsByUserResult>;
|
||||
professionalSummaryByUser(data: ProfessionalRatingSummaryByUserParams): Promise<RatingTargetSummaryResult>;
|
||||
summaryByTarget(data: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult>;
|
||||
reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]>;
|
||||
}
|
||||
|
||||
@@ -9,16 +9,24 @@ import ServicesManager from "../Services/Service";
|
||||
import UsersManager from "../Users/Users";
|
||||
import { RatingsAdapterMongoose } from "./Ratings.Adapter.Mongoose";
|
||||
import {
|
||||
CountRatingsByUserParams,
|
||||
CountRatingsByUserResult,
|
||||
CreateRatingParams,
|
||||
IRating,
|
||||
IRatingsManager,
|
||||
PendingRatingItem,
|
||||
PendingRatingsByUserParams,
|
||||
ProfessionalRatingSummaryByUserParams,
|
||||
RATING_COMMENT_MAX_LENGTH,
|
||||
RatingTargetSummaryParams,
|
||||
RatingTargetSummaryResult,
|
||||
RatingTargetReviewItem,
|
||||
RatingTargetReviewsParams,
|
||||
RatingTargetType,
|
||||
} from "./Ratings.Interface";
|
||||
|
||||
const PENDING_RATINGS_APPOINTMENT_LIMIT = 50;
|
||||
const TARGET_REVIEWS_LIMIT = 20;
|
||||
|
||||
class RatingsManager implements IRatingsManager {
|
||||
ratings: RatingsAdapterMongoose;
|
||||
@@ -180,6 +188,52 @@ class RatingsManager implements IRatingsManager {
|
||||
return pendingItems;
|
||||
}
|
||||
|
||||
public async countByUser(data: CountRatingsByUserParams): Promise<CountRatingsByUserResult> {
|
||||
await this.validateSessionUser(data.sessionUser);
|
||||
|
||||
return {
|
||||
count: await this.ratings.count({ userId: data.sessionUser }),
|
||||
};
|
||||
}
|
||||
|
||||
public async professionalSummaryByUser(data: ProfessionalRatingSummaryByUserParams): Promise<RatingTargetSummaryResult> {
|
||||
await this.validateSessionUser(data.sessionUser);
|
||||
|
||||
const employees = await EmployeesList.employees.find({
|
||||
userId: data.sessionUser,
|
||||
removed: { $ne: true },
|
||||
} as any);
|
||||
const employeeIds = employees.map((employee) => String(employee.id));
|
||||
|
||||
return this.ratings.summaryByTargets({
|
||||
targetType: RatingTargetType.EMPLOYEE,
|
||||
targetIds: employeeIds,
|
||||
});
|
||||
}
|
||||
|
||||
public async summaryByTarget(data: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult> {
|
||||
this.validateTargetType(data.targetType);
|
||||
|
||||
if (!Types.ObjectId.isValid(data.targetId)) {
|
||||
throw new Error("No se ha proporcionado un objetivo válido.");
|
||||
}
|
||||
|
||||
return this.ratings.summaryByTarget(data);
|
||||
}
|
||||
|
||||
public async reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]> {
|
||||
this.validateTargetType(data.targetType);
|
||||
|
||||
if (!Types.ObjectId.isValid(data.targetId)) {
|
||||
throw new Error("No se ha proporcionado un objetivo válido.");
|
||||
}
|
||||
|
||||
return this.ratings.reviewsByTarget({
|
||||
...data,
|
||||
limit: Math.min(Math.max(data.limit || TARGET_REVIEWS_LIMIT, 1), TARGET_REVIEWS_LIMIT),
|
||||
});
|
||||
}
|
||||
|
||||
private async validateSessionUser(sessionUser: string): Promise<void> {
|
||||
const user = await UsersManager.users.findOne({ _id: sessionUser });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user