feat: add public link and QR code generation flow to assistant

This commit is contained in:
2026-07-27 21:50:59 -03:00
parent 20b0363767
commit dc020d94d2
2 changed files with 113 additions and 2 deletions
@@ -18,6 +18,7 @@ import PinDropIcon from '@mui/icons-material/PinDrop';
import ReviewsIcon from '@mui/icons-material/Reviews';
import NotificationsIcon from '@mui/icons-material/Notifications';
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
import QrCode2Icon from '@mui/icons-material/QrCode2';
import API from "@services/Api.Service";
@@ -77,6 +78,15 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
keywords: ["opiniones", "puntuaciones", "calificaciones", "profesionales", "visibilidad", "reseñas", "publico"],
icon: ReviewsIcon
},
{
id: "public-link",
flowId: "public-link",
title: "Link Público y QR",
description: "Obtén el enlace público de tu organización y un código QR para compartirlo.",
category: "Negocio",
keywords: ["link", "enlace", "url", "slug", "qr", "codigo qr", "publico", "reservas"],
icon: QrCode2Icon
},
{
id: "notifications",
flowId: "notifications",
@@ -27,6 +27,8 @@ import TextTime from "@components/TextTime/TextTime";
import { NotificationChannel, ReminderRule } from "@models/NotificationPreferences.model";
import { OrganizationClientView } from "@models/Clients.model";
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
import QrCode2Icon from "@mui/icons-material/QrCode2";
import { getSlug } from "@core/app/helpers/Slug";
interface StepEngineProps {
flowId: string;
@@ -115,6 +117,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
const [orgShowPublicProfessionals, setOrgShowPublicProfessionals] = useState(true);
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
const [publicLinkCopied, setPublicLinkCopied] = useState(false);
// State for Notifications Flow
const [notificationsScope, setNotificationsScope] = useState<"org" | "client" | null>(null);
@@ -285,6 +288,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
setServicesShowPublicScores(services.every(service => service.showPublicScores !== false));
setServicesShowPublicOpinions(services.every(service => service.showPublicOpinions !== false));
}).catch(console.error);
} else if (flowId === "public-link") {
setCurrentStepIndex(0);
setPublicLinkCopied(false);
} else if (flowId === "setup-collaborator") {
setCurrentStepIndex(0);
if (initialEmployeeId) {
@@ -336,7 +342,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
};
React.useEffect(() => {
if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule") {
if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule") {
setIsResuming(true);
API.post<any[]>("companies/get-by-user", { sessionUser: SessionInfo.userId })
.then(response => {
@@ -357,7 +363,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
.finally(() => setIsResuming(false));
}
if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "notifications" || flowId === "whatsapp-bot" || (flowId === "onboarding-org" && action === "new-org")) {
if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || (flowId === "onboarding-org" && action === "new-org")) {
setCurrentStepIndex(0);
}
}, [flowId, SessionInfo.metrics.organizationsCount, SessionInfo.userId, router]);
@@ -4021,6 +4027,100 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
);
};
const getSelectedOrganizationSlug = () => {
if (!selectedOrganization) return "";
return selectedOrganization.slug || (selectedOrganization.name ? getSlug(selectedOrganization.name) : "");
};
const getPublicLink = () => {
const slug = getSelectedOrganizationSlug();
const publicBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar";
return slug ? `${publicBaseUrl.replace(/\/$/, "")}/${slug}` : "";
};
const getPublicLinkQrUrl = () => {
const publicLink = getPublicLink();
return publicLink ? `https://api.qrserver.com/v1/create-qr-code/?size=260x260&format=png&data=${encodeURIComponent(publicLink)}` : "";
};
const handleCopyPublicLink = async () => {
const publicLink = getPublicLink();
if (!publicLink) return;
try {
await navigator.clipboard.writeText(publicLink);
setPublicLinkCopied(true);
window.setTimeout(() => setPublicLinkCopied(false), 2000);
} catch (error) {
console.error("Error copiando el link público:", error);
alert("No se pudo copiar el link. Podés seleccionarlo y copiarlo manualmente.");
}
};
const renderPublicLinkFlow = () => {
const publicLink = getPublicLink();
const publicLinkQrUrl = getPublicLinkQrUrl();
const orgSlug = getSelectedOrganizationSlug();
return (
<QuestionCard
isActive={true}
title="Link público y código QR"
description="Compartí este enlace para que tus clientes entren directo a la página pública de reservas de la organización. También podés usar el código QR en redes, vidriera o piezas impresas."
onNext={() => router.push("/admin/assistant")}
nextLabel="Volver al asistente"
topAccessory={<QrCode2Icon style={{ fontSize: 52, color: "var(--wine-red)" }} />}
>
<div style={{ display: "flex", flexDirection: "column", gap: "18px", width: "100%", maxWidth: "560px", margin: "0 auto" }}>
<div style={{ padding: "16px", borderRadius: "14px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.75)", lineHeight: 1.5, fontSize: "14px" }}>
Organización: <strong style={{ color: "white" }}>{selectedOrganization?.name || "Sin organización seleccionada"}</strong>
{orgSlug && (
<div style={{ marginTop: "6px", color: "rgba(255,255,255,0.62)", fontSize: "13px" }}>
Slug público: <strong style={{ color: "white" }}>{orgSlug}</strong>
</div>
)}
</div>
{publicLink ? (
<>
<div style={{ display: "flex", flexDirection: "column", gap: "10px", padding: "16px", borderRadius: "14px", background: "rgba(255,42,127,0.08)", border: "1px solid rgba(255,42,127,0.32)", boxShadow: "0 0 24px rgba(255,42,127,0.12)" }}>
<span style={{ color: "rgba(255,255,255,0.65)", fontSize: "13px", fontWeight: 600 }}>Link público</span>
<a href={publicLink} target="_blank" rel="noopener noreferrer" style={{ color: "white", fontWeight: 700, wordBreak: "break-all", textDecoration: "none", fontSize: "16px" }}>
{publicLink}
</a>
</div>
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap" }}>
<button type="button" onClick={handleCopyPublicLink} style={{ flex: 1, minWidth: "140px", padding: "12px 16px", borderRadius: "10px", background: "var(--wine-red)", border: "1px solid var(--wine-red)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit" }}>
{publicLinkCopied ? "Link copiado" : "Copiar link"}
</button>
<button type="button" onClick={() => window.open(publicLink, "_blank", "noopener,noreferrer")} style={{ flex: 1, minWidth: "140px", padding: "12px 16px", borderRadius: "10px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.16)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit" }}>
Abrir link
</button>
<a href={`${publicLinkQrUrl}&download=1`} download={`turnosxpress-${orgSlug}-qr.png`} style={{ flex: 1, minWidth: "140px", padding: "12px 16px", borderRadius: "10px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.16)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit", textAlign: "center", textDecoration: "none", boxSizing: "border-box" }}>
Descargar QR
</a>
</div>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "12px", padding: "18px", borderRadius: "16px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)" }}>
<div style={{ padding: "14px", borderRadius: "12px", background: "white", boxShadow: "0 4px 20px rgba(0,0,0,0.3)" }}>
<img src={publicLinkQrUrl} width={260} height={260} alt={`Código QR para ${publicLink}`} style={{ display: "block", maxWidth: "100%", height: "auto" }} />
</div>
<span style={{ color: "rgba(255,255,255,0.62)", fontSize: "13px", textAlign: "center", lineHeight: 1.45 }}>
Este QR apunta al mismo link público. Si cambiás el nombre o slug de la organización, generá y compartí el QR actualizado.
</span>
</div>
</>
) : (
<div style={{ padding: "18px", borderRadius: "14px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.72)", textAlign: "center", lineHeight: 1.5 }}>
No encontramos un slug para esta organización. Revisá el nombre de la organización en la configuración del perfil y volvé a intentar.
</div>
)}
</div>
</QuestionCard>
);
};
const NOTIF_CHANNELS: { key: NotificationChannel; label: string; description: string }[] = [
{ key: "whatsapp", label: "WhatsApp", description: "Mensajes automáticos por WhatsApp." },
{ key: "email", label: "Email", description: "Correos electrónicos al cliente." },
@@ -4494,6 +4594,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
if (flowId === "setup-collaborator") return renderSetupCollaboratorFlow();
if (flowId === "new-collaborator") return renderNewCollaboratorFlow();
if (flowId === "public-visibility") return renderPublicVisibilityFlow();
if (flowId === "public-link") return renderPublicLinkFlow();
if (flowId === "notifications") return renderNotificationsFlow();
if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow();
if (flowId === "disable-schedule") return renderDisableScheduleFlow();