feat: add public link and QR code generation flow to assistant
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user