feat: add public visibility configuration flow for organizations and services

This commit is contained in:
2026-07-22 16:57:16 -03:00
parent dd3e07311d
commit 3988e328eb
2 changed files with 145 additions and 2 deletions
@@ -15,6 +15,7 @@ import EventRepeatIcon from "@mui/icons-material/EventRepeat";
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import ColorLensIcon from '@mui/icons-material/ColorLens';
import PinDropIcon from '@mui/icons-material/PinDrop';
import ReviewsIcon from '@mui/icons-material/Reviews';
import API from "@services/Api.Service";
@@ -65,6 +66,15 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
keywords: ["telefono", "contacto", "email", "whatsapp", "redes", "celular"],
icon: AddCircleOutlineIcon
},
{
id: "public-visibility",
flowId: "public-visibility",
title: "Visibilidad Pública",
description: "Configura opiniones, puntuaciones y profesionales visibles para tus clientes.",
category: "Negocio",
keywords: ["opiniones", "puntuaciones", "calificaciones", "profesionales", "visibilidad", "reseñas", "publico"],
icon: ReviewsIcon
},
{
id: "new-service",
flowId: "onboarding-service&action=new-service",
@@ -100,6 +100,15 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
const [previewServices, setPreviewServices] = useState<any[]>([]);
// State for Public Visibility Flow
const [selectedOrganization, setSelectedOrganization] = useState<any | null>(null);
const [publicVisibilityServices, setPublicVisibilityServices] = useState<any[]>([]);
const [orgShowPublicScores, setOrgShowPublicScores] = useState(true);
const [orgShowPublicOpinions, setOrgShowPublicOpinions] = useState(true);
const [orgShowPublicProfessionals, setOrgShowPublicProfessionals] = useState(true);
const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true);
const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true);
// State for standalone update schedule flow
const [scheduleScope, setScheduleScope] = useState<"me" | "specific" | "all" | null>(null);
const [targetEmployeeId, setTargetEmployeeId] = useState<string | null>(null);
@@ -138,6 +147,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
const loadOrganizationContext = (org: any) => {
setCreatedCompanyId(org.id || org._id);
setSelectedOrganization(org);
setOrgName(org.name || "");
setCategoryId(org.categoryId || null);
setOrgDescription(org.description || "");
@@ -156,6 +166,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
setHeatMapFraction(org.heatMapFraction || 60);
setAppointmentTime(org.appointmentTime || 24);
setCancellationTime(org.cancellationTime || 12);
setOrgShowPublicScores(org.showPublicScores !== undefined ? org.showPublicScores : true);
setOrgShowPublicOpinions(org.showPublicOpinions !== undefined ? org.showPublicOpinions : true);
setOrgShowPublicProfessionals(org.showPublicProfessionals !== undefined ? org.showPublicProfessionals : true);
if (org.headerColor) setHeaderColor(org.headerColor);
if (org.headerFontColor) setHeaderFontColor(org.headerFontColor);
@@ -237,6 +250,15 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
}).catch(console.error);
} else if (flowId === "new-collaborator") {
setCurrentStepIndex(1);
} else if (flowId === "public-visibility") {
setCurrentStepIndex(0);
API.post<any[]>("services/get-by-company", { companyId: org.id || org._id })
.then(res => {
const services = res || [];
setPublicVisibilityServices(services);
setServicesShowPublicScores(services.every(service => service.showPublicScores !== false));
setServicesShowPublicOpinions(services.every(service => service.showPublicOpinions !== false));
}).catch(console.error);
} else if (flowId === "setup-collaborator") {
setCurrentStepIndex(0);
if (initialEmployeeId) {
@@ -264,7 +286,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
};
React.useEffect(() => {
if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule") {
if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId === "public-visibility" || 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 => {
@@ -285,7 +307,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
.finally(() => setIsResuming(false));
}
if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || (flowId === "onboarding-org" && action === "new-org")) {
if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || flowId === "public-visibility" || (flowId === "onboarding-org" && action === "new-org")) {
setCurrentStepIndex(0);
}
}, [flowId, SessionInfo.metrics.organizationsCount, SessionInfo.userId, router]);
@@ -3257,6 +3279,116 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
return null;
};
const isSelectedOrganizationPaidPlan = () => {
if (!createdCompanyId) return false;
return (SessionInfo.organizationSubscriptions?.[createdCompanyId]?.plan?.price || 0) > 0;
};
const handleSavePublicVisibility = async () => {
if (!createdCompanyId || isCreating) return;
setIsCreating(true);
try {
await API.post<any>("companies/update", {
id: createdCompanyId,
showPublicScores: orgShowPublicScores,
showPublicOpinions: orgShowPublicOpinions,
showPublicProfessionals: orgShowPublicProfessionals,
sessionUser: SessionInfo.userId
});
await Promise.all(publicVisibilityServices.map(service =>
API.post<any>("services/update", {
id: service.id || service._id,
companyId: createdCompanyId,
name: service.name,
description: service.description,
originalPrice: service.originalPrice,
limit: service.limit,
length: service.length,
color: service.color,
fontColor: service.fontColor,
fontShadowColor: service.fontShadowColor,
sessionUser: SessionInfo.userId,
showPublicScores: servicesShowPublicScores,
showPublicOpinions: servicesShowPublicOpinions
})
));
setIsSuccess(true);
} catch (error: any) {
console.error("Error actualizando visibilidad pública:", error);
alert(error?.message || "Hubo un error al guardar la visibilidad pública.");
} finally {
setIsCreating(false);
}
};
const renderVisibilityCheckbox = (label: string, description: string, checked: boolean, onChange: (checked: boolean) => void) => (
<label style={{
display: 'flex', gap: '12px', alignItems: 'flex-start', padding: '14px', borderRadius: '12px',
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', cursor: 'pointer'
}}>
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
style={{ width: '18px', height: '18px', marginTop: '2px', accentColor: 'var(--wine-red)', cursor: 'pointer' }}
/>
<span style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<span style={{ color: 'white', fontWeight: 700 }}>{label}</span>
<span style={{ color: 'rgba(255,255,255,0.65)', fontSize: '13px', lineHeight: 1.4 }}>{description}</span>
</span>
</label>
);
const renderPublicVisibilityFlow = () => {
if (!isSelectedOrganizationPaidPlan()) {
return (
<QuestionCard
isActive={true}
title="Esta configuración requiere un plan pago"
description="La visibilidad pública de opiniones, puntuaciones y profesionales está disponible para organizaciones con plan pago activo. El flujo seguirá visible para que puedas volver cuando actualices tu plan."
onNext={() => router.push(createdCompanyId ? `/landing/upgrade-plan?org=${createdCompanyId}` : "/landing/pricing")}
nextLabel="Ver planes"
>
<div style={{
color: 'rgba(255,255,255,0.75)', textAlign: 'center', padding: '18px', borderRadius: '16px',
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', lineHeight: 1.5
}}>
Estás configurando {selectedOrganization?.name ? <strong>{selectedOrganization.name}</strong> : "esta organización"}. Para cambiar qué opiniones, puntuaciones y profesionales se muestran públicamente, el dueño de esta organización necesita un plan pago.
</div>
</QuestionCard>
);
}
return (
<QuestionCard
isActive={true}
title="Configura la visibilidad pública"
description="Elige qué información verán tus clientes en la página pública de la organización y sus servicios. Si un valor nunca fue configurado, se considera visible por defecto."
onNext={handleSavePublicVisibility}
disableNext={isCreating}
nextLabel={isCreating ? "Guardando..." : "Guardar configuración"}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '18px', width: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<h3 style={{ color: 'white', margin: 0, fontSize: '16px' }}>Organización</h3>
{renderVisibilityCheckbox("Mostrar puntuación pública", "Permite que los clientes vean la puntuación promedio de la organización.", orgShowPublicScores, setOrgShowPublicScores)}
{renderVisibilityCheckbox("Mostrar opiniones públicas", "Permite que los clientes vean las opiniones recibidas por la organización.", orgShowPublicOpinions, setOrgShowPublicOpinions)}
{renderVisibilityCheckbox("Mostrar profesionales públicos", "Permite que los clientes vean la lista de profesionales de la organización.", orgShowPublicProfessionals, setOrgShowPublicProfessionals)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<h3 style={{ color: 'white', margin: 0, fontSize: '16px' }}>Servicios</h3>
{renderVisibilityCheckbox("Mostrar puntuación pública de servicios", "Aplica este valor a todos los servicios de la organización.", servicesShowPublicScores, setServicesShowPublicScores)}
{renderVisibilityCheckbox("Mostrar opiniones públicas de servicios", "Aplica este valor a todos los servicios de la organización.", servicesShowPublicOpinions, setServicesShowPublicOpinions)}
</div>
</div>
</QuestionCard>
);
};
const renderFlow = () => {
if (allOrganizations.length > 1 && !createdCompanyId && action !== "new-org") {
return (
@@ -3294,6 +3426,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 === "disable-schedule") return renderDisableScheduleFlow();
if (flowId === "override-schedule") return renderOverrideScheduleFlow();
if (flowId === "update-schedule") return renderUpdateScheduleFlow();