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 ReviewsIcon from '@mui/icons-material/Reviews';
|
||||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
|
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
|
||||||
|
import QrCode2Icon from '@mui/icons-material/QrCode2';
|
||||||
import API from "@services/Api.Service";
|
import API from "@services/Api.Service";
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +78,15 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
|
|||||||
keywords: ["opiniones", "puntuaciones", "calificaciones", "profesionales", "visibilidad", "reseñas", "publico"],
|
keywords: ["opiniones", "puntuaciones", "calificaciones", "profesionales", "visibilidad", "reseñas", "publico"],
|
||||||
icon: ReviewsIcon
|
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",
|
id: "notifications",
|
||||||
flowId: "notifications",
|
flowId: "notifications",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import TextTime from "@components/TextTime/TextTime";
|
|||||||
import { NotificationChannel, ReminderRule } from "@models/NotificationPreferences.model";
|
import { NotificationChannel, ReminderRule } from "@models/NotificationPreferences.model";
|
||||||
import { OrganizationClientView } from "@models/Clients.model";
|
import { OrganizationClientView } from "@models/Clients.model";
|
||||||
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
|
||||||
|
import QrCode2Icon from "@mui/icons-material/QrCode2";
|
||||||
|
import { getSlug } from "@core/app/helpers/Slug";
|
||||||
|
|
||||||
interface StepEngineProps {
|
interface StepEngineProps {
|
||||||
flowId: string;
|
flowId: string;
|
||||||
@@ -115,6 +117,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
const [orgShowPublicProfessionals, setOrgShowPublicProfessionals] = useState(true);
|
const [orgShowPublicProfessionals, setOrgShowPublicProfessionals] = useState(true);
|
||||||
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
|
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
|
||||||
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
|
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
|
||||||
|
const [publicLinkCopied, setPublicLinkCopied] = useState(false);
|
||||||
|
|
||||||
// State for Notifications Flow
|
// State for Notifications Flow
|
||||||
const [notificationsScope, setNotificationsScope] = useState<"org" | "client" | null>(null);
|
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));
|
setServicesShowPublicScores(services.every(service => service.showPublicScores !== false));
|
||||||
setServicesShowPublicOpinions(services.every(service => service.showPublicOpinions !== false));
|
setServicesShowPublicOpinions(services.every(service => service.showPublicOpinions !== false));
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
} else if (flowId === "public-link") {
|
||||||
|
setCurrentStepIndex(0);
|
||||||
|
setPublicLinkCopied(false);
|
||||||
} else if (flowId === "setup-collaborator") {
|
} else if (flowId === "setup-collaborator") {
|
||||||
setCurrentStepIndex(0);
|
setCurrentStepIndex(0);
|
||||||
if (initialEmployeeId) {
|
if (initialEmployeeId) {
|
||||||
@@ -336,7 +342,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
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);
|
setIsResuming(true);
|
||||||
API.post<any[]>("companies/get-by-user", { sessionUser: SessionInfo.userId })
|
API.post<any[]>("companies/get-by-user", { sessionUser: SessionInfo.userId })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -357,7 +363,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
.finally(() => setIsResuming(false));
|
.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);
|
setCurrentStepIndex(0);
|
||||||
}
|
}
|
||||||
}, [flowId, SessionInfo.metrics.organizationsCount, SessionInfo.userId, router]);
|
}, [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 }[] = [
|
const NOTIF_CHANNELS: { key: NotificationChannel; label: string; description: string }[] = [
|
||||||
{ key: "whatsapp", label: "WhatsApp", description: "Mensajes automáticos por WhatsApp." },
|
{ key: "whatsapp", label: "WhatsApp", description: "Mensajes automáticos por WhatsApp." },
|
||||||
{ key: "email", label: "Email", description: "Correos electrónicos al cliente." },
|
{ 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 === "setup-collaborator") return renderSetupCollaboratorFlow();
|
||||||
if (flowId === "new-collaborator") return renderNewCollaboratorFlow();
|
if (flowId === "new-collaborator") return renderNewCollaboratorFlow();
|
||||||
if (flowId === "public-visibility") return renderPublicVisibilityFlow();
|
if (flowId === "public-visibility") return renderPublicVisibilityFlow();
|
||||||
|
if (flowId === "public-link") return renderPublicLinkFlow();
|
||||||
if (flowId === "notifications") return renderNotificationsFlow();
|
if (flowId === "notifications") return renderNotificationsFlow();
|
||||||
if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow();
|
if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow();
|
||||||
if (flowId === "disable-schedule") return renderDisableScheduleFlow();
|
if (flowId === "disable-schedule") return renderDisableScheduleFlow();
|
||||||
|
|||||||
Reference in New Issue
Block a user