From a9afb13d15ff59fedf1115feb4e41848cc8b69e4 Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Mon, 20 Jul 2026 14:46:14 -0300 Subject: [PATCH] feat: update collaborator invitation flow to handle existing users and pending invitations in StepEngine --- server/src/Models/Employees/Employee.ts | 6 +- .../org/profile/[id]/clients/page.tsx | 8 +- .../components/StepEngine/StepEngine.tsx | 194 ++++++++++++++---- txclient/src/app/landing/dashboard/page.tsx | 28 +-- 4 files changed, 180 insertions(+), 56 deletions(-) diff --git a/server/src/Models/Employees/Employee.ts b/server/src/Models/Employees/Employee.ts index 18c1075..858052d 100644 --- a/server/src/Models/Employees/Employee.ts +++ b/server/src/Models/Employees/Employee.ts @@ -167,7 +167,11 @@ class EmployeeManager implements IEmployeesManager { } if (employeeCheck) { - throw new Error("El usuario ya pertenece a la compañia"); + if (!employeeCheck.guestOk) { + throw new Error("La invitación para este usuario ya está pendiente"); + } + + throw new Error("El usuario ya pertenece a la compañía"); } const canAdd = await MetricsList.canAddEmployee(companyCheck.ownerId); diff --git a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/clients/page.tsx b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/clients/page.tsx index ab9578a..63dc90a 100644 --- a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/clients/page.tsx +++ b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/clients/page.tsx @@ -205,13 +205,13 @@ export default function ClientsGrid() { Baneado )} -

+

{client.email || "Sin email"} -

-

+

+
@@ -219,7 +219,7 @@ export default function ClientsGrid() { ? `${client.phoneCountryCode || ''} ${client.phoneAreaCode} - ${client.phoneNumber}` : "Sin teléfono" } -

+
))} diff --git a/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx b/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx index dab082c..2916e3a 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx @@ -120,6 +120,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const [inviteRole, setInviteRole] = useState<"admin" | "colaborador">("colaborador"); const [isSearchingEmail, setIsSearchingEmail] = useState(false); const [isUpgradeRequired, setIsUpgradeRequired] = useState(false); + const [inviteFlowNotice, setInviteFlowNotice] = useState(""); // State for Setup Collaborator Flow const [collaboratorToSetup, setCollaboratorToSetup] = useState(null); @@ -128,6 +129,13 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const [setupSelectedServices, setSetupSelectedServices] = useState([]); const [setupAvailableServices, setSetupAvailableServices] = useState([]); + const getEmployeeUserId = (employee: any) => employee.userId?._id || employee.userId?.id || employee.userId; + + const canManageEmployeeSchedules = (employee: any) => { + const isCurrentUser = String(getEmployeeUserId(employee)) === String(SessionInfo.userId); + return !employee.removed && (isCurrentUser || employee.guestOk === true); + }; + const loadOrganizationContext = (org: any) => { setCreatedCompanyId(org.id || org._id); setOrgName(org.name || ""); @@ -219,11 +227,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm API.post("employees/get-by-company", { companyId: org.id || org._id }) .then(empRes => { if (empRes && empRes.length > 0) { - setAllEmployees(empRes); - const me = empRes.find((e: any) => { - const eUserId = e.userId?._id || e.userId?.id || e.userId; - return String(eUserId) === String(SessionInfo.userId); - }); + const availableEmployees = empRes.filter(canManageEmployeeSchedules); + setAllEmployees(availableEmployees); + const me = availableEmployees.find((e: any) => String(getEmployeeUserId(e)) === String(SessionInfo.userId)); if (me) { setEmployeeId(me.id || me._id || me.employeeId); } @@ -1256,7 +1262,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm for (let i = 0; i <= 6; i++) { const isWorkingDay = workingDays.includes(i); - let schedules = []; + let schedules: Array<{ from: string; to: string; disabled: boolean }> = []; if (isWorkingDay) { if (hasBreak) { schedules = [ @@ -1269,9 +1275,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm ]; } } else { - schedules = [ - { from: "00:00", to: "23:59", disabled: true } - ]; + schedules = []; } await API.post("schedules/update", { @@ -1368,14 +1372,14 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm onNext={() => { if (doesProvideServices === null) return; if (!doesProvideServices) { - // If they don't provide services, finish setup directly - window.location.href = "/landing/dashboard"; + const query = createdCompanyId ? `&companyId=${createdCompanyId}` : ""; + router.push(`/admin/assistant?flow=new-collaborator${query}`); } else { handleNext(); } }} disableNext={doesProvideServices === null} - nextLabel={doesProvideServices === false ? "Finalizar" : "Siguiente"} + nextLabel={doesProvideServices === false ? "Invitar colaborador" : "Siguiente"} >

