feat: implement rating summary service methods and integrate rating display components into the organization header and pending ratings page

This commit is contained in:
2026-07-22 12:13:50 -03:00
parent 383a5bc6b4
commit da3c3abb52
27 changed files with 1334 additions and 104 deletions
+80 -67
View File
@@ -58,6 +58,8 @@ import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { useParams } from "next/navigation";
import dayjs from "dayjs";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import ReviewsCarousel from "@core/app/components/ReviewsCarousel/ReviewsCarousel";
import { RatingTargetType } from "@core/Models/Ratings.model";
enum VIEW {
POSTS = "posts",
@@ -592,92 +594,94 @@ export default function OrganizationPublicProfile() {
</div>
</div>
)}
<div className={style.organizationMap}>{LocationInfo(orgData)}</div>
<div
ref={refDivContent}
style={{
border: "1px solid var(--gray-light)",
borderRadius: "10px",
marginTop: "30px",
width: "100%",
marginBottom: "30px",
backgroundColor: "var(--white)",
}}
>
<div className={style.organizationProfileColumns}>
<div className={style.organizationMap}>{LocationInfo(orgData)}</div>
<div className={style.organizationMainColumn}>
<div
ref={refDivContent}
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
borderBottom: "1px solid var(--gray-light)",
border: "1px solid var(--gray-light)",
borderRadius: "10px",
marginTop: "30px",
width: "100%",
marginBottom: "30px",
backgroundColor: "var(--white)",
}}
>
<div
style={{
height: "50px",
display: "flex",
alignItems: "center",
gap: "20px",
flexDirection: "row",
justifyContent: "space-between",
borderBottom: "1px solid var(--gray-light)",
}}
>
<div
style={getHeaderItemStyle(VIEW.POSTS)}
onClick={() => {
setView(VIEW.POSTS);
processAddButton(VIEW.POSTS);
style={{
height: "50px",
display: "flex",
alignItems: "center",
gap: "20px",
}}
>
Posts
</div>
<div
style={getHeaderItemStyle(VIEW.SERVICES)}
onClick={() => {
setView(VIEW.SERVICES);
processAddButton(VIEW.SERVICES);
}}
>
Servicios
</div>
{checkShowInfo() && (
<div
style={getHeaderItemStyle(VIEW.INFO)}
style={getHeaderItemStyle(VIEW.POSTS)}
onClick={() => {
setView(VIEW.INFO);
processAddButton(VIEW.INFO);
setView(VIEW.POSTS);
processAddButton(VIEW.POSTS);
}}
>
Contacto
Posts
</div>
<div
style={getHeaderItemStyle(VIEW.SERVICES)}
onClick={() => {
setView(VIEW.SERVICES);
processAddButton(VIEW.SERVICES);
}}
>
Servicios
</div>
{checkShowInfo() && (
<div
style={getHeaderItemStyle(VIEW.INFO)}
onClick={() => {
setView(VIEW.INFO);
processAddButton(VIEW.INFO);
}}
>
Contacto
</div>
)}
</div>
{view === VIEW.SERVICES && (
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<ButtonGroup
variant="outlined"
aria-label="Basic button group"
sx={{ marginTop: "10px", marginRight: "30px" }}
>
<IconButton onClick={() => setViewMode("card")}>
<GridViewOutlinedIcon
sx={{
color:
viewMode === "card" ? "var(--wine-red)" : "ineherit",
}}
/>
</IconButton>
<IconButton onClick={() => setViewMode("grid")}>
<SplitscreenOutlinedIcon
sx={{
color:
viewMode === "grid" ? "var(--wine-red)" : "ineherit",
}}
/>
</IconButton>
</ButtonGroup>
</div>
)}
</div>
{view === VIEW.SERVICES && (
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<ButtonGroup
variant="outlined"
aria-label="Basic button group"
sx={{ marginTop: "10px", marginRight: "30px" }}
>
<IconButton onClick={() => setViewMode("card")}>
<GridViewOutlinedIcon
sx={{
color:
viewMode === "card" ? "var(--wine-red)" : "ineherit",
}}
/>
</IconButton>
<IconButton onClick={() => setViewMode("grid")}>
<SplitscreenOutlinedIcon
sx={{
color:
viewMode === "grid" ? "var(--wine-red)" : "ineherit",
}}
/>
</IconButton>
</ButtonGroup>
</div>
)}
</div>
<div>
<div>
{view === VIEW.POSTS && (
<div
ref={refPostsContainer}
@@ -867,7 +871,16 @@ export default function OrganizationPublicProfile() {
</div>
)}
</div>
</div>
</div>
</div>
<ReviewsCarousel
targetType={RatingTargetType.COMPANY}
targetId={orgData.id}
title="Lo que dicen quienes ya visitaron esta organización"
subtitle="Opiniones reales de personas que reservaron turnos acá."
className={style.organizationReviews}
/>
</div>
)}
</>
@@ -1,5 +1,6 @@
@media (max-width: 800px) {
.organizationContent {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
@@ -7,7 +8,14 @@
margin-left: 30px;
margin-right: 30px;
gap: 20px;
overflow: hidden;
overflow: visible;
}
.organizationProfileColumns {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
.organizationMap {
@@ -80,6 +88,14 @@
margin-bottom: 10px;
}
.organizationMainColumn {
width: 100%;
}
.organizationReviews {
margin-bottom: 30px;
}
.organizationServices {
margin-bottom: 30px;
}
@@ -87,15 +103,29 @@
@media (min-width: 801px) {
.organizationContent {
position: relative;
display: flex;
flex-direction: row;
align-items: flex-start;
flex-direction: column;
align-items: stretch;
/*margin-top: 30px;*/
margin-left: 30px;
margin-right: 30px;
gap: 20px;
}
.organizationProfileColumns {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 20px;
width: 100%;
}
.organizationMainColumn {
width: 100%;
min-width: 0;
}
.organizationMap {
border: solid 1px var(--gray-light);
border-radius: 10px;
@@ -136,4 +166,9 @@
width: 100%;
overflow: hidden;
}
.organizationReviews {
margin-bottom: 30px;
width: 100%;
}
}
@@ -13,7 +13,7 @@ import { Box, Button, Rating, TextField } from "@mui/material";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { createRating, pendingRatingsByUser } from "@services/Ratings.Service";
import { countRatingsByUser, createRating, pendingRatingsByUser } from "@services/Ratings.Service";
import { PendingRatingItem, RATING_COMMENT_MAX_LENGTH, RatingTargetType } from "@core/Models/Ratings.model";
import styles from "./page.module.css";
@@ -37,6 +37,7 @@ export default function PendingRatingsPage() {
const [comments, setComments] = useState<Record<string, string>>({});
const [submittingKey, setSubmittingKey] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [createdRatingsCount, setCreatedRatingsCount] = useState(0);
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
@@ -45,14 +46,21 @@ export default function PendingRatingsPage() {
const loadPendingRatings = () => {
if (!SessionInfo.userId) {
setPendingRatings([]);
setCreatedRatingsCount(0);
setIsLoading(false);
return;
}
setIsLoading(true);
eventHandler.setEventType(EVENT_TYPES.LOADING);
pendingRatingsByUser({ sessionUser: SessionInfo.userId })
.then((res) => setPendingRatings(res))
Promise.all([
pendingRatingsByUser({ sessionUser: SessionInfo.userId }),
countRatingsByUser({ sessionUser: SessionInfo.userId }),
])
.then(([pendingRatingsResponse, createdRatingsResponse]) => {
setPendingRatings(pendingRatingsResponse);
setCreatedRatingsCount(createdRatingsResponse.count);
})
.catch((error) => {
alert.showError(error.message || "No pudimos cargar tus calificaciones pendientes.");
console.error("Failed to load pending ratings", {
@@ -135,8 +143,8 @@ export default function PendingRatingsPage() {
<div className={styles.heroPanel}>
<span className={styles.panelLabel}>Tu aporte</span>
<strong>{isLoading ? "..." : pendingRatings.length}</strong>
<span>{pendingRatings.length === 1 ? "experiencia pendiente" : "experiencias pendientes"}</span>
<strong>{isLoading ? "..." : createdRatingsCount}</strong>
<span>{createdRatingsCount === 1 ? "opinión realizada" : "opiniones realizadas"}</span>
</div>
<div className={styles.heroStats} aria-label="Resumen de calificaciones pendientes">
@@ -10,6 +10,8 @@ import { ScrollToTop } from "@core/app/components/ScrollTop";
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
import ServicePrivate from "@core/app/components/ServicePrivate/ServicePrivate";
import EvangelizeBanner from "@components/EvangelizeBanner/EvangelizeBanner";
import ReviewsCarousel from "@components/ReviewsCarousel/ReviewsCarousel";
import { RatingTargetType } from "@core/Models/Ratings.model";
const GetServiceData = async (serviceId: string): Promise<PublicOrganizationServiceView> => {
try {
@@ -80,6 +82,12 @@ export default async function OrganizationPublicProfile({ params }: Props) {
title="Otros Servicios &#x1F3C6;"
data={organizationService.organization.services.data}
/>
<ReviewsCarousel
targetType={RatingTargetType.SERVICE}
targetId={sid}
title="Lo que dicen quienes ya reservaron"
subtitle="Comentarios de clientes que calificaron este servicio después de su turno."
/>
</div>
</AnimatedContainer>
)}