feat: implement full-stack ratings system for appointments with user submission and pending ratings dashboard integration
This commit is contained in:
@@ -44,6 +44,7 @@ import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
|
||||
import FinancialWidget from "@components/FinancialWidget/FinancialWidget";
|
||||
import PlanMetricsWidget from "@components/PlanMetricsWidget/PlanMetricsWidget";
|
||||
import PendingRepeatsCard from "@core/app/components/PendingRepeatsCard/PendingRepeatsCard";
|
||||
import PendingRatingsBanner from "@components/PendingRatingsBanner/PendingRatingsBanner";
|
||||
|
||||
function DashboardContent() {
|
||||
const { goTo } = useNavigation();
|
||||
@@ -367,6 +368,7 @@ function DashboardContent() {
|
||||
{SessionInfo.userId != "" && !displayedPlan && (
|
||||
<div className="homeCentered">No tiene una suscripción activa.</div>
|
||||
)}
|
||||
{displayedPlan && <PendingRatingsBanner variant="banner" />}
|
||||
{displayedPlan && (
|
||||
<div className={style.dashboardGrid}>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ScrollToTop } from "@core/app/components/ScrollTop";
|
||||
import PendingRepeatsCard from "@core/app/components/PendingRepeatsCard/PendingRepeatsCard";
|
||||
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
||||
import EvangelizeBanner from "@components/EvangelizeBanner/EvangelizeBanner";
|
||||
import PendingRatingsBanner from "@components/PendingRatingsBanner/PendingRatingsBanner";
|
||||
import { Button, Tabs, Tab, Box } from "@mui/material";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import turnosXpressTheme from "@core/app/theme/turnosXpress";
|
||||
@@ -295,6 +296,8 @@ export default function MyAppointmentsPage() {
|
||||
|
||||
<EvangelizeBanner />
|
||||
|
||||
<PendingRatingsBanner />
|
||||
|
||||
<Box sx={{ width: "100%", mt: 4 }}>
|
||||
{collaboratorAppointments.length > 0 ? (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
.pageShell {
|
||||
--ratings-purple: #330080;
|
||||
--ratings-purple-soft: #6c34c2;
|
||||
--ratings-orange: #ff7a2f;
|
||||
--ratings-pink: #ff2a7f;
|
||||
--ratings-ink: #24143d;
|
||||
--ratings-muted: #695f76;
|
||||
--ratings-line: rgba(97, 54, 148, 0.16);
|
||||
--ratings-surface: rgba(255, 255, 255, 0.86);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 1.25rem 3.5rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
gap: 1.5rem;
|
||||
overflow: hidden;
|
||||
margin: 0 auto 1.25rem;
|
||||
padding: 3rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 34px;
|
||||
background:
|
||||
radial-gradient(circle at 84% 6%, rgba(255, 122, 47, 0.3) 0, rgba(255, 122, 47, 0) 30%),
|
||||
radial-gradient(circle at 8% 18%, rgba(255, 42, 127, 0.18) 0, rgba(255, 42, 127, 0) 32%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(253, 246, 255, 0.94) 46%, rgba(255, 244, 235, 0.96) 100%);
|
||||
box-shadow: 0 24px 70px rgba(51, 0, 128, 0.16);
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
pointer-events: none;
|
||||
border-radius: 33px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.76), rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.heroContent,
|
||||
.heroPanel,
|
||||
.heroStats {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
max-width: 710px;
|
||||
margin: 0.45rem 0 0.85rem;
|
||||
color: var(--ratings-ink);
|
||||
font-size: clamp(2.45rem, 5.8vw, 4.7rem);
|
||||
letter-spacing: -0.055em;
|
||||
line-height: 0.96;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
max-width: 640px;
|
||||
margin: 0;
|
||||
color: var(--ratings-muted);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.heroPanel {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
min-height: 230px;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 1.35rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 26px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.34), transparent 38%),
|
||||
linear-gradient(145deg, var(--ratings-purple) 0%, var(--ratings-purple-soft) 56%, var(--ratings-pink) 100%);
|
||||
color: var(--white);
|
||||
box-shadow: 0 18px 40px rgba(51, 0, 128, 0.24);
|
||||
}
|
||||
|
||||
.panelLabel {
|
||||
align-self: flex-start;
|
||||
padding: 0.42rem 0.7rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.heroPanel strong {
|
||||
margin-top: auto;
|
||||
font-size: 4.25rem;
|
||||
line-height: 0.95;
|
||||
}
|
||||
|
||||
.heroPanel span:last-child {
|
||||
color: rgba(255, 255, 255, 0.84);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.heroStats {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.heroStats div {
|
||||
padding: 1rem 1.1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.heroStats strong {
|
||||
display: block;
|
||||
color: var(--ratings-purple);
|
||||
font-size: 1.45rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.heroStats span {
|
||||
display: block;
|
||||
margin-top: 0.35rem;
|
||||
color: var(--ratings-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.035em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.contentCard {
|
||||
padding: 1.6rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 18px 55px rgba(51, 0, 128, 0.1);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.35rem;
|
||||
padding: 0.35rem 0.25rem 0;
|
||||
}
|
||||
|
||||
.sectionHeader h2 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--ratings-ink);
|
||||
font-size: 1.65rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.sectionHeader p {
|
||||
max-width: 680px;
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--ratings-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.targetLabel {
|
||||
color: var(--ratings-pink);
|
||||
font-size: 0.73rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.075em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.countPill,
|
||||
.datePill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.countPill {
|
||||
padding: 0.58rem 0.9rem;
|
||||
background: linear-gradient(135deg, rgba(255, 122, 47, 0.16), rgba(255, 42, 127, 0.14));
|
||||
color: var(--ratings-purple);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.datePill {
|
||||
min-width: 78px;
|
||||
padding: 0.46rem 0.75rem;
|
||||
border: 1px solid var(--ratings-line);
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
color: var(--ratings-purple);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.ratingList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1.15rem;
|
||||
}
|
||||
|
||||
.ratingCard {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--ratings-line);
|
||||
border-radius: 26px;
|
||||
background:
|
||||
linear-gradient(var(--ratings-surface), var(--ratings-surface)) padding-box,
|
||||
linear-gradient(140deg, rgba(255, 122, 47, 0.42), rgba(255, 42, 127, 0.2), rgba(51, 0, 128, 0.18)) border-box;
|
||||
box-shadow: 0 18px 44px rgba(51, 0, 128, 0.1);
|
||||
}
|
||||
|
||||
.ratingCard::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
right: -70px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(circle, rgba(255, 122, 47, 0.18), transparent 68%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cardTop {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cardIdentity {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.targetIcon,
|
||||
.stateIcon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, var(--ratings-orange) 0%, var(--ratings-pink) 48%, var(--ratings-purple) 100%);
|
||||
color: var(--white);
|
||||
font-weight: 900;
|
||||
box-shadow: 0 10px 24px rgba(255, 42, 127, 0.24);
|
||||
}
|
||||
|
||||
.targetIcon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.cardTop h3 {
|
||||
margin: 0.18rem 0 0;
|
||||
color: var(--ratings-ink);
|
||||
font-size: 1.26rem;
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.contextBox {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
gap: 0.18rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid rgba(51, 0, 128, 0.08);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.contextBox span,
|
||||
.contextBox small {
|
||||
color: var(--ratings-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.contextBox strong {
|
||||
color: var(--ratings-ink);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.ratingControl {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 1.15rem 1rem;
|
||||
border: 1px solid rgba(255, 122, 47, 0.18);
|
||||
border-radius: 22px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 0%, rgba(255, 122, 47, 0.18), transparent 46%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(255, 247, 241, 0.78));
|
||||
}
|
||||
|
||||
.ratingControl span {
|
||||
color: var(--ratings-ink);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.ratingControl :global(.MuiRating-root) {
|
||||
font-size: 2.45rem;
|
||||
color: #ffb23f;
|
||||
filter: drop-shadow(0 6px 10px rgba(255, 122, 47, 0.18));
|
||||
}
|
||||
|
||||
.ratingControl small {
|
||||
color: var(--ratings-muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ratingCard :global(.MuiOutlinedInput-root) {
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
.ratingCard :global(.MuiFormHelperText-root) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 22px rgba(255, 42, 127, 0.22);
|
||||
}
|
||||
|
||||
.stateCard {
|
||||
display: flex;
|
||||
min-height: 310px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
padding: 2.5rem;
|
||||
text-align: center;
|
||||
border: 1px dashed rgba(97, 54, 148, 0.28);
|
||||
border-radius: 26px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 0%, rgba(255, 122, 47, 0.16), transparent 34%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.86), rgba(253, 246, 255, 0.82));
|
||||
}
|
||||
|
||||
.stateIcon {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.stateCard h3 {
|
||||
margin: 0;
|
||||
color: var(--ratings-ink);
|
||||
font-size: 1.35rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.stateCard p {
|
||||
max-width: 460px;
|
||||
margin: 0;
|
||||
color: var(--ratings-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero,
|
||||
.ratingList {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.heroPanel {
|
||||
min-height: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pageShell {
|
||||
padding: 5.75rem 0.9rem 2.25rem;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.contentCard {
|
||||
padding: 1.15rem;
|
||||
border-radius: 26px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: clamp(2.25rem, 12vw, 3.25rem);
|
||||
}
|
||||
|
||||
.heroStats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sectionHeader,
|
||||
.cardTop {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.cardIdentity {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.datePill {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ratingCard {
|
||||
padding: 1rem;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.ratingControl :global(.MuiRating-root) {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.ratingControl :global(.MuiRating-root) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
"use client";
|
||||
|
||||
import AnimatedContainer from "@components/AnimatedConainer/AnimatedContainer";
|
||||
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
||||
import { ScrollToTop } from "@core/app/components/ScrollTop";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
import turnosXpressTheme from "@core/app/theme/turnosXpress";
|
||||
import { useAlert } from "@core/Store/Alert.Store";
|
||||
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
|
||||
import { useSessionStore } from "@core/Store/Sesion.Store";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
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 { PendingRatingItem, RATING_COMMENT_MAX_LENGTH, RatingTargetType } from "@core/Models/Ratings.model";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
dayjs.locale("es");
|
||||
|
||||
const targetTypeLabel = (targetType: RatingTargetType) => {
|
||||
if (targetType === RatingTargetType.COMPANY) return "organización";
|
||||
if (targetType === RatingTargetType.SERVICE) return "servicio";
|
||||
return "profesional";
|
||||
};
|
||||
|
||||
const targetTypeIcon = (targetType: RatingTargetType) => {
|
||||
if (targetType === RatingTargetType.COMPANY) return "TX";
|
||||
if (targetType === RatingTargetType.SERVICE) return "SV";
|
||||
return "PR";
|
||||
};
|
||||
|
||||
export default function PendingRatingsPage() {
|
||||
const [pendingRatings, setPendingRatings] = useState<PendingRatingItem[]>([]);
|
||||
const [scores, setScores] = useState<Record<string, number>>({});
|
||||
const [comments, setComments] = useState<Record<string, string>>({});
|
||||
const [submittingKey, setSubmittingKey] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const SessionInfo = useSessionStore();
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const alert = useAlert();
|
||||
const { goTo } = useNavigation();
|
||||
|
||||
const loadPendingRatings = () => {
|
||||
if (!SessionInfo.userId) {
|
||||
setPendingRatings([]);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
pendingRatingsByUser({ sessionUser: SessionInfo.userId })
|
||||
.then((res) => setPendingRatings(res))
|
||||
.catch((error) => {
|
||||
alert.showError(error.message || "No pudimos cargar tus calificaciones pendientes.");
|
||||
console.error("Failed to load pending ratings", {
|
||||
message: error?.message,
|
||||
code: error?.code,
|
||||
desc: error?.desc,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPendingRatings();
|
||||
}, [SessionInfo.userId]);
|
||||
|
||||
const submitRating = (item: PendingRatingItem) => {
|
||||
const itemKey = `${item.targetType}:${item.targetId}`;
|
||||
const score = scores[itemKey];
|
||||
|
||||
if (!score) {
|
||||
alert.showError("Selecciona una puntuación para continuar.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmittingKey(itemKey);
|
||||
createRating({
|
||||
appointmentId: item.appointmentId,
|
||||
targetType: item.targetType,
|
||||
targetId: item.targetId,
|
||||
score,
|
||||
comment: comments[itemKey] || "",
|
||||
sessionUser: SessionInfo.userId,
|
||||
})
|
||||
.then(() => {
|
||||
alert.showSuccess("Gracias por compartir tu calificación.");
|
||||
setScores((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[itemKey];
|
||||
return next;
|
||||
});
|
||||
setComments((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[itemKey];
|
||||
return next;
|
||||
});
|
||||
loadPendingRatings();
|
||||
})
|
||||
.catch((error) => alert.showError(error.message))
|
||||
.finally(() => setSubmittingKey(""));
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatedContainer
|
||||
color="#aeaeae"
|
||||
minSize={150}
|
||||
maxSize={500}
|
||||
items={10}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ScrollToTop />
|
||||
<HeaderConfProvider />
|
||||
<main className={styles.pageShell}>
|
||||
<section className={styles.hero}>
|
||||
<div className={styles.heroContent}>
|
||||
<span className={styles.eyebrow}>Calificaciones pendientes</span>
|
||||
<h1>Contanos cómo fue tu experiencia</h1>
|
||||
<p>
|
||||
Tus comentarios ayudan a mejorar las recomendaciones y a que otras personas elijan con más
|
||||
confianza.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className={styles.heroStats} aria-label="Resumen de calificaciones pendientes">
|
||||
<div>
|
||||
<strong>{isLoading ? "..." : pendingRatings.length}</strong>
|
||||
<span>Pendientes</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>2 min</strong>
|
||||
<span>Promedio</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.contentCard}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div>
|
||||
<span className={styles.eyebrow}>Experiencias por calificar</span>
|
||||
<h2>Turnos recientes</h2>
|
||||
<p>Elegí una puntuación, sumá un comentario si querés y enviá cada experiencia por separado.</p>
|
||||
</div>
|
||||
{!isLoading && pendingRatings.length > 0 && (
|
||||
<span className={styles.countPill}>{pendingRatings.length} pendientes</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className={styles.stateCard}>
|
||||
<div className={styles.stateIcon}>...</div>
|
||||
<h3>Cargando tus experiencias...</h3>
|
||||
<p>Estamos buscando las calificaciones que tenés pendientes.</p>
|
||||
</div>
|
||||
) : pendingRatings.length === 0 ? (
|
||||
<div className={styles.stateCard}>
|
||||
<div className={styles.stateIcon}>OK</div>
|
||||
<h3>No tenés calificaciones pendientes</h3>
|
||||
<p>Cuando completes un turno, vas a poder compartir tu opinión desde acá.</p>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button variant="contained" color="primary" onClick={() => goTo("/landing/my-appointments")}>
|
||||
Volver a Mis Turnos
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.ratingList}>
|
||||
{pendingRatings.map((item) => {
|
||||
const itemKey = `${item.targetType}:${item.targetId}`;
|
||||
return (
|
||||
<article key={itemKey} className={styles.ratingCard}>
|
||||
<div className={styles.cardTop}>
|
||||
<div className={styles.cardIdentity}>
|
||||
<div className={styles.targetIcon}>{targetTypeIcon(item.targetType)}</div>
|
||||
<div>
|
||||
<span className={styles.targetLabel}>{targetTypeLabel(item.targetType)}</span>
|
||||
<h3>{item.targetName}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<span className={styles.datePill}>{dayjs(item.appointmentDate).format("D MMM")}</span>
|
||||
</div>
|
||||
<div className={styles.contextBox}>
|
||||
<span>Turno realizado</span>
|
||||
<strong>{item.serviceName}</strong>
|
||||
<small>{dayjs(item.appointmentDate).format("dddd D [de] MMMM [de] YYYY")}</small>
|
||||
</div>
|
||||
<Box className={styles.ratingControl}>
|
||||
<span>Tu calificación</span>
|
||||
<Rating
|
||||
name={`rating-${itemKey}`}
|
||||
value={scores[itemKey] || 0}
|
||||
onChange={(_, value) =>
|
||||
setScores((prev) => ({ ...prev, [itemKey]: value || 0 }))
|
||||
}
|
||||
size="large"
|
||||
/>
|
||||
<small>{scores[itemKey] ? `${scores[itemKey]} de 5` : "Tocá las estrellas"}</small>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Comentario opcional"
|
||||
value={comments[itemKey] || ""}
|
||||
onChange={(event) =>
|
||||
setComments((prev) => ({ ...prev, [itemKey]: event.target.value }))
|
||||
}
|
||||
multiline
|
||||
minRows={2}
|
||||
size="small"
|
||||
fullWidth
|
||||
inputProps={{ maxLength: RATING_COMMENT_MAX_LENGTH }}
|
||||
helperText={`${(comments[itemKey] || "").length}/${RATING_COMMENT_MAX_LENGTH}`}
|
||||
/>
|
||||
<div className={styles.actions}>
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={submittingKey === itemKey}
|
||||
onClick={() => submitRating(item)}
|
||||
>
|
||||
{submittingKey === itemKey ? "Enviando..." : "Enviar calificación"}
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</AnimatedContainer>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { createAppointment, getAvailableDates } from "./Appointment.Service";
|
||||
import { useAlert } from "@core/Store/Alert.Store";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
import MaterialAlert from "@mui/material/Alert";
|
||||
import PendingRatingsBanner from "@components/PendingRatingsBanner/PendingRatingsBanner";
|
||||
|
||||
export interface AppointmentProps {
|
||||
companyId: string;
|
||||
@@ -200,6 +201,7 @@ export default function Appointment(props: AppointmentProps) {
|
||||
)}
|
||||
{SessionInfo.loged && (
|
||||
<div style={{ marginLeft: "30px", marginRight: "30px", textAlign: "left" }}>
|
||||
<PendingRatingsBanner />
|
||||
<TextObjectFilter
|
||||
placeholder="Seleccionar Profesional:"
|
||||
content={employeeFilterContent}
|
||||
|
||||
Reference in New Issue
Block a user