No, tengo un equipo

-

Terminaremos aquí. Luego podrás invitar a tus colaboradores.

+

Te guiaremos para invitar al primer colaborador.

@@ -1724,7 +1728,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm for (let i = 0; i <= 6; i++) { const isWorkingDay = workingDays.includes(i); - let schedules = []; + let schedules: Array<{ from: string; to: string; disabled: boolean }> = []; if (isWorkingDay) { if (hasBreak) { schedules = [ @@ -1737,9 +1741,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm ]; } } else { - schedules = [ - { from: "00:00", to: "23:59", disabled: true } - ]; + schedules = []; } await API.post("schedules/update", { @@ -2016,7 +2018,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm return; } - let schedules = []; + let schedules: Array<{ from: string; to: string; disabled: boolean }> = []; if (hasBreak) { schedules = [ { from: openTime, to: breakStart, disabled: false }, @@ -2653,6 +2655,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const handleCreateCollaborator = async () => { if (!inviteCollaborator || !createdCompanyId) return; setIsCreating(true); + setInviteFlowNotice(""); try { await API.post("employees/create", { companyId: createdCompanyId, @@ -2661,13 +2664,21 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm hostOk: true, sessionUser: SessionInfo.userId }); - handleActionSuccess(); + handleNext(); } catch (error: any) { const errorMessage = error.desc || error.message || "Error al invitar al colaborador"; - if (errorMessage.toLowerCase().includes("limite") || errorMessage.toLowerCase().includes("límite")) { + const normalizedErrorMessage = errorMessage.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + if (normalizedErrorMessage.includes("limite")) { setIsUpgradeRequired(true); + } else if (normalizedErrorMessage.includes("invitacion") && normalizedErrorMessage.includes("pendiente")) { + setInviteFlowNotice("Ya se envió una invitación para que este usuario forme parte de la compañía y todavía no respondió."); + setCurrentStepIndex(5); + } else if (normalizedErrorMessage.includes("ya pertenece")) { + setInviteFlowNotice("Este usuario ya pertenece a la compañía."); + setCurrentStepIndex(6); } else { - alert(errorMessage); + setInviteFlowNotice(errorMessage); + setCurrentStepIndex(5); } } finally { setIsCreating(false); @@ -2752,6 +2763,109 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm {null} + +
+
{ + setInviteEmail(""); + setInviteCollaborator(null); + setInviteEmailError(""); + setInviteRole("colaborador"); + setIsUpgradeRequired(false); + setInviteFlowNotice(""); + setCurrentStepIndex(1); + }} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Invitar otro colaborador
+
Agrega otra persona a tu equipo.
+
+
{ + const query = createdCompanyId ? `&companyId=${createdCompanyId}` : ""; + router.push(`/admin/assistant?flow=update-schedule${query}`); + }} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Configurar mis horarios
+
Define tus horarios personales de atención.
+
+
router.push("/admin/assistant")} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Terminar
+
Volver a la home del asistente.
+
+
+
+ + +
+
{ + setInviteEmail(""); + setInviteCollaborator(null); + setInviteEmailError(""); + setInviteRole("colaborador"); + setIsUpgradeRequired(false); + setInviteFlowNotice(""); + setCurrentStepIndex(1); + }} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Invitar otro colaborador
+
Busca otra persona para sumar al equipo.
+
+
router.push("/admin/assistant")} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Volver al asistente
+
Regresar a la home del asistente.
+
+
+
+ + +
+
{ + setInviteEmail(""); + setInviteCollaborator(null); + setInviteEmailError(""); + setInviteRole("colaborador"); + setIsUpgradeRequired(false); + setInviteFlowNotice(""); + setCurrentStepIndex(1); + }} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Invitar otro colaborador
+
Busca otra persona para sumar al equipo.
+
+
router.push("/admin/assistant")} + style={{ padding: '15px', borderRadius: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', color: 'white', transition: 'all 0.2s' }} + > +
Volver al asistente
+
Regresar a la home del asistente.
+
+
+
+ ) => { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = '0 6px 20px rgba(255,255,255,0.3)'; + }; + + const handleSuccessButtonMouseOut = (e: React.MouseEvent) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 4px 15px rgba(0,0,0,0.3)'; + }; + return (
- diff --git a/txclient/src/app/landing/dashboard/page.tsx b/txclient/src/app/landing/dashboard/page.tsx index 3b04c95..afbfd12 100644 --- a/txclient/src/app/landing/dashboard/page.tsx +++ b/txclient/src/app/landing/dashboard/page.tsx @@ -341,7 +341,7 @@ function DashboardContent() { selectedCompanyPlanSupportsRepeats && (isCompanyOwner || selectedOrganization?.isAdmin) ); - const showFreePlanStarterContent = Boolean(displayedPlan && displayedPlan.price === 0 && organizations.length === 0); + const showNoOrganizationStarterContent = Boolean(displayedPlan && organizations.length === 0); const agendaItems = dashboardData?.personalAgenda ?? []; const filteredAgendaItems = agendaItems.filter(item => !showOnlyMine || item.collaboratorId === SessionInfo.userId); @@ -518,28 +518,28 @@ function DashboardContent() {
)} - {showFreePlanStarterContent && ( + {showNoOrganizationStarterContent && (
- Tu plan gratuito ya está activo -

Ahora podés empezar a ofrecer turnos

+ Tu plan {displayedPlan.name} ya está activo +

Creá tu primera organización para empezar a trabajar

- Todavía no pertenecés a ninguna organización. Podés crear tu propio negocio con el asistente, - o esperar una invitación para sumarte al equipo de una organización existente. + Todavía no pertenecés a ninguna organización. Podés seguir el flujo guiado con el asistente + o hacerlo manualmente desde Mis Organizaciones.

-
)} - {showFreePlanStarterContent && ( + {showNoOrganizationStarterContent && (
1 @@ -568,10 +568,10 @@ function DashboardContent() {
)} - {showFreePlanStarterContent && ( + {showNoOrganizationStarterContent && (
- Qué podés hacer con tu plan básico + Qué podés hacer con tu plan

Tu cuenta ya está lista para operar

Usá este primer paso para preparar la base de tu negocio. Cuando tengas tu organización creada, @@ -588,7 +588,7 @@ function DashboardContent() { )} {/* Left Column (Sidebar) */} - {!showFreePlanStarterContent &&

+ {!showNoOrganizationStarterContent &&
{/* Agenda del Día */} {dashboardData && dashboardData.role !== "NONE" && (
@@ -674,7 +674,7 @@ function DashboardContent() {
} {/* Right Column (Main Content) */} - {!showFreePlanStarterContent &&
+ {!showNoOrganizationStarterContent &&
{/* Rendimiento Financiero (Only for ADMIN) */} {dashboardData && dashboardData.role === "ADMIN" && (