feat: add public employee profile view and implement ratings review page components
This commit is contained in:
@@ -69,6 +69,7 @@ export class CompaniesAdapterMongoose implements ICompaniesAdapter {
|
|||||||
banned: { type: Boolean, required: false, default: false },
|
banned: { type: Boolean, required: false, default: false },
|
||||||
showPublicScores: { type: Boolean, required: false, default: true },
|
showPublicScores: { type: Boolean, required: false, default: true },
|
||||||
showPublicOpinions: { type: Boolean, required: false, default: true },
|
showPublicOpinions: { type: Boolean, required: false, default: true },
|
||||||
|
showPublicProfessionals: { type: Boolean, required: false, default: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.companyList = model<ICompanyDocument>("Company", this.schema);
|
this.companyList = model<ICompanyDocument>("Company", this.schema);
|
||||||
@@ -245,6 +246,10 @@ export class CompaniesAdapterMongoose implements ICompaniesAdapter {
|
|||||||
updateCompany.showPublicOpinions = data.showPublicOpinions;
|
updateCompany.showPublicOpinions = data.showPublicOpinions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.showPublicProfessionals !== undefined) {
|
||||||
|
updateCompany.showPublicProfessionals = data.showPublicProfessionals;
|
||||||
|
}
|
||||||
|
|
||||||
updateCompany.save();
|
updateCompany.save();
|
||||||
|
|
||||||
//actualizo los campos correspondientes a la localizacion de los servicios de la organizacion.
|
//actualizo los campos correspondientes a la localizacion de los servicios de la organizacion.
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export type UpdateCompanyParams = {
|
|||||||
banned?: boolean;
|
banned?: boolean;
|
||||||
showPublicScores?: boolean;
|
showPublicScores?: boolean;
|
||||||
showPublicOpinions?: boolean;
|
showPublicOpinions?: boolean;
|
||||||
|
showPublicProfessionals?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SetCompanyFileParams = {
|
export type SetCompanyFileParams = {
|
||||||
@@ -158,6 +159,7 @@ export interface ICompany {
|
|||||||
banned?: boolean;
|
banned?: boolean;
|
||||||
showPublicScores?: boolean;
|
showPublicScores?: boolean;
|
||||||
showPublicOpinions?: boolean;
|
showPublicOpinions?: boolean;
|
||||||
|
showPublicProfessionals?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MyOranizationsView {
|
export interface MyOranizationsView {
|
||||||
@@ -209,6 +211,7 @@ export interface MyOranizationsView {
|
|||||||
banned?: boolean;
|
banned?: boolean;
|
||||||
showPublicScores?: boolean;
|
showPublicScores?: boolean;
|
||||||
showPublicOpinions?: boolean;
|
showPublicOpinions?: boolean;
|
||||||
|
showPublicProfessionals?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientOrganizationView {
|
export interface ClientOrganizationView {
|
||||||
|
|||||||
@@ -500,7 +500,11 @@ class CompaniesManager implements ICompaniesManager {
|
|||||||
throw new Error(NoPermissionMessage());
|
throw new Error(NoPermissionMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.showPublicScores !== undefined || data.showPublicOpinions !== undefined) {
|
if (
|
||||||
|
data.showPublicScores !== undefined ||
|
||||||
|
data.showPublicOpinions !== undefined ||
|
||||||
|
data.showPublicProfessionals !== undefined
|
||||||
|
) {
|
||||||
await this.validatePublicRatingFlagsPlan(String(companyCheck.ownerId));
|
await this.validatePublicRatingFlagsPlan(String(companyCheck.ownerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,6 +618,7 @@ class CompaniesManager implements ICompaniesManager {
|
|||||||
onboardingCompleted: company.onboardingCompleted,
|
onboardingCompleted: company.onboardingCompleted,
|
||||||
showPublicScores: company.showPublicScores ?? true,
|
showPublicScores: company.showPublicScores ?? true,
|
||||||
showPublicOpinions: company.showPublicOpinions ?? true,
|
showPublicOpinions: company.showPublicOpinions ?? true,
|
||||||
|
showPublicProfessionals: company.showPublicProfessionals ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,6 +740,7 @@ class CompaniesManager implements ICompaniesManager {
|
|||||||
onboardingCompleted: company.onboardingCompleted,
|
onboardingCompleted: company.onboardingCompleted,
|
||||||
showPublicScores: company.showPublicScores ?? true,
|
showPublicScores: company.showPublicScores ?? true,
|
||||||
showPublicOpinions: company.showPublicOpinions ?? true,
|
showPublicOpinions: company.showPublicOpinions ?? true,
|
||||||
|
showPublicProfessionals: company.showPublicProfessionals ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,6 +813,7 @@ class CompaniesManager implements ICompaniesManager {
|
|||||||
onboardingCompleted: company.onboardingCompleted,
|
onboardingCompleted: company.onboardingCompleted,
|
||||||
showPublicScores: company.showPublicScores ?? true,
|
showPublicScores: company.showPublicScores ?? true,
|
||||||
showPublicOpinions: company.showPublicOpinions ?? true,
|
showPublicOpinions: company.showPublicOpinions ?? true,
|
||||||
|
showPublicProfessionals: company.showPublicProfessionals ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
PaginateEmployeesParams,
|
PaginateEmployeesParams,
|
||||||
PaginateEmployeesResults,
|
PaginateEmployeesResults,
|
||||||
CompanyEmployeesView,
|
CompanyEmployeesView,
|
||||||
|
PublicCompanyEmployeeView,
|
||||||
|
FindPublicEmployeeParams,
|
||||||
|
PublicEmployeeView,
|
||||||
FindEmployeesParams,
|
FindEmployeesParams,
|
||||||
FindEmployeesByIdParams,
|
FindEmployeesByIdParams,
|
||||||
UpdateEmployeeRolesParams,
|
UpdateEmployeeRolesParams,
|
||||||
@@ -442,6 +445,71 @@ class EmployeeManager implements IEmployeesManager {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findPublicByCompanyId(
|
||||||
|
data: FindEmployeesParams
|
||||||
|
): Promise<PublicCompanyEmployeeView[]> {
|
||||||
|
const employees = await this.employees.find({
|
||||||
|
companyId: data.companyId,
|
||||||
|
guestOk: true,
|
||||||
|
hostOk: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const view = await Promise.all(
|
||||||
|
employees.map(async (employee) => {
|
||||||
|
const userData = await UsersManager.users.findOne({ _id: employee.userId });
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strFirstName = isNull<string>(userData.firstName, "");
|
||||||
|
const strLastName = isNull<string>(userData.lastName, "");
|
||||||
|
const strFullName = joinStrings([strFirstName, strLastName], " ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
employeeId: isNull<string>(employee.id, ""),
|
||||||
|
userId: employee.userId,
|
||||||
|
firstName: strFirstName,
|
||||||
|
lastName: strLastName,
|
||||||
|
avatarUrl: getAvatar(userData.id, userData.avatar, strFullName),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return view.filter((employee): employee is PublicCompanyEmployeeView => employee !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findPublicByEmployeeId(data: FindPublicEmployeeParams): Promise<PublicEmployeeView> {
|
||||||
|
const employee = await this.employees.findOne({
|
||||||
|
_id: data.employeeId,
|
||||||
|
guestOk: true,
|
||||||
|
hostOk: true,
|
||||||
|
removed: { $ne: true },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
if (!employee) {
|
||||||
|
throw new Error("El colaborador no existe");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await UsersManager.users.findOne({ _id: employee.userId });
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
throw new Error("El colaborador no existe");
|
||||||
|
}
|
||||||
|
|
||||||
|
const strFirstName = isNull<string>(userData.firstName, "");
|
||||||
|
const strLastName = isNull<string>(userData.lastName, "");
|
||||||
|
const strFullName = joinStrings([strFirstName, strLastName], " ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
employeeId: isNull<string>(employee.id, ""),
|
||||||
|
firstName: strFirstName,
|
||||||
|
lastName: strLastName,
|
||||||
|
avatarUrl: getAvatar(userData.id, userData.avatar, strFullName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView> {
|
public async findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView> {
|
||||||
const employee = await this.employees.findOne({ _id: data.id });
|
const employee = await this.employees.findOne({ _id: data.id });
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,25 @@ export interface CompanyEmployeesView {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PublicCompanyEmployeeView {
|
||||||
|
employeeId: string;
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindPublicEmployeeParams {
|
||||||
|
employeeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicEmployeeView {
|
||||||
|
employeeId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IncompleteCollaboratorView {
|
export interface IncompleteCollaboratorView {
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
@@ -143,6 +162,8 @@ export interface IEmployeesManager {
|
|||||||
updateEmployeeRoles(data: UpdateEmployeeRolesParams): Promise<void>;
|
updateEmployeeRoles(data: UpdateEmployeeRolesParams): Promise<void>;
|
||||||
update(data: UpdateEmployeeParams): Promise<void>;
|
update(data: UpdateEmployeeParams): Promise<void>;
|
||||||
findByCompanyId(data: FindEmployeesParams): Promise<CompanyEmployeesView[]>;
|
findByCompanyId(data: FindEmployeesParams): Promise<CompanyEmployeesView[]>;
|
||||||
|
findPublicByCompanyId(data: FindEmployeesParams): Promise<PublicCompanyEmployeeView[]>;
|
||||||
|
findPublicByEmployeeId(data: FindPublicEmployeeParams): Promise<PublicEmployeeView>;
|
||||||
findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView>;
|
findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView>;
|
||||||
textObjectFilter(data: FindEmployeesParams): Promise<TextObjectFilterResult[]>;
|
textObjectFilter(data: FindEmployeesParams): Promise<TextObjectFilterResult[]>;
|
||||||
deleteEmployeesByCompany(data: DeleteEmployeesByCompany): Promise<void>;
|
deleteEmployeesByCompany(data: DeleteEmployeesByCompany): Promise<void>;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
RATING_COMMENT_MAX_LENGTH,
|
RATING_COMMENT_MAX_LENGTH,
|
||||||
RatingTargetSummaryParams,
|
RatingTargetSummaryParams,
|
||||||
RatingTargetSummaryResult,
|
RatingTargetSummaryResult,
|
||||||
RatingTargetReviewItem,
|
|
||||||
RatingTargetReviewsParams,
|
RatingTargetReviewsParams,
|
||||||
|
RatingTargetReviewsResult,
|
||||||
RatingTargetType,
|
RatingTargetType,
|
||||||
} from "./Ratings.Interface";
|
} from "./Ratings.Interface";
|
||||||
|
|
||||||
@@ -127,20 +127,26 @@ export class RatingsAdapterMongoose implements IRatingsAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reviewsByTarget(filters: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]> {
|
public async reviewsByTarget(filters: RatingTargetReviewsParams): Promise<RatingTargetReviewsResult> {
|
||||||
const ratings = await this.ratingsList
|
const page = filters.page || 1;
|
||||||
.find({
|
const pageSize = filters.pageSize || filters.limit || 20;
|
||||||
|
const targetReviewsFilter = {
|
||||||
targetType: filters.targetType,
|
targetType: filters.targetType,
|
||||||
targetId: new Types.ObjectId(filters.targetId),
|
targetId: new Types.ObjectId(filters.targetId),
|
||||||
comment: { $regex: /\S/ },
|
comment: { $regex: /\S/ },
|
||||||
})
|
};
|
||||||
|
|
||||||
|
const total = await this.ratingsList.countDocuments(targetReviewsFilter).exec();
|
||||||
|
const ratings = await this.ratingsList
|
||||||
|
.find(targetReviewsFilter)
|
||||||
.sort({ score: -1, createdAt: -1 })
|
.sort({ score: -1, createdAt: -1 })
|
||||||
.limit(filters.limit || 20)
|
.skip((page - 1) * pageSize)
|
||||||
|
.limit(pageSize)
|
||||||
.select("score comment createdAt userId")
|
.select("score comment createdAt userId")
|
||||||
.populate("userId", "firstName lastName avatar")
|
.populate("userId", "firstName lastName avatar")
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
return (ratings as unknown as RatingReviewDocument[]).map((rating) => {
|
const reviews = (ratings as unknown as RatingReviewDocument[]).map((rating) => {
|
||||||
const user = rating.userId;
|
const user = rating.userId;
|
||||||
const displayName = user ? joinStrings([user.firstName, user.lastName], " ") : "";
|
const displayName = user ? joinStrings([user.firstName, user.lastName], " ") : "";
|
||||||
const authorName = displayName || GENERIC_REVIEW_AUTHOR;
|
const authorName = displayName || GENERIC_REVIEW_AUTHOR;
|
||||||
@@ -156,6 +162,14 @@ export class RatingsAdapterMongoose implements IRatingsAdapter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
reviews,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages: Math.ceil(total / pageSize),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapRating(rating: IRatingDocument): IRating {
|
private mapRating(rating: IRatingDocument): IRating {
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export type RatingTargetSummaryResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RatingTargetReviewsParams = RatingTargetSummaryParams & {
|
export type RatingTargetReviewsParams = RatingTargetSummaryParams & {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +69,14 @@ export type RatingTargetReviewItem = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RatingTargetReviewsResult = {
|
||||||
|
reviews: RatingTargetReviewItem[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IRating {
|
export interface IRating {
|
||||||
id?: string;
|
id?: string;
|
||||||
appointmentId: string;
|
appointmentId: string;
|
||||||
@@ -101,7 +111,7 @@ export interface IRatingsAdapter {
|
|||||||
count(filters: FindRatingsParams): Promise<number>;
|
count(filters: FindRatingsParams): Promise<number>;
|
||||||
summaryByTarget(filters: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult>;
|
summaryByTarget(filters: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult>;
|
||||||
summaryByTargets(filters: { targetType: RatingTargetType; targetIds: string[] }): Promise<RatingTargetSummaryResult>;
|
summaryByTargets(filters: { targetType: RatingTargetType; targetIds: string[] }): Promise<RatingTargetSummaryResult>;
|
||||||
reviewsByTarget(filters: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]>;
|
reviewsByTarget(filters: RatingTargetReviewsParams): Promise<RatingTargetReviewsResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRatingsManager {
|
export interface IRatingsManager {
|
||||||
@@ -111,5 +121,5 @@ export interface IRatingsManager {
|
|||||||
countByUser(data: CountRatingsByUserParams): Promise<CountRatingsByUserResult>;
|
countByUser(data: CountRatingsByUserParams): Promise<CountRatingsByUserResult>;
|
||||||
professionalSummaryByUser(data: ProfessionalRatingSummaryByUserParams): Promise<RatingTargetSummaryResult>;
|
professionalSummaryByUser(data: ProfessionalRatingSummaryByUserParams): Promise<RatingTargetSummaryResult>;
|
||||||
summaryByTarget(data: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult>;
|
summaryByTarget(data: RatingTargetSummaryParams): Promise<RatingTargetSummaryResult>;
|
||||||
reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]>;
|
reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewsResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import {
|
|||||||
RATING_COMMENT_MAX_LENGTH,
|
RATING_COMMENT_MAX_LENGTH,
|
||||||
RatingTargetSummaryParams,
|
RatingTargetSummaryParams,
|
||||||
RatingTargetSummaryResult,
|
RatingTargetSummaryResult,
|
||||||
RatingTargetReviewItem,
|
|
||||||
RatingTargetReviewsParams,
|
RatingTargetReviewsParams,
|
||||||
|
RatingTargetReviewsResult,
|
||||||
RatingTargetType,
|
RatingTargetType,
|
||||||
} from "./Ratings.Interface";
|
} from "./Ratings.Interface";
|
||||||
|
|
||||||
@@ -221,16 +221,21 @@ class RatingsManager implements IRatingsManager {
|
|||||||
return this.ratings.summaryByTarget(data);
|
return this.ratings.summaryByTarget(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]> {
|
public async reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewsResult> {
|
||||||
this.validateTargetType(data.targetType);
|
this.validateTargetType(data.targetType);
|
||||||
|
|
||||||
if (!Types.ObjectId.isValid(data.targetId)) {
|
if (!Types.ObjectId.isValid(data.targetId)) {
|
||||||
throw new Error("No se ha proporcionado un objetivo válido.");
|
throw new Error("No se ha proporcionado un objetivo válido.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pageSize = Math.min(Math.max(data.pageSize || data.limit || TARGET_REVIEWS_LIMIT, 1), TARGET_REVIEWS_LIMIT);
|
||||||
|
const page = Math.max(data.page || 1, 1);
|
||||||
|
|
||||||
return this.ratings.reviewsByTarget({
|
return this.ratings.reviewsByTarget({
|
||||||
...data,
|
...data,
|
||||||
limit: Math.min(Math.max(data.limit || TARGET_REVIEWS_LIMIT, 1), TARGET_REVIEWS_LIMIT),
|
page,
|
||||||
|
pageSize,
|
||||||
|
limit: pageSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export type PublicOrganizationView = {
|
|||||||
appointmentAlert: string;
|
appointmentAlert: string;
|
||||||
showPublicScores: boolean;
|
showPublicScores: boolean;
|
||||||
showPublicOpinions: boolean;
|
showPublicOpinions: boolean;
|
||||||
|
showPublicProfessionals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PublicOrganizationServiceViewParams = {
|
export type PublicOrganizationServiceViewParams = {
|
||||||
@@ -167,6 +168,7 @@ class Views {
|
|||||||
appointmentAlert: isNull<string>(company.appointmentAlert, ""),
|
appointmentAlert: isNull<string>(company.appointmentAlert, ""),
|
||||||
showPublicScores: company.showPublicScores ?? true,
|
showPublicScores: company.showPublicScores ?? true,
|
||||||
showPublicOpinions: company.showPublicOpinions ?? true,
|
showPublicOpinions: company.showPublicOpinions ?? true,
|
||||||
|
showPublicProfessionals: company.showPublicProfessionals ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
PaginateEmployeesResults,
|
PaginateEmployeesResults,
|
||||||
ValidateEmployeeParams,
|
ValidateEmployeeParams,
|
||||||
CompanyEmployeesView,
|
CompanyEmployeesView,
|
||||||
|
PublicCompanyEmployeeView,
|
||||||
|
FindPublicEmployeeParams,
|
||||||
|
PublicEmployeeView,
|
||||||
FindEmployeesByIdParams,
|
FindEmployeesByIdParams,
|
||||||
UpdateEmployeeRolesParams,
|
UpdateEmployeeRolesParams,
|
||||||
UpdateEmployeeParams,
|
UpdateEmployeeParams,
|
||||||
@@ -117,6 +120,46 @@ export class GetEmployeesByCompanyController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Route("employees/public-by-company")
|
||||||
|
export class GetPublicEmployeesByCompanyController extends Controller {
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Post()
|
||||||
|
public async findPublicByCompanyId(
|
||||||
|
@Body() requestBody: FindEmployeesParams
|
||||||
|
): Promise<PublicCompanyEmployeeView[] | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const employees = await new EmployeesService().findPublicByCompanyId(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return employees;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route("employees/public-by-id")
|
||||||
|
export class GetPublicEmployeeByIdController extends Controller {
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Post()
|
||||||
|
public async findPublicByEmployeeId(
|
||||||
|
@Body() requestBody: FindPublicEmployeeParams
|
||||||
|
): Promise<PublicEmployeeView | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const employee = await new EmployeesService().findPublicByEmployeeId(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return employee;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Route("employees/get-by-id")
|
@Route("employees/get-by-id")
|
||||||
export class GetEmployeesByIdController extends Controller {
|
export class GetEmployeesByIdController extends Controller {
|
||||||
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
PaginateEmployeesParams,
|
PaginateEmployeesParams,
|
||||||
PaginateEmployeesResults,
|
PaginateEmployeesResults,
|
||||||
CompanyEmployeesView,
|
CompanyEmployeesView,
|
||||||
|
PublicCompanyEmployeeView,
|
||||||
|
FindPublicEmployeeParams,
|
||||||
|
PublicEmployeeView,
|
||||||
FindEmployeesByIdParams,
|
FindEmployeesByIdParams,
|
||||||
UpdateEmployeeRolesParams,
|
UpdateEmployeeRolesParams,
|
||||||
UpdateEmployeeParams,
|
UpdateEmployeeParams,
|
||||||
@@ -60,6 +63,24 @@ export class EmployeesService {
|
|||||||
return employees;
|
return employees;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findPublicByCompanyId(
|
||||||
|
data: FindEmployeesParams
|
||||||
|
): Promise<PublicCompanyEmployeeView[]> {
|
||||||
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
|
||||||
|
const employees = await EmployeesList.findPublicByCompanyId(data);
|
||||||
|
|
||||||
|
return employees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findPublicByEmployeeId(data: FindPublicEmployeeParams): Promise<PublicEmployeeView> {
|
||||||
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
|
||||||
|
const employee = await EmployeesList.findPublicByEmployeeId(data);
|
||||||
|
|
||||||
|
return employee;
|
||||||
|
}
|
||||||
|
|
||||||
public async findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView> {
|
public async findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView> {
|
||||||
await connect(`${process.env.DATABASE_CONNECTION}`);
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
PendingRatingItem,
|
PendingRatingItem,
|
||||||
PendingRatingsByUserParams,
|
PendingRatingsByUserParams,
|
||||||
ProfessionalRatingSummaryByUserParams,
|
ProfessionalRatingSummaryByUserParams,
|
||||||
RatingTargetReviewItem,
|
|
||||||
RatingTargetReviewsParams,
|
RatingTargetReviewsParams,
|
||||||
|
RatingTargetReviewsResult,
|
||||||
RatingTargetSummaryParams,
|
RatingTargetSummaryParams,
|
||||||
RatingTargetSummaryResult,
|
RatingTargetSummaryResult,
|
||||||
} from "../../Models/Ratings/Ratings.Interface";
|
} from "../../Models/Ratings/Ratings.Interface";
|
||||||
@@ -126,7 +126,7 @@ export class RatingTargetReviewsController extends Controller {
|
|||||||
@Post()
|
@Post()
|
||||||
public async reviewsByTarget(
|
public async reviewsByTarget(
|
||||||
@Body() requestBody: RatingTargetReviewsParams
|
@Body() requestBody: RatingTargetReviewsParams
|
||||||
): Promise<RatingTargetReviewItem[] | ApiValidationError> {
|
): Promise<RatingTargetReviewsResult | ApiValidationError> {
|
||||||
try {
|
try {
|
||||||
const ratingReviews = await new RatingsService().reviewsByTarget(requestBody);
|
const ratingReviews = await new RatingsService().reviewsByTarget(requestBody);
|
||||||
this.setStatus(200);
|
this.setStatus(200);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
ProfessionalRatingSummaryByUserParams,
|
ProfessionalRatingSummaryByUserParams,
|
||||||
RatingTargetSummaryParams,
|
RatingTargetSummaryParams,
|
||||||
RatingTargetSummaryResult,
|
RatingTargetSummaryResult,
|
||||||
RatingTargetReviewItem,
|
|
||||||
RatingTargetReviewsParams,
|
RatingTargetReviewsParams,
|
||||||
|
RatingTargetReviewsResult,
|
||||||
} from "../../Models/Ratings/Ratings.Interface";
|
} from "../../Models/Ratings/Ratings.Interface";
|
||||||
|
|
||||||
export class RatingsService {
|
export class RatingsService {
|
||||||
@@ -40,7 +40,7 @@ export class RatingsService {
|
|||||||
return await RatingsList.summaryByTarget(data);
|
return await RatingsList.summaryByTarget(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]> {
|
public async reviewsByTarget(data: RatingTargetReviewsParams): Promise<RatingTargetReviewsResult> {
|
||||||
await connect(`${process.env.DATABASE_CONNECTION}`);
|
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||||
return await RatingsList.reviewsByTarget(data);
|
return await RatingsList.reviewsByTarget(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface MyOranizationsView {
|
|||||||
onboardingCompleted?: boolean;
|
onboardingCompleted?: boolean;
|
||||||
showPublicScores: boolean;
|
showPublicScores: boolean;
|
||||||
showPublicOpinions: boolean;
|
showPublicOpinions: boolean;
|
||||||
|
showPublicProfessionals: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateCompanyParams = {
|
export type CreateCompanyParams = {
|
||||||
@@ -97,6 +98,7 @@ export type UpdateCompanyParams = {
|
|||||||
onboardingCompleted?: boolean;
|
onboardingCompleted?: boolean;
|
||||||
showPublicScores?: boolean;
|
showPublicScores?: boolean;
|
||||||
showPublicOpinions?: boolean;
|
showPublicOpinions?: boolean;
|
||||||
|
showPublicProfessionals?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteCompanyParams = {
|
export type DeleteCompanyParams = {
|
||||||
@@ -164,6 +166,7 @@ export interface ICompany {
|
|||||||
banned?: boolean;
|
banned?: boolean;
|
||||||
showPublicScores?: boolean;
|
showPublicScores?: boolean;
|
||||||
showPublicOpinions?: boolean;
|
showPublicOpinions?: boolean;
|
||||||
|
showPublicProfessionals?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PublicOrganizationViewParams = {
|
export type PublicOrganizationViewParams = {
|
||||||
@@ -192,6 +195,7 @@ export type PublicOrganizationView = {
|
|||||||
banned?: boolean;
|
banned?: boolean;
|
||||||
showPublicScores: boolean;
|
showPublicScores: boolean;
|
||||||
showPublicOpinions: boolean;
|
showPublicOpinions: boolean;
|
||||||
|
showPublicProfessionals: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SetNotificationAutoParams = {
|
export type SetNotificationAutoParams = {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export type PublicCompanyEmployeeView = {
|
||||||
|
employeeId: string;
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindPublicCompanyEmployeesParams = {
|
||||||
|
companyId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindPublicEmployeeParams = {
|
||||||
|
employeeId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublicEmployeeView = {
|
||||||
|
employeeId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
};
|
||||||
@@ -42,6 +42,8 @@ export type RatingTargetSummaryResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type RatingTargetReviewsParams = RatingTargetSummaryParams & {
|
export type RatingTargetReviewsParams = RatingTargetSummaryParams & {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,6 +58,14 @@ export type RatingTargetReviewItem = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RatingTargetReviewsResult = {
|
||||||
|
reviews: RatingTargetReviewItem[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface PendingRatingItem {
|
export interface PendingRatingItem {
|
||||||
appointmentId: string;
|
appointmentId: string;
|
||||||
appointmentDate: string;
|
appointmentDate: string;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ApiError } from "@core/Models/Server.Error.model";
|
||||||
|
import {
|
||||||
|
FindPublicCompanyEmployeesParams,
|
||||||
|
FindPublicEmployeeParams,
|
||||||
|
PublicCompanyEmployeeView,
|
||||||
|
PublicEmployeeView,
|
||||||
|
} from "@core/Models/Employees.model";
|
||||||
|
import ApiRequest from "@services/Api.Service";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
|
||||||
|
const schemaFindPublicCompanyEmployees = Yup.object().shape({
|
||||||
|
companyId: Yup.string().required("No se ha proporcionado la organización."),
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaFindPublicEmployee = Yup.object().shape({
|
||||||
|
employeeId: Yup.string().required("No se ha proporcionado el profesional."),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const findPublicCompanyEmployees = async (
|
||||||
|
data: FindPublicCompanyEmployeesParams
|
||||||
|
): Promise<PublicCompanyEmployeeView[]> => {
|
||||||
|
return new Promise<PublicCompanyEmployeeView[]>((resolve, reject) => {
|
||||||
|
schemaFindPublicCompanyEmployees
|
||||||
|
.validate(data, { abortEarly: true })
|
||||||
|
.then(() => {
|
||||||
|
resolve(ApiRequest.post<PublicCompanyEmployeeView[]>("employees/public-by-company", data));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(new ApiError(500, error.message));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findPublicEmployee = async (
|
||||||
|
data: FindPublicEmployeeParams
|
||||||
|
): Promise<PublicEmployeeView> => {
|
||||||
|
return new Promise<PublicEmployeeView>((resolve, reject) => {
|
||||||
|
schemaFindPublicEmployee
|
||||||
|
.validate(data, { abortEarly: true })
|
||||||
|
.then(() => {
|
||||||
|
resolve(ApiRequest.post<PublicEmployeeView>("employees/public-by-id", data));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(new ApiError(500, error.message));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
RATING_COMMENT_MAX_LENGTH,
|
RATING_COMMENT_MAX_LENGTH,
|
||||||
RatingTargetSummaryParams,
|
RatingTargetSummaryParams,
|
||||||
RatingTargetSummaryResult,
|
RatingTargetSummaryResult,
|
||||||
RatingTargetReviewItem,
|
|
||||||
RatingTargetReviewsParams,
|
RatingTargetReviewsParams,
|
||||||
|
RatingTargetReviewsResult,
|
||||||
RatingTargetType,
|
RatingTargetType,
|
||||||
} from "@core/Models/Ratings.model";
|
} from "@core/Models/Ratings.model";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
@@ -48,6 +48,8 @@ const schemaRatingTargetSummary = Yup.object().shape({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const schemaRatingTargetReviews = schemaRatingTargetSummary.shape({
|
const schemaRatingTargetReviews = schemaRatingTargetSummary.shape({
|
||||||
|
page: Yup.number().integer().min(1),
|
||||||
|
pageSize: Yup.number().integer().min(1).max(20),
|
||||||
limit: Yup.number().integer().min(1).max(20),
|
limit: Yup.number().integer().min(1).max(20),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,12 +120,12 @@ export const ratingTargetSummary = async (data: RatingTargetSummaryParams): Prom
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ratingTargetReviews = async (data: RatingTargetReviewsParams): Promise<RatingTargetReviewItem[]> => {
|
export const ratingTargetReviews = async (data: RatingTargetReviewsParams): Promise<RatingTargetReviewsResult> => {
|
||||||
return new Promise<RatingTargetReviewItem[]>((resolve, reject) => {
|
return new Promise<RatingTargetReviewsResult>((resolve, reject) => {
|
||||||
schemaRatingTargetReviews
|
schemaRatingTargetReviews
|
||||||
.validate(data, { abortEarly: true })
|
.validate(data, { abortEarly: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
resolve(ApiRequest.post<RatingTargetReviewItem[]>("ratings/target-reviews", data));
|
resolve(ApiRequest.post<RatingTargetReviewsResult>("ratings/target-reviews", data));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(new ApiError(500, error.message));
|
reject(new ApiError(500, error.message));
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export default function OrganizationEdit() {
|
|||||||
const [orgSlug, setOrgSlug] = useState("");
|
const [orgSlug, setOrgSlug] = useState("");
|
||||||
const [showPublicScores, setShowPublicScores] = useState(true);
|
const [showPublicScores, setShowPublicScores] = useState(true);
|
||||||
const [showPublicOpinions, setShowPublicOpinions] = useState(true);
|
const [showPublicOpinions, setShowPublicOpinions] = useState(true);
|
||||||
|
const [showPublicProfessionals, setShowPublicProfessionals] = useState(true);
|
||||||
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
|
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
|
||||||
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
|
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
|
||||||
const [originalServicesShowPublicScores, setOriginalServicesShowPublicScores] = useState(true);
|
const [originalServicesShowPublicScores, setOriginalServicesShowPublicScores] = useState(true);
|
||||||
@@ -76,7 +77,7 @@ export default function OrganizationEdit() {
|
|||||||
|
|
||||||
const [combosLoaded, setCombosLoaded] = useState<boolean>(false);
|
const [combosLoaded, setCombosLoaded] = useState<boolean>(false);
|
||||||
const ownerSubscription = SessionInfo.organizationSubscriptions[id];
|
const ownerSubscription = SessionInfo.organizationSubscriptions[id];
|
||||||
const canEditPublicRatingFlags = (ownerSubscription?.plan.price || 0) > 0;
|
const canEditPublicVisibilityFlags = (ownerSubscription?.plan.price || 0) > 0;
|
||||||
|
|
||||||
const [categoryFilterContent] = useState<TextObjectModalContent>({
|
const [categoryFilterContent] = useState<TextObjectModalContent>({
|
||||||
header: (
|
header: (
|
||||||
@@ -242,6 +243,11 @@ export default function OrganizationEdit() {
|
|||||||
setOrgPublished(organization.published);
|
setOrgPublished(organization.published);
|
||||||
setShowPublicScores(organization.showPublicScores !== undefined ? organization.showPublicScores : true);
|
setShowPublicScores(organization.showPublicScores !== undefined ? organization.showPublicScores : true);
|
||||||
setShowPublicOpinions(organization.showPublicOpinions !== undefined ? organization.showPublicOpinions : true);
|
setShowPublicOpinions(organization.showPublicOpinions !== undefined ? organization.showPublicOpinions : true);
|
||||||
|
setShowPublicProfessionals(
|
||||||
|
organization.showPublicProfessionals !== undefined
|
||||||
|
? organization.showPublicProfessionals
|
||||||
|
: true
|
||||||
|
);
|
||||||
setServices(loadedServices);
|
setServices(loadedServices);
|
||||||
setServicesShowPublicScores(
|
setServicesShowPublicScores(
|
||||||
loadedServices.every((service) => service.showPublicScores !== false)
|
loadedServices.every((service) => service.showPublicScores !== false)
|
||||||
@@ -302,10 +308,11 @@ export default function OrganizationEdit() {
|
|||||||
templateWapNotifId: templateWapNotifId === "" ? undefined : templateWapNotifId,
|
templateWapNotifId: templateWapNotifId === "" ? undefined : templateWapNotifId,
|
||||||
templateWapAltaId: templateWapAltaId === "" ? undefined : templateWapAltaId,
|
templateWapAltaId: templateWapAltaId === "" ? undefined : templateWapAltaId,
|
||||||
templateWapCancellationId: templateWapCancellationId === "" ? undefined : templateWapCancellationId,
|
templateWapCancellationId: templateWapCancellationId === "" ? undefined : templateWapCancellationId,
|
||||||
...(canEditPublicRatingFlags
|
...(canEditPublicVisibilityFlags
|
||||||
? {
|
? {
|
||||||
showPublicScores,
|
showPublicScores,
|
||||||
showPublicOpinions,
|
showPublicOpinions,
|
||||||
|
showPublicProfessionals,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
@@ -378,7 +385,7 @@ export default function OrganizationEdit() {
|
|||||||
const servicesShowPublicOpinionsChanged =
|
const servicesShowPublicOpinionsChanged =
|
||||||
originalServicesShowPublicOpinions !== servicesShowPublicOpinions;
|
originalServicesShowPublicOpinions !== servicesShowPublicOpinions;
|
||||||
const changedServices =
|
const changedServices =
|
||||||
canEditPublicRatingFlags &&
|
canEditPublicVisibilityFlags &&
|
||||||
(servicesShowPublicScoresChanged || servicesShowPublicOpinionsChanged)
|
(servicesShowPublicScoresChanged || servicesShowPublicOpinionsChanged)
|
||||||
? services
|
? services
|
||||||
: [];
|
: [];
|
||||||
@@ -574,13 +581,18 @@ export default function OrganizationEdit() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
|
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
|
||||||
<b>Visibilidad pública:</b> Elegí si querés mostrar puntuaciones y opiniones en el perfil público y las páginas de reserva. Disponible para planes pagos.
|
<b>Visibilidad pública:</b> Elegí si querés mostrar puntuaciones, opiniones y profesionales en el perfil público y las páginas de reserva. Disponible para planes pagos.
|
||||||
</MaterialAlert>
|
</MaterialAlert>
|
||||||
|
{!canEditPublicVisibilityFlags && (
|
||||||
|
<MaterialAlert severity="warning" sx={{ marginTop: "10px" }}>
|
||||||
|
Para modificar las opciones de visibilidad pública de opiniones, puntuaciones y profesionales de esta organización y sus servicios, el propietario de la organización necesita un plan pago.
|
||||||
|
</MaterialAlert>
|
||||||
|
)}
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "10px", marginTop: "10px" }}>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={showPublicScores}
|
checked={showPublicScores}
|
||||||
disabled={!canEditPublicRatingFlags}
|
disabled={!canEditPublicVisibilityFlags}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setShowPublicScores(event.target.checked);
|
setShowPublicScores(event.target.checked);
|
||||||
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
@@ -591,7 +603,7 @@ export default function OrganizationEdit() {
|
|||||||
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={showPublicOpinions}
|
checked={showPublicOpinions}
|
||||||
disabled={!canEditPublicRatingFlags}
|
disabled={!canEditPublicVisibilityFlags}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setShowPublicOpinions(event.target.checked);
|
setShowPublicOpinions(event.target.checked);
|
||||||
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
@@ -599,7 +611,18 @@ export default function OrganizationEdit() {
|
|||||||
/>
|
/>
|
||||||
Mostrar opiniones públicas
|
Mostrar opiniones públicas
|
||||||
</label>
|
</label>
|
||||||
{!canEditPublicRatingFlags && (
|
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={showPublicProfessionals}
|
||||||
|
disabled={!canEditPublicVisibilityFlags}
|
||||||
|
onChange={(event) => {
|
||||||
|
setShowPublicProfessionals(event.target.checked);
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Mostrar profesionales en el perfil público
|
||||||
|
</label>
|
||||||
|
{!canEditPublicVisibilityFlags && (
|
||||||
<span style={{ color: "var(--gray-darkest)", fontSize: "13px" }}>
|
<span style={{ color: "var(--gray-darkest)", fontSize: "13px" }}>
|
||||||
Disponible para planes pagos.
|
Disponible para planes pagos.
|
||||||
</span>
|
</span>
|
||||||
@@ -622,7 +645,7 @@ export default function OrganizationEdit() {
|
|||||||
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={servicesShowPublicScores}
|
checked={servicesShowPublicScores}
|
||||||
disabled={!canEditPublicRatingFlags}
|
disabled={!canEditPublicVisibilityFlags}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setServicesShowPublicScores(event.target.checked);
|
setServicesShowPublicScores(event.target.checked);
|
||||||
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
@@ -633,7 +656,7 @@ export default function OrganizationEdit() {
|
|||||||
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
<label style={{ display: "flex", alignItems: "center", gap: "10px", fontWeight: 600 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={servicesShowPublicOpinions}
|
checked={servicesShowPublicOpinions}
|
||||||
disabled={!canEditPublicRatingFlags}
|
disabled={!canEditPublicVisibilityFlags}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setServicesShowPublicOpinions(event.target.checked);
|
setServicesShowPublicOpinions(event.target.checked);
|
||||||
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
@@ -641,7 +664,7 @@ export default function OrganizationEdit() {
|
|||||||
/>
|
/>
|
||||||
Mostrar opiniones públicas de los servicios
|
Mostrar opiniones públicas de los servicios
|
||||||
</label>
|
</label>
|
||||||
{!canEditPublicRatingFlags && (
|
{!canEditPublicVisibilityFlags && (
|
||||||
<span style={{ color: "var(--gray-darkest)", fontSize: "13px" }}>
|
<span style={{ color: "var(--gray-darkest)", fontSize: "13px" }}>
|
||||||
Para cambiar la visibilidad pública de los servicios necesitás un plan pago.
|
Para cambiar la visibilidad pública de los servicios necesitás un plan pago.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
.professionals {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4px 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin: 0 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
color: #241f1b;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
max-width: 620px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: #6d6258;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 10px 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
cursor: grab;
|
||||||
|
scroll-padding-left: 10px;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 min(300px, calc(100vw - 40px));
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 116px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(45, 29, 21, 0.1);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 10px 24px rgba(45, 29, 21, 0.06);
|
||||||
|
color: inherit;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
border-color: rgba(116, 35, 59, 0.28);
|
||||||
|
box-shadow: 0 14px 30px rgba(45, 29, 21, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBody {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBody h3 {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
color: #342d27;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
color: #e8a533;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
color: #342d27;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyRating {
|
||||||
|
margin: 0;
|
||||||
|
color: #8b8178;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.card {
|
||||||
|
flex-basis: calc(100vw - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState, type PointerEvent, type ReactElement } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Avatar from "@components/Avatar/Avatar";
|
||||||
|
import { PublicCompanyEmployeeView } from "@core/Models/Employees.model";
|
||||||
|
import { RatingTargetSummaryResult, RatingTargetType } from "@core/Models/Ratings.model";
|
||||||
|
import { findPublicCompanyEmployees } from "@services/Employees.Service";
|
||||||
|
import { ratingTargetSummary } from "@services/Ratings.Service";
|
||||||
|
import style from "./OrganizationProfessionals.module.css";
|
||||||
|
|
||||||
|
interface OrganizationProfessionalsProps {
|
||||||
|
companyId: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfessionalWithRating = PublicCompanyEmployeeView & {
|
||||||
|
rating: RatingTargetSummaryResult | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStars = (score: number): string => {
|
||||||
|
const normalizedScore = Math.min(Math.max(Math.round(score), 1), 5);
|
||||||
|
return "★".repeat(normalizedScore) + "☆".repeat(5 - normalizedScore);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatScore = (score: number): string => score.toFixed(1);
|
||||||
|
|
||||||
|
const getFullName = (professional: PublicCompanyEmployeeView): string => {
|
||||||
|
return [professional.firstName, professional.lastName].filter(Boolean).join(" ").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OrganizationProfessionals({
|
||||||
|
companyId,
|
||||||
|
title = "Profesionales de la organización",
|
||||||
|
subtitle = "Conocé a quienes atienden y sus opiniones.",
|
||||||
|
className,
|
||||||
|
}: OrganizationProfessionalsProps): ReactElement | null {
|
||||||
|
const [professionals, setProfessionals] = useState<ProfessionalWithRating[]>([]);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
findPublicCompanyEmployees({ companyId })
|
||||||
|
.then(async (employees) => {
|
||||||
|
const employeesWithRatings = await Promise.all(
|
||||||
|
employees.map(async (employee) => {
|
||||||
|
try {
|
||||||
|
const rating = await ratingTargetSummary({
|
||||||
|
targetType: RatingTargetType.EMPLOYEE,
|
||||||
|
targetId: employee.employeeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...employee, rating };
|
||||||
|
} catch {
|
||||||
|
return { ...employee, rating: null };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
setProfessionals(employeesWithRatings);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (active) {
|
||||||
|
setProfessionals([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [companyId]);
|
||||||
|
|
||||||
|
const handlePointerDown = () => {
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
||||||
|
const scroller = scrollerRef.current;
|
||||||
|
|
||||||
|
if (!isDragging || !scroller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scroller.scrollLeft -= event.movementX;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDragging = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (professionals.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={classNames(style.professionals, className)} aria-label="Profesionales">
|
||||||
|
<div className={style.header}>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
{subtitle && <p>{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={classNames(style.scroller, { [style.dragging]: isDragging })}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
|
onPointerUp={stopDragging}
|
||||||
|
onPointerLeave={stopDragging}
|
||||||
|
>
|
||||||
|
{professionals.map((professional) => {
|
||||||
|
const fullName = getFullName(professional) || "Profesional";
|
||||||
|
const hasRating = Boolean(professional.rating && professional.rating.totalCount > 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={style.card}
|
||||||
|
href={`/landing/reviews?targetType=employee&targetId=${professional.employeeId}`}
|
||||||
|
key={professional.employeeId}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
name={`professional-${professional.employeeId}`}
|
||||||
|
src={professional.avatarUrl || fullName}
|
||||||
|
alt={`Avatar de ${fullName}`}
|
||||||
|
size="medium"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
<div className={style.cardBody}>
|
||||||
|
<h3>{fullName}</h3>
|
||||||
|
{hasRating ? (
|
||||||
|
<div
|
||||||
|
className={style.rating}
|
||||||
|
aria-label={`${formatScore(professional.rating!.averageScore)} de 5 estrellas`}
|
||||||
|
>
|
||||||
|
<span className={style.stars} aria-hidden="true">
|
||||||
|
{renderStars(professional.rating!.averageScore)}
|
||||||
|
</span>
|
||||||
|
<span className={style.score}>
|
||||||
|
{formatScore(professional.rating!.averageScore)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className={style.emptyRating}>Sin opiniones aún</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
margin: 0 10px 5px;
|
margin: 0 10px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.header h2 {
|
.header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@@ -17,6 +24,19 @@
|
|||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewMore {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--wine-red);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewMore:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.header p {
|
.header p {
|
||||||
max-width: 620px;
|
max-width: 620px;
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
@@ -109,6 +129,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
|
.titleRow {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.reviewCard {
|
.reviewCard {
|
||||||
flex-basis: calc(100vw - 40px);
|
flex-basis: calc(100vw - 40px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState, type PointerEvent, type ReactElement } from "react";
|
import { useEffect, useRef, useState, type PointerEvent, type ReactElement } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import Link from "next/link";
|
||||||
import Avatar from "@components/Avatar/Avatar";
|
import Avatar from "@components/Avatar/Avatar";
|
||||||
import { RatingTargetReviewItem, RatingTargetType } from "@core/Models/Ratings.model";
|
import { RatingTargetReviewItem, RatingTargetType } from "@core/Models/Ratings.model";
|
||||||
import { ratingTargetReviews } from "@services/Ratings.Service";
|
import { ratingTargetReviews } from "@services/Ratings.Service";
|
||||||
@@ -58,9 +59,9 @@ export default function ReviewsCarousel({
|
|||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
ratingTargetReviews({ targetType, targetId, limit })
|
ratingTargetReviews({ targetType, targetId, limit })
|
||||||
.then((targetReviews) => {
|
.then((targetReviewsResult) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
setReviews(sortReviewsByScore(targetReviews.filter((review) => review.comment.trim().length > 0)));
|
setReviews(sortReviewsByScore(targetReviewsResult.reviews.filter((review) => review.comment.trim().length > 0)));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -99,7 +100,12 @@ export default function ReviewsCarousel({
|
|||||||
return (
|
return (
|
||||||
<section className={classNames(style.reviewsCarousel, className)} aria-label="Opiniones">
|
<section className={classNames(style.reviewsCarousel, className)} aria-label="Opiniones">
|
||||||
<div className={style.header}>
|
<div className={style.header}>
|
||||||
|
<div className={style.titleRow}>
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
|
<Link className={style.viewMore} href={`/landing/reviews?targetType=${targetType}&targetId=${targetId}`}>
|
||||||
|
Ver más
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
{subtitle && <p>{subtitle}</p>}
|
{subtitle && <p>{subtitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { useParams } from "next/navigation";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useConfirmStore } from "@core/Store/Confirm.Store";
|
import { useConfirmStore } from "@core/Store/Confirm.Store";
|
||||||
import ReviewsCarousel from "@core/app/components/ReviewsCarousel/ReviewsCarousel";
|
import ReviewsCarousel from "@core/app/components/ReviewsCarousel/ReviewsCarousel";
|
||||||
|
import OrganizationProfessionals from "@core/app/components/OrganizationProfessionals/OrganizationProfessionals";
|
||||||
import { RatingTargetType } from "@core/Models/Ratings.model";
|
import { RatingTargetType } from "@core/Models/Ratings.model";
|
||||||
|
|
||||||
enum VIEW {
|
enum VIEW {
|
||||||
@@ -337,7 +338,7 @@ export default function OrganizationPublicProfile() {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
const newPosts = response.data.filter(
|
const newPosts = response.data.filter(
|
||||||
(newPost) => !prevPosts.some((prevPost) => prevPost._id === newPost._id)
|
(newPost) => !prevPosts.some((prevPost) => prevPost._id === newPost._id),
|
||||||
);
|
);
|
||||||
|
|
||||||
return [...prevPosts, ...newPosts];
|
return [...prevPosts, ...newPosts];
|
||||||
@@ -453,7 +454,7 @@ export default function OrganizationPublicProfile() {
|
|||||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
() => {}
|
() => {},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -665,7 +666,9 @@ export default function OrganizationPublicProfile() {
|
|||||||
<GridViewOutlinedIcon
|
<GridViewOutlinedIcon
|
||||||
sx={{
|
sx={{
|
||||||
color:
|
color:
|
||||||
viewMode === "card" ? "var(--wine-red)" : "ineherit",
|
viewMode === "card"
|
||||||
|
? "var(--wine-red)"
|
||||||
|
: "ineherit",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -673,7 +676,9 @@ export default function OrganizationPublicProfile() {
|
|||||||
<SplitscreenOutlinedIcon
|
<SplitscreenOutlinedIcon
|
||||||
sx={{
|
sx={{
|
||||||
color:
|
color:
|
||||||
viewMode === "grid" ? "var(--wine-red)" : "ineherit",
|
viewMode === "grid"
|
||||||
|
? "var(--wine-red)"
|
||||||
|
: "ineherit",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -736,7 +741,9 @@ export default function OrganizationPublicProfile() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ color: "var(--gray-darkest)" }}>
|
<div style={{ color: "var(--gray-darkest)" }}>
|
||||||
{dayjs(post.createdAt).format("DD MMMM YYYY HH:mm")}
|
{dayjs(post.createdAt).format(
|
||||||
|
"DD MMMM YYYY HH:mm",
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{post.fixed && (
|
{post.fixed && (
|
||||||
<div
|
<div
|
||||||
@@ -755,7 +762,9 @@ export default function OrganizationPublicProfile() {
|
|||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: preprocessQuillHtml(
|
__html: preprocessQuillHtml(
|
||||||
transformQuillContentForDisplay(post.content)
|
transformQuillContentForDisplay(
|
||||||
|
post.content,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -788,7 +797,10 @@ export default function OrganizationPublicProfile() {
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
startIcon={<UpIcon />}
|
startIcon={<UpIcon />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
moverPost(`${post._id}`, -1);
|
moverPost(
|
||||||
|
`${post._id}`,
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Subir
|
Subir
|
||||||
@@ -798,7 +810,10 @@ export default function OrganizationPublicProfile() {
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
startIcon={<DownIcon />}
|
startIcon={<DownIcon />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
moverPost(`${post._id}`, 1);
|
moverPost(
|
||||||
|
`${post._id}`,
|
||||||
|
1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Bajar
|
Bajar
|
||||||
@@ -878,11 +893,14 @@ export default function OrganizationPublicProfile() {
|
|||||||
<ReviewsCarousel
|
<ReviewsCarousel
|
||||||
targetType={RatingTargetType.COMPANY}
|
targetType={RatingTargetType.COMPANY}
|
||||||
targetId={orgData.id}
|
targetId={orgData.id}
|
||||||
title="Lo que dicen quienes ya visitaron esta organización"
|
title={`Opiniones sobre ${orgData.name}`}
|
||||||
subtitle="Opiniones reales de personas que reservaron turnos acá."
|
subtitle="Opiniones reales de personas que reservaron turnos acá."
|
||||||
className={style.organizationReviews}
|
className={style.organizationReviews}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{orgData.showPublicProfessionals !== false && (
|
||||||
|
<OrganizationProfessionals companyId={orgData.id} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
.page {
|
||||||
|
width: min(1080px, calc(100% - 32px));
|
||||||
|
min-height: 70vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 44px 0 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 92px minmax(0, 1fr);
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
border: 1px solid rgba(127, 42, 255, 0.14);
|
||||||
|
border-radius: 28px;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(255, 42, 127, 0.08), transparent 36%),
|
||||||
|
linear-gradient(180deg, var(--white), var(--white-dark));
|
||||||
|
box-shadow: 0 26px 70px rgba(16, 0, 39, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroRail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 14px;
|
||||||
|
border-radius: 26px 0 0 26px;
|
||||||
|
background: linear-gradient(180deg, var(--wine-darkest), var(--wine-dark));
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroRail span:last-child {
|
||||||
|
color: var(--yellow);
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroContent {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(220px, 280px);
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(22px, 4vw, 44px);
|
||||||
|
padding: clamp(26px, 5vw, 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyBlock {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border: 1px solid rgba(255, 42, 127, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--yellow-light);
|
||||||
|
color: var(--wine-red);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.targetLabel {
|
||||||
|
display: inline-flex;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding: 6px 11px;
|
||||||
|
border: 1px solid rgba(127, 42, 255, 0.16);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--wine-dark);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalHero {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalIdentity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(14px, 3vw, 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.targetAvatar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid rgba(255, 42, 127, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--white);
|
||||||
|
box-shadow: 0 14px 32px rgba(16, 0, 39, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.targetTitleBlock {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalText {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: clamp(2.05rem, 5vw, 3.75rem);
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalIdentity h1 {
|
||||||
|
max-width: 600px;
|
||||||
|
font-size: clamp(1.9rem, 4.3vw, 3.35rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalText p {
|
||||||
|
max-width: 540px;
|
||||||
|
font-size: clamp(0.98rem, 1.7vw, 1.12rem);
|
||||||
|
line-height: 1.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p:last-child {
|
||||||
|
max-width: 660px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--black-light);
|
||||||
|
font-size: clamp(1.03rem, 2vw, 1.22rem);
|
||||||
|
line-height: 1.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 9px;
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(127, 42, 255, 0.13);
|
||||||
|
border-radius: 24px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, var(--white), rgba(253, 228, 84, 0.16));
|
||||||
|
box-shadow: 0 18px 42px rgba(16, 0, 39, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryLabel {
|
||||||
|
color: var(--gray-dark);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryScore {
|
||||||
|
color: var(--wine-red);
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryStars {
|
||||||
|
color: #e8a533;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryText {
|
||||||
|
color: var(--black-light);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(45, 29, 21, 0.08);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: #4d443d;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader span {
|
||||||
|
color: #8b8178;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultsCount {
|
||||||
|
margin: 0;
|
||||||
|
color: #6d6258;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewsList {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewCard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(45, 29, 21, 0.1);
|
||||||
|
border-radius: 24px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 252, 249, 0.94));
|
||||||
|
box-shadow: 0 14px 32px rgba(45, 29, 21, 0.07);
|
||||||
|
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewCard:hover {
|
||||||
|
border-color: rgba(141, 24, 44, 0.18);
|
||||||
|
box-shadow: 0 18px 42px rgba(45, 29, 21, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
color: #e8a533;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score {
|
||||||
|
color: #342d27;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin: 0;
|
||||||
|
color: #2f2924;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 4px;
|
||||||
|
color: #8b8178;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorBlock {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #342d27;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px solid rgba(45, 29, 21, 0.08);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.76);
|
||||||
|
color: #6d6258;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.page {
|
||||||
|
width: min(100% - 20px, 960px);
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroRail {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 26px 26px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroRail span:last-child {
|
||||||
|
writing-mode: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroContent {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard {
|
||||||
|
max-width: none;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalIdentity {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalHero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.professionalText {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Suspense, useEffect, useState, type ChangeEvent, type ReactElement } from "react";
|
||||||
|
import Avatar from "@components/Avatar/Avatar";
|
||||||
|
import ApiRequest from "@services/Api.Service";
|
||||||
|
import { PublicOrganizationView } from "@core/Models/Company.model";
|
||||||
|
import { RatingTargetReviewItem, RatingTargetType } from "@core/Models/Ratings.model";
|
||||||
|
import { PublicOrganizationServiceView } from "@core/Models/Service.model";
|
||||||
|
import { findPublicEmployee } from "@services/Employees.Service";
|
||||||
|
import { ratingTargetReviews, ratingTargetSummary } from "@services/Ratings.Service";
|
||||||
|
import Pagination from "@mui/material/Pagination";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import style from "./page.module.css";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
const OBJECT_ID_PATTERN = /^[a-f\d]{24}$/i;
|
||||||
|
|
||||||
|
const formatReviewDate = (date: string): string => {
|
||||||
|
return new Intl.DateTimeFormat("es-AR", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(new Date(date));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStars = (score: number): string => {
|
||||||
|
const normalizedScore = Math.min(Math.max(Math.round(score), 1), 5);
|
||||||
|
return "★".repeat(normalizedScore) + "☆".repeat(5 - normalizedScore);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatScore = (score: number): string => score.toFixed(1);
|
||||||
|
|
||||||
|
type TargetContext = {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TARGET_FALLBACKS: Record<RatingTargetType, TargetContext> = {
|
||||||
|
[RatingTargetType.COMPANY]: {
|
||||||
|
label: "Organización",
|
||||||
|
name: "esta organización",
|
||||||
|
description: "Opiniones sobre la experiencia general con la organización.",
|
||||||
|
},
|
||||||
|
[RatingTargetType.SERVICE]: {
|
||||||
|
label: "Servicio",
|
||||||
|
name: "este servicio",
|
||||||
|
description: "Opiniones sobre un servicio específico reservado en TurnosXpress.",
|
||||||
|
},
|
||||||
|
[RatingTargetType.EMPLOYEE]: {
|
||||||
|
label: "Profesional",
|
||||||
|
name: "este profesional",
|
||||||
|
description: "Opiniones sobre la atención recibida con este profesional.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidTargetType = (value: string | null): value is RatingTargetType => {
|
||||||
|
return Boolean(value && Object.values(RatingTargetType).includes(value as RatingTargetType));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTargetContext = async (targetType: RatingTargetType, targetId: string): Promise<TargetContext> => {
|
||||||
|
const fallback = TARGET_FALLBACKS[targetType];
|
||||||
|
|
||||||
|
if (targetType === RatingTargetType.COMPANY) {
|
||||||
|
const organization = await ApiRequest.post<PublicOrganizationView>("views/organization", { companyId: targetId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
name: organization.name || fallback.name,
|
||||||
|
description: organization.description || fallback.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType === RatingTargetType.SERVICE) {
|
||||||
|
const view = await ApiRequest.post<PublicOrganizationServiceView>("views/service", { serviceId: targetId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
name: view.service?.name || fallback.name,
|
||||||
|
description: view.organization?.name ? `Servicio ofrecido por ${view.organization.name}.` : fallback.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetType === RatingTargetType.EMPLOYEE) {
|
||||||
|
const employee = await findPublicEmployee({ employeeId: targetId });
|
||||||
|
const fullName = [employee.firstName, employee.lastName].filter(Boolean).join(" ").trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
name: fullName || fallback.name,
|
||||||
|
avatarUrl: employee.avatarUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ReviewsContent(): ReactElement {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const targetType = searchParams.get("targetType");
|
||||||
|
const targetId = searchParams.get("targetId");
|
||||||
|
|
||||||
|
const [reviews, setReviews] = useState<RatingTargetReviewItem[]>([]);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [totalRatings, setTotalRatings] = useState(0);
|
||||||
|
const [averageScore, setAverageScore] = useState(0);
|
||||||
|
const [targetContext, setTargetContext] = useState<TargetContext | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isContextLoading, setIsContextLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const hasValidTarget = isValidTargetType(targetType) && Boolean(targetId && OBJECT_ID_PATTERN.test(targetId));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
}, [targetType, targetId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
if (!hasValidTarget) {
|
||||||
|
setReviews([]);
|
||||||
|
setTotalPages(0);
|
||||||
|
setTotal(0);
|
||||||
|
setTotalRatings(0);
|
||||||
|
setAverageScore(0);
|
||||||
|
setTargetContext(null);
|
||||||
|
setError("No encontramos una referencia válida para mostrar opiniones.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setIsContextLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
const validTargetType = targetType as RatingTargetType;
|
||||||
|
const validTargetId = targetId || "";
|
||||||
|
|
||||||
|
resolveTargetContext(validTargetType, validTargetId)
|
||||||
|
.then((context) => {
|
||||||
|
if (active) {
|
||||||
|
setTargetContext(context);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (active) {
|
||||||
|
setTargetContext(TARGET_FALLBACKS[validTargetType]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (active) {
|
||||||
|
setIsContextLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ratingTargetSummary({ targetType: validTargetType, targetId: validTargetId })
|
||||||
|
.then((summary) => {
|
||||||
|
if (active) {
|
||||||
|
setAverageScore(summary.averageScore);
|
||||||
|
setTotalRatings(summary.totalCount);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (active) {
|
||||||
|
setAverageScore(0);
|
||||||
|
setTotalRatings(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ratingTargetReviews({ targetType: validTargetType, targetId: validTargetId, page, pageSize: PAGE_SIZE })
|
||||||
|
.then((result) => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReviews(result.reviews.filter((review) => review.comment.trim().length > 0));
|
||||||
|
setTotalPages(result.totalPages);
|
||||||
|
setTotal(result.total);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReviews([]);
|
||||||
|
setTotalPages(0);
|
||||||
|
setTotal(0);
|
||||||
|
setError("No pudimos cargar las opiniones. Intentá nuevamente en unos minutos.");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (active) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [hasValidTarget, page, targetId, targetType]);
|
||||||
|
|
||||||
|
const handlePageChange = (_event: ChangeEvent<unknown>, value: number) => {
|
||||||
|
setPage(value);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleContext = targetContext || (isValidTargetType(targetType) ? TARGET_FALLBACKS[targetType] : null);
|
||||||
|
const heroTitle = isContextLoading ? "Cargando contexto..." : visibleContext?.name || "Opiniones verificadas";
|
||||||
|
const reviewLabel = totalRatings === 1 ? "opinión" : "opiniones";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={style.page}>
|
||||||
|
<section className={style.hero}>
|
||||||
|
<div className={style.heroRail} aria-hidden="true">
|
||||||
|
<span>TX</span>
|
||||||
|
<span>Reviews</span>
|
||||||
|
</div>
|
||||||
|
<div className={style.heroContent}>
|
||||||
|
<div className={style.copyBlock}>
|
||||||
|
<p className={style.kicker}>Opiniones verificadas</p>
|
||||||
|
{targetType === RatingTargetType.EMPLOYEE && visibleContext ? (
|
||||||
|
<div className={style.professionalHero}>
|
||||||
|
<div className={style.professionalIdentity}>
|
||||||
|
<div className={style.targetAvatar}>
|
||||||
|
<Avatar
|
||||||
|
name="target-professional-avatar"
|
||||||
|
src={visibleContext.avatarUrl || visibleContext.name}
|
||||||
|
alt={`Avatar de ${visibleContext.name}`}
|
||||||
|
size="large"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={style.professionalText}>
|
||||||
|
<span className={style.targetLabel}>{visibleContext.label}</span>
|
||||||
|
<h1>{heroTitle}</h1>
|
||||||
|
<p>{visibleContext.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={style.targetTitleBlock}>
|
||||||
|
<span className={style.targetLabel}>{visibleContext?.label || "Opiniones"}</span>
|
||||||
|
<h1>{heroTitle}</h1>
|
||||||
|
<p>{visibleContext?.description || "Conocé experiencias reales de personas que ya reservaron."}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={style.summaryCard} aria-label="Resumen de opiniones">
|
||||||
|
<span className={style.summaryLabel}>Promedio general</span>
|
||||||
|
<span className={style.summaryScore}>{averageScore > 0 ? formatScore(averageScore) : "--"}</span>
|
||||||
|
<span className={style.summaryStars} aria-hidden="true">
|
||||||
|
{averageScore > 0 ? renderStars(averageScore) : "☆☆☆☆☆"}
|
||||||
|
</span>
|
||||||
|
<span className={style.summaryText}>{totalRatings} {reviewLabel} verificadas</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{isLoading && <p className={style.state}>Cargando opiniones...</p>}
|
||||||
|
|
||||||
|
{!isLoading && error && <p className={style.state}>{error}</p>}
|
||||||
|
|
||||||
|
{!isLoading && !error && reviews.length === 0 && (
|
||||||
|
<p className={style.state}>Todavía no hay opiniones con comentarios para mostrar.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && reviews.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className={style.listHeader}>
|
||||||
|
<p className={style.resultsCount}>{total} opiniones con comentarios</p>
|
||||||
|
<span>Ordenadas por calificación y fecha</span>
|
||||||
|
</div>
|
||||||
|
<div className={style.reviewsList}>
|
||||||
|
{reviews.map((review) => (
|
||||||
|
<article className={style.reviewCard} key={review.id}>
|
||||||
|
<div className={style.rating} aria-label={`${formatScore(review.score)} de 5 estrellas`}>
|
||||||
|
<span className={style.stars} aria-hidden="true">
|
||||||
|
{renderStars(review.score)}
|
||||||
|
</span>
|
||||||
|
<span className={style.score}>{formatScore(review.score)}</span>
|
||||||
|
</div>
|
||||||
|
<p className={style.comment}>“{review.comment}”</p>
|
||||||
|
<div className={style.footer}>
|
||||||
|
<div className={style.authorBlock}>
|
||||||
|
<Avatar
|
||||||
|
name={`review-author-${review.id}`}
|
||||||
|
src={review.author.avatarUrl || review.author.displayName}
|
||||||
|
alt={`Avatar de ${review.author.displayName}`}
|
||||||
|
size="small"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
<span className={style.author}>{review.author.displayName}</span>
|
||||||
|
</div>
|
||||||
|
<time dateTime={review.createdAt}>{formatReviewDate(review.createdAt)}</time>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className={style.pagination}>
|
||||||
|
<Pagination count={totalPages} page={page} variant="outlined" onChange={handlePageChange} />
|
||||||
|
<span>
|
||||||
|
Página {page} de {totalPages}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReviewsPage(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<main className={style.page}>Cargando opiniones...</main>}>
|
||||||
|
<ReviewsContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ const GetServiceData = async (serviceId: string): Promise<PublicOrganizationServ
|
|||||||
"views/service",
|
"views/service",
|
||||||
{
|
{
|
||||||
serviceId: serviceId,
|
serviceId: serviceId,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return organizationServiceData;
|
return organizationServiceData;
|
||||||
@@ -42,9 +42,7 @@ export default async function OrganizationPublicProfile({ params }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!organizationService.organization && !organizationService.service && (
|
{!organizationService.organization && !organizationService.service && <ServicePrivate />}
|
||||||
<ServicePrivate />
|
|
||||||
)}
|
|
||||||
{organizationService.organization && organizationService.service && (
|
{organizationService.organization && organizationService.service && (
|
||||||
<AnimatedContainer
|
<AnimatedContainer
|
||||||
color={organizationService.organization.headerColor}
|
color={organizationService.organization.headerColor}
|
||||||
@@ -70,9 +68,7 @@ export default async function OrganizationPublicProfile({ params }: Props) {
|
|||||||
<Appointment
|
<Appointment
|
||||||
serviceId={sid}
|
serviceId={sid}
|
||||||
companyId={organizationService.organization.id}
|
companyId={organizationService.organization.id}
|
||||||
appointmentAlert={
|
appointmentAlert={organizationService.organization.appointmentAlert}
|
||||||
organizationService.organization.appointmentAlert
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,10 +82,18 @@ export default async function OrganizationPublicProfile({ params }: Props) {
|
|||||||
<ReviewsCarousel
|
<ReviewsCarousel
|
||||||
targetType={RatingTargetType.SERVICE}
|
targetType={RatingTargetType.SERVICE}
|
||||||
targetId={sid}
|
targetId={sid}
|
||||||
title="Lo que dicen quienes ya reservaron"
|
title={`Opiniones sobre ${organizationService.service.name}`}
|
||||||
subtitle="Comentarios de clientes que calificaron este servicio después de su turno."
|
subtitle="Comentarios de clientes que calificaron este servicio después de su turno."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{organizationService.organization.showPublicOpinions !== false && (
|
||||||
|
<ReviewsCarousel
|
||||||
|
targetType={RatingTargetType.COMPANY}
|
||||||
|
targetId={organizationService.organization.id}
|
||||||
|
title="Opiniones sobre la organización"
|
||||||
|
subtitle={`Comentarios de clientes que calificaron a ${organizationService.organization.name} después de su turno.`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AnimatedContainer>
|
</AnimatedContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user