feat: implement rating summary service methods and integrate rating display components into the organization header and pending ratings page
This commit is contained in:
@@ -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 🏆"
|
||||
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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user