first commit
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
"use client";
|
||||
import MetricGraph from "@components/MetricGraph/MetricGraph";
|
||||
import AnimatedContainer from "@core/app/components/AnimatedConainer/AnimatedContainer";
|
||||
import Button from "@core/app/components/Button/Button";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
import turnosXpressTheme from "@core/app/theme/turnosXpress";
|
||||
import { useConfirmStore } from "@core/Store/Confirm.Store";
|
||||
import { useSessionStore } from "@core/Store/Sesion.Store";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading";
|
||||
import Secure from "@components/Secure/Secure";
|
||||
import { useEffect, useState, Suspense } from "react";
|
||||
import {
|
||||
cancellSubscription,
|
||||
getSuscriptionInitPoint,
|
||||
getDashboardSummary,
|
||||
DashboardSummaryResult,
|
||||
getCompanyMetrics,
|
||||
DashboardMetrics
|
||||
} from "./Dashboard.Service";
|
||||
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
|
||||
import { useAlert } from "@core/Store/Alert.Store";
|
||||
import { loadOrganizationsByUser } from "@core/app/admin/org/Org.Service";
|
||||
import { MyOranizationsView } from "@core/Models/Company.model";
|
||||
import {
|
||||
getSubStatusText,
|
||||
ISubscriptionInfo,
|
||||
MP_SUBS_STATUS,
|
||||
} from "@core/Models/Subscription.Model";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import style from "./style.module.css";
|
||||
import { blurElementsOnLoad } from "@core/helpers/blur";
|
||||
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
||||
import classNames from "classnames";
|
||||
import { ScrollToTop } from "@core/app/components/ScrollTop";
|
||||
import { isNull } from "@core/app/helpers/IsNull";
|
||||
|
||||
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
|
||||
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
|
||||
import ChatBubbleOutlineOutlinedIcon from "@mui/icons-material/ChatBubbleOutlineOutlined";
|
||||
import AddCircleOutlineOutlinedIcon from "@mui/icons-material/AddCircleOutlineOutlined";
|
||||
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";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { goTo } = useNavigation();
|
||||
const SessionInfo = useSessionStore();
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirmStore();
|
||||
|
||||
const [metrics, setMetrics] = useState({
|
||||
organizationsCount: 0,
|
||||
employeesCount: 0,
|
||||
clientsCount: 0,
|
||||
appointmentsCount: 0,
|
||||
servicesCount: 0,
|
||||
repeatsCount: 0,
|
||||
});
|
||||
|
||||
const [showOnlyMine, setShowOnlyMine] = useState(false);
|
||||
|
||||
const [subscription, setSubscription] = useState<ISubscriptionInfo>({
|
||||
plan: {
|
||||
name: "Free",
|
||||
limitOrganizations: 1,
|
||||
limitEmployees: 1,
|
||||
limitServices: 1,
|
||||
limitAppointments: 1,
|
||||
limitClients: 1,
|
||||
limitRepeats: 1,
|
||||
description: "",
|
||||
features: [],
|
||||
code: "",
|
||||
price: 0,
|
||||
annualPrice: 0,
|
||||
id: "",
|
||||
mailNotifications: false,
|
||||
smsNotifications: false,
|
||||
wapNotifications: false,
|
||||
bot: false,
|
||||
active: false,
|
||||
dateLimit: false,
|
||||
payments: false,
|
||||
},
|
||||
id: "",
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
isActive: false,
|
||||
autoRenew: false,
|
||||
mpStatus: "",
|
||||
mpDateCreated: undefined,
|
||||
});
|
||||
|
||||
const [dashboardData, setDashboardData] = useState<DashboardSummaryResult | null>(null);
|
||||
const [organizations, setOrganizations] = useState<MyOranizationsView[]>([]);
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string>("");
|
||||
|
||||
// Per-company states
|
||||
const [companyMetrics, setCompanyMetrics] = useState<DashboardMetrics | null>(null);
|
||||
const [companySubscription, setCompanySubscription] = useState<ISubscriptionInfo | null>(null);
|
||||
const [isCompanyOwner, setIsCompanyOwner] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (SessionInfo.userId && !SessionInfo.subscription) {
|
||||
goTo("/");
|
||||
}
|
||||
|
||||
blurElementsOnLoad();
|
||||
|
||||
if (SessionInfo.userId) {
|
||||
loadDashboardData();
|
||||
}
|
||||
}, [SessionInfo.userId]);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const data = await getDashboardSummary({ sessionUser: SessionInfo.userId });
|
||||
setDashboardData(data);
|
||||
|
||||
const orgs = await loadOrganizationsByUser(SessionInfo.userId);
|
||||
setOrganizations(orgs);
|
||||
if (orgs.length > 0) {
|
||||
setSelectedOrgId(orgs[0].id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error loading dashboard data", e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOrgId && SessionInfo.userId) {
|
||||
getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId: selectedOrgId })
|
||||
.then(res => {
|
||||
setCompanyMetrics(res.metrics);
|
||||
setCompanySubscription(res.subscription);
|
||||
setIsCompanyOwner(res.isOwner);
|
||||
})
|
||||
.catch(e => {
|
||||
const errMsg = e.format ? e.format() : e.message;
|
||||
console.warn("Acceso denegado a métricas de la empresa: ", errMsg);
|
||||
setCompanyMetrics(null);
|
||||
setCompanySubscription(null);
|
||||
setIsCompanyOwner(false);
|
||||
});
|
||||
}
|
||||
}, [selectedOrgId, SessionInfo.userId]);
|
||||
|
||||
const getPaymentLink = () => {
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
getSuscriptionInitPoint({
|
||||
sessionUser: SessionInfo.userId,
|
||||
})
|
||||
.then((response) => {
|
||||
goTo(response.init_point);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
};
|
||||
|
||||
const cancelarSubscription = () => {
|
||||
confirm.show(
|
||||
"Estas a punto de cancelar tu suscripción. Al realizar esta acción dejaras de tener acceso a la información de tus organizaciones cuando se termine el periodo contratado. Puedes volver a suscribirte a cualquier otro plan y recuperarlo en el futuro. Estas seguro que quieres cancelar tu suscripcion?",
|
||||
() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
cancellSubscription({
|
||||
sessionUser: SessionInfo.userId,
|
||||
})
|
||||
.then(() => {
|
||||
SessionInfo.clearSubscription();
|
||||
setSubscription({
|
||||
...SessionInfo.subscription,
|
||||
mpStatus: MP_SUBS_STATUS.CANCELLED,
|
||||
});
|
||||
alert.showSuccess("La suscripción ha sido cancelada con éxito");
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.showWarning(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
alert.showWarning("No se ha cancelado la suscripción");
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleNewAppointmentClick = () => {
|
||||
if (organizations.length === 0) {
|
||||
alert.showWarning("No perteneces a ninguna organización");
|
||||
return;
|
||||
}
|
||||
|
||||
if (organizations.length === 1) {
|
||||
const todayStr = dayjs().startOf('day').toISOString();
|
||||
goTo(`/admin/org/profile/${organizations[0].id}/appointments/${todayStr}/create`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mostrar listado de organizaciones para elegir usando alert / modal
|
||||
// Simplificado con un confirm adaptado o directamente yendo a mis organizaciones si no hay modal para esto.
|
||||
// Dado que turnosXpress usa confirm.show para preguntas, la mejor UI aquí sin crear un modal nuevo
|
||||
// es ir a /admin/org y pedirle que elija desde allí, o crear un modal custom.
|
||||
// Como pidió que se despliegue un listado, si tienen un componente modal, podríamos usarlo.
|
||||
// Por practicidad y reuso, voy a redirigirlo a Mis Organizaciones si son múltiples, pero en el comentario decia "desplegar un listado".
|
||||
// Voy a intentar redirigir a /admin/org y que elija ahí.
|
||||
alert.showWarning("Perteneces a múltiples organizaciones, selecciona una para crear el turno.");
|
||||
goTo("/admin/org");
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<ScrollToTop />
|
||||
<HeaderConfProvider />
|
||||
<Suspense fallback={<SuspenseLoading />}>
|
||||
<Secure>
|
||||
<AnimatedContainer
|
||||
color="#aeaeae"
|
||||
minSize={150}
|
||||
maxSize={500}
|
||||
items={10}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{SessionInfo.userId != "" && subscription.plan.name == "" && (
|
||||
<div className="homeCentered">No tiene una suscripción activa.</div>
|
||||
)}
|
||||
{subscription.plan.name != "" && (
|
||||
<div className={style.dashboardGrid}>
|
||||
|
||||
{/* Welcome Header */}
|
||||
<div className={style.welcomeCard}>
|
||||
{organizations.length > 1 && (
|
||||
<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
|
||||
value={selectedOrgId}
|
||||
onChange={e => setSelectedOrgId(e.target.value)}
|
||||
style={{ padding: "8px 12px", borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "14px", fontWeight: "500", backgroundColor: "#f9fafb", cursor: "pointer", outline: "none" }}
|
||||
>
|
||||
{organizations.map(org => (
|
||||
<option key={org.id} value={org.id}>{org.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.welcomeContainer}>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: '5px'}}>
|
||||
<h3>Bienvenido, {SessionInfo.userFullName}</h3>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<span style={{ fontWeight: 'bold' }}>{subscription.plan.name}</span>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor:
|
||||
getSubStatusText(subscription.mpStatus) ===
|
||||
"ACTIVO"
|
||||
? "var(--sub-status-active)"
|
||||
: "var(--sub-status-inactive)",
|
||||
borderRadius: "12px",
|
||||
padding: "4px 10px",
|
||||
fontSize: "12px",
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{getSubStatusText(subscription.mpStatus)}
|
||||
</span>
|
||||
</div>
|
||||
{subscription.endDate && subscription.plan.price > 0 && (
|
||||
<span style={{ fontSize: '14px', color: '#666' }}>
|
||||
Vigente hasta el {dayjs(subscription.endDate).format("DD/MM/YYYY")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subscription.mpStatus === MP_SUBS_STATUS.PENDING && (
|
||||
<Button
|
||||
text="Ir a pagar"
|
||||
color="primary"
|
||||
style={{ marginTop: "10px" }}
|
||||
onClick={getPaymentLink}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={style.quickActions}>
|
||||
<div className={classNames(style.actionButton, style.buttonOrganizations)} onClick={() => goTo("/admin/org")}>
|
||||
<StorefrontOutlinedIcon /> Mis Organizaciones
|
||||
</div>
|
||||
<div className={classNames(style.actionButton, style.buttonNotification)} onClick={() => goTo("/user/profile/notifications")}>
|
||||
<NotificationsNoneOutlinedIcon /> Notificaciones
|
||||
</div>
|
||||
<div className={classNames(style.actionButton, style.buttonMessenger)} onClick={() => goTo("/messenger")}>
|
||||
<ChatBubbleOutlineOutlinedIcon /> Mensajes
|
||||
</div>
|
||||
<div className={classNames(style.actionButton, style.buttonOrganizations)} onClick={() => goTo("/admin/assistant")} style={{backgroundColor: "#800020", color: "white"}}>
|
||||
<AutoAwesomeIcon /> Asistente
|
||||
</div>
|
||||
<div className={classNames(style.actionButton)} style={{backgroundColor: "var(--wine-red)"}} onClick={handleNewAppointmentClick}>
|
||||
<AddCircleOutlineOutlinedIcon /> Nuevo Turno
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Left Column (Sidebar) */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
{/* Agenda del Día */}
|
||||
{dashboardData && dashboardData.role !== "NONE" && (
|
||||
<div className={style.agendaCard}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
||||
<h3 style={{ margin: 0 }}>
|
||||
{showOnlyMine ? "Mi Agenda de Hoy" : "Agenda General de Hoy"}
|
||||
</h3>
|
||||
<label style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '5px', cursor: 'pointer', color: '#666' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showOnlyMine}
|
||||
onChange={(e) => setShowOnlyMine(e.target.checked)}
|
||||
/>
|
||||
Solo mis turnos
|
||||
</label>
|
||||
</div>
|
||||
<div className={style.agendaList}>
|
||||
{dashboardData.personalAgenda.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId).length === 0 && (
|
||||
<div style={{color: '#666', marginTop: '10px'}}>
|
||||
No hay turnos programados para hoy con estos filtros.
|
||||
</div>
|
||||
)}
|
||||
{dashboardData.personalAgenda
|
||||
.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId)
|
||||
.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>
|
||||
)}
|
||||
|
||||
{/* Plan Metrics (Only for ADMIN of the selected org) */}
|
||||
{companySubscription && companyMetrics && (
|
||||
<PlanMetricsWidget
|
||||
subscription={companySubscription}
|
||||
metrics={companyMetrics}
|
||||
isOwner={isCompanyOwner}
|
||||
onUpgrade={() => {
|
||||
const freePlan = companySubscription.plan.price === 0;
|
||||
|
||||
if (freePlan) {
|
||||
goTo("/landing/pricing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
companySubscription.mpStatus !==
|
||||
MP_SUBS_STATUS.CANCELLED
|
||||
) {
|
||||
confirm.show(
|
||||
"Para cambiarte a un plan superior, debes cancelar primero el plan actual.",
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
return;
|
||||
}
|
||||
goTo("/landing/pricing");
|
||||
}}
|
||||
onRenew={() => goTo("/landing/pricing")}
|
||||
onCancel={cancelarSubscription}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column (Main Content) */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
{/* Rendimiento Financiero (Only for ADMIN) */}
|
||||
{dashboardData && dashboardData.role === "ADMIN" && (
|
||||
<FinancialWidget
|
||||
sessionUser={SessionInfo.userId}
|
||||
selectedOrgId={selectedOrgId}
|
||||
organizationName={organizations.find(o => o.id === selectedOrgId)?.name}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Repeats Pending Card */}
|
||||
{dashboardData && selectedOrgId && (
|
||||
<PendingRepeatsCard
|
||||
companyId={selectedOrgId}
|
||||
allowEmployeeFilter={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatedContainer>
|
||||
</Secure>
|
||||
</Suspense>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user