57c5deb3bd
- Added `qrcode-tx-assistant.png` for transaction assistant functionality. - Added `qrcode-tx-login.png` for user login process. - Added `qrcode-tx-plans.png` for displaying available plans.
717 lines
40 KiB
TypeScript
717 lines
40 KiB
TypeScript
"use client";
|
||
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, useRef, useState, Suspense } from "react";
|
||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||
import {
|
||
cancellSubscription,
|
||
getSuscriptionInitPoint,
|
||
getDashboardSummary,
|
||
DashboardSummaryResult,
|
||
getCompanyMetrics,
|
||
DashboardMetrics,
|
||
verifyPendingPlanPayment
|
||
} 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 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 HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||
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();
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const searchParams = useSearchParams();
|
||
const SessionInfo = useSessionStore();
|
||
const eventHandler = useEventHandlerStore();
|
||
const alert = useAlert();
|
||
const confirm = useConfirmStore();
|
||
const infoEmail = process.env.NEXT_PUBLIC_INFO_EMAIL || "info@turnosxpress.com.ar";
|
||
|
||
const [showOnlyMine, setShowOnlyMine] = useState(false);
|
||
const [isAgendaCollapsed, setIsAgendaCollapsed] = useState(true);
|
||
|
||
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);
|
||
const [isVerifyingPayment, setIsVerifyingPayment] = useState(false);
|
||
const verifiedPaymentReturnKey = useRef<string | null>(null);
|
||
|
||
const getErrorMessage = (error: unknown): string => {
|
||
if (error && typeof error === "object" && "format" in error && typeof error.format === "function") {
|
||
return error.format();
|
||
}
|
||
|
||
if (error instanceof Error) {
|
||
return error.message;
|
||
}
|
||
|
||
return "Ha ocurrido un error inesperado";
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (SessionInfo.userId && !SessionInfo.subscription) {
|
||
goTo("/");
|
||
}
|
||
|
||
if (SessionInfo.subscription?.plan?.id) {
|
||
setSubscription(SessionInfo.subscription);
|
||
}
|
||
|
||
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) {
|
||
loadCompanyMetrics(selectedOrgId);
|
||
}
|
||
}, [selectedOrgId, SessionInfo.userId]);
|
||
|
||
const loadCompanyMetrics = async (companyId: string) => {
|
||
setCompanySubscription(null);
|
||
setCompanyMetrics(null);
|
||
setIsCompanyOwner(false);
|
||
try {
|
||
const res = await getCompanyMetrics({ sessionUser: SessionInfo.userId, companyId });
|
||
setCompanyMetrics(res.metrics);
|
||
setCompanySubscription(res.subscription);
|
||
setSubscription(res.subscription);
|
||
setIsCompanyOwner(res.isOwner);
|
||
if (res.isOwner) {
|
||
SessionInfo.setSubscription(res.subscription);
|
||
}
|
||
} catch (e) {
|
||
const errMsg = getErrorMessage(e);
|
||
console.warn("Acceso denegado a métricas de la empresa: ", errMsg);
|
||
setCompanyMetrics(null);
|
||
setCompanySubscription(null);
|
||
setIsCompanyOwner(false);
|
||
}
|
||
};
|
||
|
||
const refreshDashboardSubscriptionData = async () => {
|
||
await loadDashboardData();
|
||
if (selectedOrgId) {
|
||
await loadCompanyMetrics(selectedOrgId);
|
||
}
|
||
};
|
||
|
||
const getPaymentLink = () => {
|
||
const pendingPaymentInitPoint = displayedSubscription?.pendingPaymentInitPoint;
|
||
|
||
if (pendingPaymentInitPoint) {
|
||
goTo(pendingPaymentInitPoint);
|
||
return;
|
||
}
|
||
|
||
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 handleVerifyPendingPayment = async () => {
|
||
setIsVerifyingPayment(true);
|
||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||
try {
|
||
const response = await verifyPendingPlanPayment({ sessionUser: SessionInfo.userId });
|
||
if (response.approved) {
|
||
alert.showSuccess(response.message);
|
||
await refreshDashboardSubscriptionData();
|
||
return;
|
||
}
|
||
|
||
if (["rejected", "failed", "cancelled"].includes(response.status)) {
|
||
alert.showWarning(response.message);
|
||
await refreshDashboardSubscriptionData();
|
||
return;
|
||
}
|
||
|
||
alert.showWarning(response.message);
|
||
} catch (error) {
|
||
alert.showError(getErrorMessage(error));
|
||
} finally {
|
||
setIsVerifyingPayment(false);
|
||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!SessionInfo.userId) return;
|
||
|
||
const hasPaymentReturnParams = [
|
||
"mp_return",
|
||
"status",
|
||
"collection_status",
|
||
"payment_id",
|
||
"preference_id",
|
||
"merchant_order_id",
|
||
].some((param) => searchParams.has(param));
|
||
|
||
if (!hasPaymentReturnParams) return;
|
||
|
||
const paymentReturnKey = searchParams.toString();
|
||
if (verifiedPaymentReturnKey.current === paymentReturnKey) return;
|
||
|
||
verifiedPaymentReturnKey.current = paymentReturnKey;
|
||
|
||
handleVerifyPendingPayment().finally(() => {
|
||
router.replace(pathname, { scroll: false });
|
||
});
|
||
}, [SessionInfo.userId, searchParams, pathname, router]);
|
||
|
||
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");
|
||
};
|
||
|
||
const isUsingSessionSubscriptionFallback = !companySubscription && Boolean(subscription.plan.id);
|
||
const selectedOrganization = organizations.find((organization) => organization.id === selectedOrgId);
|
||
const displayedSubscription = companySubscription ?? (subscription.plan.id ? subscription : null);
|
||
const displayedPlan = displayedSubscription?.plan;
|
||
const displayedPlanStatusText = displayedPlan?.price === 0 ? "ACTIVO" : getSubStatusText(displayedSubscription?.mpStatus);
|
||
const hasPendingPaymentMetadata = Boolean(displayedSubscription?.pendingPaymentPreferenceId);
|
||
const hasRecentFailedPayment = Boolean(
|
||
displayedSubscription?.lastPaymentStatus &&
|
||
["rejected", "failed", "cancelled"].includes(displayedSubscription.lastPaymentStatus) &&
|
||
displayedSubscription.lastPaymentAt &&
|
||
dayjs().diff(dayjs(displayedSubscription.lastPaymentAt), "day") <= 14
|
||
);
|
||
const selectedCompanyHasPendingPayment = Boolean(
|
||
companySubscription &&
|
||
(companySubscription.mpStatus === MP_SUBS_STATUS.PENDING || companySubscription.pendingPaymentPreferenceId)
|
||
);
|
||
const showPaymentAction = selectedOrgId
|
||
? Boolean(selectedCompanyHasPendingPayment && isCompanyOwner)
|
||
: Boolean(
|
||
(displayedSubscription?.mpStatus === MP_SUBS_STATUS.PENDING || hasPendingPaymentMetadata) &&
|
||
isUsingSessionSubscriptionFallback
|
||
);
|
||
const showFailedPaymentCard = Boolean(hasRecentFailedPayment && !showPaymentAction);
|
||
const showExpiredDowngradeCard = Boolean(
|
||
displayedSubscription?.downgradeReason === "expired" &&
|
||
displayedSubscription.downgradedAt &&
|
||
isCompanyOwner
|
||
);
|
||
const selectedCompanyPlanSupportsRepeats = Boolean(
|
||
companySubscription?.plan &&
|
||
(companySubscription.plan.limitRepeats === -1 || companySubscription.plan.limitRepeats > 0)
|
||
);
|
||
const canLoadPendingRepeats = Boolean(
|
||
selectedCompanyPlanSupportsRepeats &&
|
||
(isCompanyOwner || selectedOrganization?.isAdmin)
|
||
);
|
||
const showNoOrganizationStarterContent = Boolean(displayedPlan && organizations.length === 0);
|
||
const agendaItems = dashboardData?.personalAgenda ?? [];
|
||
const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorUserId === SessionInfo.userId);
|
||
|
||
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: "flex-start",
|
||
alignItems: "center",
|
||
minHeight: "calc(100vh - 60px)",
|
||
}}
|
||
>
|
||
{SessionInfo.userId != "" && !displayedPlan && (
|
||
<div className="homeCentered">No tiene una suscripción activa.</div>
|
||
)}
|
||
{displayedPlan && <PendingRatingsBanner variant="banner" />}
|
||
{displayedPlan && (
|
||
<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" }}>
|
||
<select
|
||
value={selectedOrgId}
|
||
onChange={e => {
|
||
setCompanySubscription(null);
|
||
setCompanyMetrics(null);
|
||
setIsCompanyOwner(false);
|
||
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' }}>{displayedPlan.name}</span>
|
||
{displayedSubscription && (
|
||
<span
|
||
style={{
|
||
backgroundColor:
|
||
displayedPlanStatusText === "ACTIVO"
|
||
? "var(--sub-status-active)"
|
||
: "var(--sub-status-inactive)",
|
||
borderRadius: "12px",
|
||
padding: "4px 10px",
|
||
fontSize: "12px",
|
||
color: "white",
|
||
fontWeight: "bold",
|
||
}}
|
||
>
|
||
{displayedPlanStatusText}
|
||
</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 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 className={classNames(style.actionButton, style.buttonHelp)} onClick={() => goTo("/landing/help")}>
|
||
<HelpOutlineIcon /> Obtener ayuda
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{showPaymentAction && (
|
||
<div className={style.pendingPaymentCard}>
|
||
<div className={style.pendingPaymentContent}>
|
||
<span className={style.pendingPaymentEyebrow}>Pago pendiente</span>
|
||
<h2>Tu plan está esperando confirmación de MercadoPago</h2>
|
||
<p>
|
||
Si ya pagaste, puede tardar unos minutos en actualizarse porque dependemos de la notificación de MercadoPago.
|
||
No te preocupes: podés volver al checkout si quedó incompleto o pedirnos que verifiquemos el pago ahora mismo.
|
||
{" "}Si el problema continúa, escribinos a <a className={style.pendingPaymentLink} href={`mailto:${infoEmail}`}>{infoEmail}</a> y te ayudamos a revisarlo.
|
||
</p>
|
||
</div>
|
||
<div className={style.pendingPaymentActions}>
|
||
<Button
|
||
text="Ir a pagar"
|
||
color="plain"
|
||
width="custom"
|
||
style={{
|
||
margin: 0,
|
||
width: "220px",
|
||
backgroundColor: "#9a4b00",
|
||
color: "white",
|
||
border: "1px solid #9a4b00",
|
||
}}
|
||
onClick={getPaymentLink}
|
||
/>
|
||
<Button
|
||
text={isVerifyingPayment ? "Verificando..." : "Verificar pago"}
|
||
color="plain"
|
||
width="custom"
|
||
style={{
|
||
margin: 0,
|
||
width: "220px",
|
||
backgroundColor: "#fff3d6",
|
||
color: "#9a4b00",
|
||
border: "1px solid rgba(154, 75, 0, 0.28)",
|
||
}}
|
||
onClick={handleVerifyPendingPayment}
|
||
disabled={isVerifyingPayment}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showFailedPaymentCard && (
|
||
<div className={style.failedPaymentCard}>
|
||
<div className={style.pendingPaymentContent}>
|
||
<span className={style.failedPaymentEyebrow}>Pago no aprobado</span>
|
||
<h2>Tu pago no se aprobó y tu plan sigue igual</h2>
|
||
<p>
|
||
El intento de pago fue rechazado, falló o se canceló. No cambiamos tu plan actual ni la fecha de finalización.
|
||
Cuando quieras, podés volver a intentarlo desde los detalles del plan.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showExpiredDowngradeCard && (
|
||
<div className={style.expiredDowngradeCard}>
|
||
<div className={style.pendingPaymentContent}>
|
||
<span className={style.expiredDowngradeEyebrow}>Plan vencido</span>
|
||
<h2>Tu plan pago venció y pasaste al plan básico</h2>
|
||
<p>
|
||
No borramos tu organización ni tus datos. Desde ahora las nuevas acciones usan los límites del plan básico.
|
||
{displayedSubscription?.downgradedFromPlanName && ` Tu plan anterior era ${displayedSubscription.downgradedFromPlanName}.`}
|
||
</p>
|
||
</div>
|
||
<button
|
||
className={style.expiredDowngradeButton}
|
||
onClick={() => goTo(selectedOrgId ? `/landing/upgrade-plan?org=${selectedOrgId}` : "/landing/pricing")}
|
||
>
|
||
Volver a contratar un plan
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{showNoOrganizationStarterContent && (
|
||
<div className={style.starterCard}>
|
||
<div className={style.starterContent}>
|
||
<span className={style.starterEyebrow}>Tu plan {displayedPlan.name} ya está activo</span>
|
||
<h2>Creá tu primera organización para empezar a trabajar</h2>
|
||
<p>
|
||
Todavía no pertenecés a ninguna organización. Podés seguir el flujo guiado con el asistente
|
||
o hacerlo manualmente desde Mis Organizaciones.
|
||
</p>
|
||
</div>
|
||
<div className={style.starterActions}>
|
||
<button className={style.starterPrimaryButton} onClick={() => goTo("/admin/assistant?flow=onboarding-org&action=new-org")}>
|
||
Usar el asistente
|
||
</button>
|
||
<button className={style.starterSecondaryButton} onClick={() => goTo("/admin/org")}>
|
||
Crear manualmente
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showNoOrganizationStarterContent && (
|
||
<div className={style.starterGuideGrid}>
|
||
<section className={style.starterGuideCard}>
|
||
<span className={style.starterGuideNumber}>1</span>
|
||
<h3>Creá tu espacio de trabajo</h3>
|
||
<p>
|
||
El asistente te guía paso a paso para cargar tu negocio, servicios, horarios y colaboradores.
|
||
Es el camino recomendado si querés empezar a recibir reservas.
|
||
</p>
|
||
</section>
|
||
<section className={style.starterGuideCard}>
|
||
<span className={style.starterGuideNumber}>2</span>
|
||
<h3>Compartí tu enlace público</h3>
|
||
<p>
|
||
Cuando termines la configuración vas a tener una página pública para que tus clientes reserven
|
||
turnos sin escribirte por WhatsApp a cada rato.
|
||
</p>
|
||
</section>
|
||
<section className={style.starterGuideCard}>
|
||
<span className={style.starterGuideNumber}>3</span>
|
||
<h3>También podés sumarte a un equipo</h3>
|
||
<p>
|
||
Si trabajás para una organización existente, ahora pueden invitarte y vas a poder gestionar tus
|
||
turnos desde esta misma cuenta.
|
||
</p>
|
||
</section>
|
||
</div>
|
||
)}
|
||
|
||
{showNoOrganizationStarterContent && (
|
||
<div className={style.starterNextStepsPanel}>
|
||
<div>
|
||
<span className={style.starterEyebrow}>Qué podés hacer con tu plan</span>
|
||
<h3>Tu cuenta ya está lista para operar</h3>
|
||
<p>
|
||
Usá este primer paso para preparar la base de tu negocio. Cuando tengas tu organización creada,
|
||
vas a poder administrar servicios, horarios, reservas e invitaciones desde el dashboard.
|
||
</p>
|
||
</div>
|
||
<ul className={style.starterChecklist}>
|
||
<li>Crear una organización para tu negocio o actividad profesional.</li>
|
||
<li>Configurar servicios y horarios disponibles.</li>
|
||
<li>Recibir invitaciones de otras organizaciones y formar parte de su equipo.</li>
|
||
<li>Seguir usando “Mis turnos” para tus reservas como cliente.</li>
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
{/* Left Column (Sidebar) */}
|
||
{!showNoOrganizationStarterContent && <div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||
{/* Agenda del Día */}
|
||
{dashboardData && dashboardData.role !== "NONE" && (
|
||
<div className={style.agendaCard}>
|
||
<button
|
||
className={style.agendaHeader}
|
||
type="button"
|
||
onClick={() => setIsAgendaCollapsed(prev => !prev)}
|
||
>
|
||
<div className={style.agendaHeaderTitleWrap}>
|
||
<h3 style={{ margin: 0 }}>
|
||
{showOnlyMine ? "Mi Agenda de Hoy" : "Agenda General de Hoy"}
|
||
</h3>
|
||
<span className={style.agendaCounter}>{filteredAgendaItems.length}</span>
|
||
</div>
|
||
<span className={style.agendaToggleIcon}>{isAgendaCollapsed ? "+" : "−"}</span>
|
||
</button>
|
||
{!isAgendaCollapsed && (
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: '15px' }}>
|
||
<label
|
||
style={{ fontSize: '13px', display: 'flex', alignItems: 'center', gap: '5px', cursor: 'pointer', color: '#666' }}
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={showOnlyMine}
|
||
onChange={(e) => setShowOnlyMine(e.target.checked)}
|
||
/>
|
||
Solo mis turnos
|
||
</label>
|
||
</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>
|
||
)}
|
||
|
||
{/* 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/subscription/${companySubscription.plan.id}?renew=1`)}
|
||
onViewDetails={() => goTo("/landing/current-plan")}
|
||
onCancel={cancelarSubscription}
|
||
/>
|
||
)}
|
||
</div>}
|
||
|
||
{/* Right Column (Main Content) */}
|
||
{!showNoOrganizationStarterContent && <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 && canLoadPendingRepeats && (
|
||
<PendingRepeatsCard
|
||
companyId={selectedOrgId}
|
||
allowEmployeeFilter={true}
|
||
/>
|
||
)}
|
||
</div>}
|
||
</div>
|
||
)}
|
||
</AnimatedContainer>
|
||
</Secure>
|
||
</Suspense>
|
||
</ThemeProvider>
|
||
);
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
return (
|
||
<Suspense fallback={<SuspenseLoading />}>
|
||
<DashboardContent />
|
||
</Suspense>
|
||
);
|
||
}
|