feat: update collaborator invitation flow to handle existing users and pending invitations in StepEngine
This commit is contained in:
@@ -205,13 +205,13 @@ export default function ClientsGrid() {
|
||||
<span className={pageStyle.bannedBadge}>Baneado</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={pageStyle.clientDetail}>
|
||||
<div className={pageStyle.clientDetail}>
|
||||
<div className={pageStyle.iconWrapper}>
|
||||
<EmailOutlinedIcon sx={{ fontSize: 14 }} />
|
||||
</div>
|
||||
{client.email || "Sin email"}
|
||||
</p>
|
||||
<p className={pageStyle.clientPhone}>
|
||||
</div>
|
||||
<div className={pageStyle.clientPhone}>
|
||||
<div className={pageStyle.iconWrapper}>
|
||||
<PhoneOutlinedIcon sx={{ fontSize: 14 }} />
|
||||
</div>
|
||||
@@ -219,7 +219,7 @@ export default function ClientsGrid() {
|
||||
? `${client.phoneCountryCode || ''} ${client.phoneAreaCode} - ${client.phoneNumber}`
|
||||
: "Sin teléfono"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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<any | null>(null);
|
||||
@@ -128,6 +129,13 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
const [setupSelectedServices, setSetupSelectedServices] = useState<string[]>([]);
|
||||
const [setupAvailableServices, setSetupAvailableServices] = useState<any[]>([]);
|
||||
|
||||
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<any[]>("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<any>("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"}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '15px', flexWrap: 'wrap', width: '100%', justifyContent: 'center' }}>
|
||||
<div
|
||||
@@ -1412,7 +1416,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 5px 0', fontSize: '18px', fontWeight: doesProvideServices === false ? 'bold' : 'normal' }}>No, tengo un equipo</h4>
|
||||
<p style={{ margin: 0, fontSize: '13px', color: 'rgba(255,255,255,0.7)' }}>Terminaremos aquí. Luego podrás invitar a tus colaboradores.</p>
|
||||
<p style={{ margin: 0, fontSize: '13px', color: 'rgba(255,255,255,0.7)' }}>Te guiaremos para invitar al primer colaborador.</p>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
@@ -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<any>("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<any>("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}
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 4}
|
||||
title="Invitación Enviada"
|
||||
description="¿Cómo quieres continuar?"
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', width: '100%' }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Invitar otro colaborador</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Agrega otra persona a tu equipo.</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Configurar mis horarios</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Define tus horarios personales de atención.</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Terminar</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Volver a la home del asistente.</div>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 5}
|
||||
title="Invitación Pendiente"
|
||||
description={inviteFlowNotice || "No pudimos enviar la invitación en este momento."}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', width: '100%' }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Invitar otro colaborador</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Busca otra persona para sumar al equipo.</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Volver al asistente</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Regresar a la home del asistente.</div>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 6}
|
||||
title="El usuario ya pertenece"
|
||||
description={inviteFlowNotice || "Este usuario ya pertenece a la compañía."}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', width: '100%' }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Invitar otro colaborador</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Busca otra persona para sumar al equipo.</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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' }}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>Volver al asistente</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.8, marginTop: '5px' }}>Regresar a la home del asistente.</div>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 3 && isUpgradeRequired}
|
||||
title="¡Haz crecer tu equipo!"
|
||||
@@ -3202,6 +3316,24 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
return "Has configurado exitosamente tu negocio. Todo está listo para que empieces a recibir reservas y gestionar tu agenda como un profesional.";
|
||||
};
|
||||
|
||||
const successButtonStyle: React.CSSProperties = {
|
||||
padding: '16px 40px', fontSize: '18px', fontWeight: 'bold',
|
||||
background: 'white', color: 'var(--wine-red)', border: 'none',
|
||||
borderRadius: '30px', cursor: 'pointer',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.3)',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s'
|
||||
};
|
||||
|
||||
const handleSuccessButtonMouseOver = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 20px rgba(255,255,255,0.3)';
|
||||
};
|
||||
|
||||
const handleSuccessButtonMouseOut = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 15px rgba(0,0,0,0.3)';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh',
|
||||
@@ -3232,7 +3364,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
{getSuccessMessage()}
|
||||
</p>
|
||||
|
||||
<button
|
||||
<button
|
||||
onClick={() => {
|
||||
if (action) {
|
||||
router.push("/admin/assistant");
|
||||
@@ -3240,21 +3372,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
window.location.href = "/landing/dashboard";
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '16px 40px', fontSize: '18px', fontWeight: 'bold',
|
||||
background: 'white', color: 'var(--wine-red)', border: 'none',
|
||||
borderRadius: '30px', cursor: 'pointer',
|
||||
boxShadow: '0 4px 15px rgba(0,0,0,0.3)',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s'
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 20px rgba(255,255,255,0.3)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 15px rgba(0,0,0,0.3)';
|
||||
}}
|
||||
style={successButtonStyle}
|
||||
onMouseOver={handleSuccessButtonMouseOver}
|
||||
onMouseOut={handleSuccessButtonMouseOut}
|
||||
>
|
||||
{action ? "Volver al Menú Principal" : "Ir a mi Panel de Control"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user