feat: add collapsible state and item count to dashboard agenda component
This commit is contained in:
@@ -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}>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user