feat: add collapsible state and item count to dashboard agenda component

This commit is contained in:
2026-07-18 10:54:27 -03:00
parent 8d9a03cd23
commit 81616c7c1c
5 changed files with 183 additions and 128 deletions
@@ -104,6 +104,7 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
return dayjs(d.date).format("DD/MM"); return dayjs(d.date).format("DD/MM");
}); });
const seriesData = chartData.map(d => d.amount); const seriesData = chartData.map(d => d.amount);
const hasChartData = chartData.some(d => d.amount > 0);
return ( return (
<div className={styles.container}> <div className={styles.container}>
@@ -155,70 +156,74 @@ export default function FinancialWidget({ sessionUser, selectedOrgId, organizati
)} )}
</div> </div>
<div className={styles.chartContainer}> {loading && (
{!loading && chartData.length > 0 ? ( <div className={styles.chartContainer}>
<LineChart <div style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#9ca3af'}}>
height={350} Cargando datos...
xAxis={[{ </div>
scaleType: 'point', </div>
data: xAxisData, )}
}]}
series={[ {!loading && hasChartData && (
{ <div className={styles.chartContainer}>
data: seriesData, <LineChart
color: 'rgb(36, 169, 53)', height={350}
area: true, xAxis={[{
showMark: true, scaleType: 'point',
curve: 'monotoneX', data: xAxisData,
valueFormatter: (val: number | null) => val ? formatCurrency(val) : "0", }]}
}, series={[
]} {
grid={{ horizontal: true }} data: seriesData,
margin={{ top: 20, bottom: 40, left: 70, right: 20 }} color: 'rgb(36, 169, 53)',
sx={{ area: true,
'.MuiLineElement-root': { showMark: true,
strokeWidth: 3, curve: 'monotoneX',
}, valueFormatter: (val: number | null) => val ? formatCurrency(val) : "0",
'.MuiAreaElement-root': { },
fill: 'url(#gradient)', ]}
}, grid={{ horizontal: true }}
'.MuiChartsGrid-line': { margin={{ top: 20, bottom: 40, left: 70, right: 20 }}
strokeDasharray: '5 5', sx={{
stroke: '#e5e7eb', '.MuiLineElement-root': {
}, strokeWidth: 3,
'.MuiChartsAxis-line': { },
stroke: 'transparent', '.MuiAreaElement-root': {
}, fill: 'url(#gradient)',
'.MuiChartsAxis-tick': { },
stroke: 'transparent', '.MuiChartsGrid-line': {
}, strokeDasharray: '5 5',
'.MuiChartsAxis-tickLabel': { stroke: '#e5e7eb',
fill: '#9ca3af', },
fontFamily: 'inherit', '.MuiChartsAxis-line': {
fontSize: '12px', stroke: 'transparent',
fontWeight: 500, },
}, '.MuiChartsAxis-tick': {
'.MuiMarkElement-root': { stroke: 'transparent',
stroke: 'rgb(36, 169, 53)', },
strokeWidth: 2, '.MuiChartsAxis-tickLabel': {
fill: '#ffffff', fill: '#9ca3af',
scale: '1.2', fontFamily: 'inherit',
} fontSize: '12px',
}} fontWeight: 500,
> },
<defs> '.MuiMarkElement-root': {
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1"> stroke: 'rgb(36, 169, 53)',
<stop offset="5%" stopColor="rgb(36, 169, 53)" stopOpacity={0.3}/> strokeWidth: 2,
<stop offset="95%" stopColor="rgb(36, 169, 53)" stopOpacity={0.0}/> fill: '#ffffff',
</linearGradient> scale: '1.2',
</defs> }
</LineChart> }}
) : ( >
<div style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#9ca3af'}}> <defs>
{loading ? 'Cargando datos...' : 'No hay datos para este periodo'} <linearGradient id="gradient" x1="0" y1="0" x2="0" y2="1">
</div> <stop offset="5%" stopColor="rgb(36, 169, 53)" stopOpacity={0.3}/>
)} <stop offset="95%" stopColor="rgb(36, 169, 53)" stopOpacity={0.0}/>
</div> </linearGradient>
</defs>
</LineChart>
</div>
)}
</> </>
)} )}
</div> </div>
@@ -29,6 +29,12 @@
font-weight: 500; font-weight: 500;
} }
.validUntil {
font-size: 14px;
color: var(--wine-red);
font-weight: 700;
}
.metricsList { .metricsList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -106,12 +106,12 @@ export default function PlanMetricsWidget({
<div className={styles.widgetContainer}> <div className={styles.widgetContainer}>
<div className={styles.widgetHeader}> <div className={styles.widgetHeader}>
<h3 className={styles.title}>Métricas del Plan {subscription.plan.name}</h3> <h3 className={styles.title}>Métricas del Plan {subscription.plan.name}</h3>
<span className={styles.subtitle}>Uso actual de tu suscripción</span>
{subscription.endDate && subscription.plan.price > 0 && ( {subscription.endDate && subscription.plan.price > 0 && (
<span className={styles.subtitle}> <span className={styles.validUntil}>
Vigente hasta el {dayjs(subscription.endDate).format("DD/MM/YYYY")} Vigente hasta el {dayjs(subscription.endDate).format("DD/MM/YYYY")}
</span> </span>
)} )}
<span className={styles.subtitle}>Uso actual de tu suscripción</span>
</div> </div>
<div className={styles.metricsList}> <div className={styles.metricsList}>
+56 -46
View File
@@ -62,6 +62,7 @@ export default function DashboardPage() {
}); });
const [showOnlyMine, setShowOnlyMine] = useState(false); const [showOnlyMine, setShowOnlyMine] = useState(false);
const [isAgendaCollapsed, setIsAgendaCollapsed] = useState(true);
const [subscription, setSubscription] = useState<ISubscriptionInfo>({ const [subscription, setSubscription] = useState<ISubscriptionInfo>({
plan: { plan: {
@@ -220,6 +221,8 @@ export default function DashboardPage() {
}; };
const displayedSubscription = companySubscription; const displayedSubscription = companySubscription;
const agendaItems = dashboardData?.personalAgenda ?? [];
const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId);
return ( return (
<ThemeProvider theme={turnosXpressTheme}> <ThemeProvider theme={turnosXpressTheme}>
@@ -249,7 +252,6 @@ export default function DashboardPage() {
<div className={style.welcomeCard}> <div className={style.welcomeCard}>
{organizations.length > 1 && ( {organizations.length > 1 && (
<div style={{ position: "absolute", top: "20px", right: "20px", display: "flex", alignItems: "center", gap: "10px" }}> <div style={{ position: "absolute", top: "20px", right: "20px", display: "flex", alignItems: "center", gap: "10px" }}>
<span style={{ fontSize: "13px", fontWeight: "600", color: "#6b7280" }}>Organización:</span>
<select <select
value={selectedOrgId} value={selectedOrgId}
onChange={e => setSelectedOrgId(e.target.value)} onChange={e => setSelectedOrgId(e.target.value)}
@@ -284,24 +286,17 @@ export default function DashboardPage() {
{getSubStatusText(displayedSubscription.mpStatus)} {getSubStatusText(displayedSubscription.mpStatus)}
</span> </span>
)} )}
{displayedSubscription && isCompanyOwner && (
<Button
color="link"
text="Ver detalles del plan"
width="custom"
style={{ margin: 0, padding: 0, fontSize: "14px", fontWeight: "700", color: "var(--wine-red)", minHeight: "auto", width: "fit-content" }}
onClick={() => goTo("/landing/current-plan")}
/>
)}
</div> </div>
</div> </div>
<div className={style.planHeaderActions}>
{displayedSubscription?.endDate && displayedSubscription.plan.price > 0 && (
<span style={{ fontSize: '14px', color: '#666' }}>
Vigente hasta el {dayjs(displayedSubscription.endDate).format("DD/MM/YYYY")}
</span>
)}
{displayedSubscription && isCompanyOwner && (
<Button
color="link"
text="Ver detalle del plan"
width="custom"
style={{ margin: 0, padding: 0, fontSize: "14px", fontWeight: "700", color: "var(--wine-red)", minHeight: "auto", width: "fit-content" }}
onClick={() => goTo("/landing/current-plan")}
/>
)}
</div>
{subscription.mpStatus === MP_SUBS_STATUS.PENDING && ( {subscription.mpStatus === MP_SUBS_STATUS.PENDING && (
<Button <Button
text="Ir a pagar" text="Ir a pagar"
@@ -335,35 +330,50 @@ export default function DashboardPage() {
{/* Agenda del Día */} {/* Agenda del Día */}
{dashboardData && dashboardData.role !== "NONE" && ( {dashboardData && dashboardData.role !== "NONE" && (
<div className={style.agendaCard}> <div className={style.agendaCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}> <button
<h3 style={{ margin: 0 }}> className={style.agendaHeader}
{showOnlyMine ? "Mi Agenda de Hoy" : "Agenda General de Hoy"} type="button"
</h3> onClick={() => setIsAgendaCollapsed(prev => !prev)}
<label style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '5px', cursor: 'pointer', color: '#666' }}> >
<input <div className={style.agendaHeaderTitleWrap}>
type="checkbox" <h3 style={{ margin: 0 }}>
checked={showOnlyMine} {showOnlyMine ? "Mi Agenda de Hoy" : "Agenda General de Hoy"}
onChange={(e) => setShowOnlyMine(e.target.checked)} </h3>
/> <span className={style.agendaCounter}>{agendaItems.length}</span>
Solo mis turnos </div>
</label> <span className={style.agendaToggleIcon}>{isAgendaCollapsed ? "+" : ""}</span>
</div> </button>
<div className={style.agendaList}> {!isAgendaCollapsed && (
{dashboardData.personalAgenda.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId).length === 0 && ( <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: '15px' }}>
<div style={{color: '#666', marginTop: '10px'}}> <label
No hay turnos programados para hoy con estos filtros. style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '5px', cursor: 'pointer', color: '#666' }}
</div> onClick={e => e.stopPropagation()}
)} >
{dashboardData.personalAgenda <input
.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId) type="checkbox"
.map(item => ( checked={showOnlyMine}
<div key={item.id} className={style.agendaItem}> onChange={(e) => setShowOnlyMine(e.target.checked)}
<span style={{fontWeight: 'bold'}}>{dayjs(item.appointmentDate).format("HH:mm")} - {item.clientName}</span> />
<span style={{fontSize: '14px', color: '#666'}}>{item.serviceName}</span> Solo mis turnos
<span style={{fontSize: '12px', color: '#999', marginTop: '5px'}}>{item.collaboratorName}</span> </label>
</div> </div>
))} )}
</div> {!isAgendaCollapsed && (
<div className={style.agendaList}>
{filteredAgendaItems.length === 0 && (
<div style={{color: '#666', marginTop: '10px'}}>
No hay turnos programados para hoy con estos filtros.
</div>
)}
{filteredAgendaItems.map(item => (
<div key={item.id} className={style.agendaItem}>
<span style={{fontWeight: 'bold'}}>{dayjs(item.appointmentDate).format("HH:mm")} - {item.clientName}</span>
<span style={{fontSize: '14px', color: '#666'}}>{item.serviceName}</span>
<span style={{fontSize: '12px', color: '#999', marginTop: '5px'}}>{item.collaboratorName}</span>
</div>
))}
</div>
)}
</div> </div>
)} )}
@@ -35,22 +35,6 @@
width: 100%; width: 100%;
} }
.planHeaderActions {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
width: 100%;
}
@media (min-width: 850px) {
.planHeaderActions {
align-items: flex-end;
margin-left: auto;
width: auto;
}
}
.metricsCard { .metricsCard {
background-color: white; background-color: white;
border-radius: 16px; border-radius: 16px;
@@ -71,6 +55,56 @@
gap: 15px; gap: 15px;
} }
.agendaHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 100%;
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
text-align: left;
}
.agendaHeaderTitleWrap {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.agendaCounter {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
padding: 0 9px;
border-radius: 999px;
background: var(--wine-red);
color: var(--white);
font-size: 13px;
font-weight: 900;
}
.agendaToggleIcon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 32px;
height: 32px;
border-radius: 999px;
background: var(--white);
color: var(--wine-red);
border: 1px solid var(--wine-red);
font-size: 22px;
font-weight: 800;
line-height: 1;
}
.agendaList { .agendaList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;