feat: implement backend and frontend support for managing reservation period extensions and summaries
This commit is contained in:
@@ -8,7 +8,10 @@ import { ApiVoidResult } from "@models/Api.VoidResult.type";
|
||||
import ApiRequest from "@services/Api.Service";
|
||||
import {
|
||||
EnableScheduleParams,
|
||||
ExtendReservationPeriodsParams,
|
||||
FindSchedulesEnabledParams,
|
||||
ReservationPeriodsSummary,
|
||||
ReservationPeriodsSummaryParams,
|
||||
SchedulesEnabledView,
|
||||
} from "@models/SchedulesEnabled.type";
|
||||
import {
|
||||
@@ -56,6 +59,18 @@ export const disableSchedule = async (data: EnableScheduleParams): Promise<ApiVo
|
||||
return ApiRequest.post<ApiVoidResult>("schedules-enabled/disable", data);
|
||||
};
|
||||
|
||||
export const summarizeReservationPeriods = async (
|
||||
data: ReservationPeriodsSummaryParams
|
||||
): Promise<ReservationPeriodsSummary> => {
|
||||
return ApiRequest.post<ReservationPeriodsSummary>("schedules-enabled/reservation-periods/summary", data);
|
||||
};
|
||||
|
||||
export const extendReservationPeriods = async (
|
||||
data: ExtendReservationPeriodsParams
|
||||
): Promise<ReservationPeriodsSummary> => {
|
||||
return ApiRequest.post<ReservationPeriodsSummary>("schedules-enabled/reservation-periods/extend", data);
|
||||
};
|
||||
|
||||
export const uploadHeaderImage = async (data: FormData): Promise<ApiVoidResult> => {
|
||||
return ApiRequest.postFile<ApiVoidResult>("companies/upload-header", data);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ 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";
|
||||
import { ReservationPeriodsSummary } from "@models/SchedulesEnabled.type";
|
||||
|
||||
|
||||
type FlowDefinition = {
|
||||
@@ -123,6 +124,15 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
|
||||
keywords: ["horarios", "disponibilidad", "semana", "apertura", "cierre", "jornada"],
|
||||
icon: CalendarMonthIcon
|
||||
},
|
||||
{
|
||||
id: "open-reservation-periods",
|
||||
flowId: "open-reservation-periods",
|
||||
title: "Extender Reservas",
|
||||
description: "Abrir más semanas para que tus clientes puedan reservar turnos.",
|
||||
category: "Horarios",
|
||||
keywords: ["reservas", "periodo", "agenda", "extender", "habilitar", "semanas"],
|
||||
icon: EventBusyIcon
|
||||
},
|
||||
{
|
||||
id: "override-schedule",
|
||||
flowId: "override-schedule",
|
||||
@@ -165,6 +175,7 @@ export default function AssistantHome() {
|
||||
|
||||
const [incompleteOrgs, setIncompleteOrgs] = React.useState<IncompleteOrgData[]>([]);
|
||||
const [incompleteCollaborators, setIncompleteCollaborators] = React.useState<any[]>([]);
|
||||
const [reservationPeriodAlerts, setReservationPeriodAlerts] = React.useState<Array<{ org: any; summary: ReservationPeriodsSummary }>>([]);
|
||||
const [isLoadingFlow, setIsLoadingFlow] = React.useState(true);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
|
||||
@@ -174,9 +185,25 @@ export default function AssistantHome() {
|
||||
const response = await API.post<any[]>("companies/get-by-user", { sessionUser: SessionInfo.userId });
|
||||
const orgs = response || [];
|
||||
const newIncompleteOrgs: IncompleteOrgData[] = [];
|
||||
const reservationAlerts: Array<{ org: any; summary: ReservationPeriodsSummary }> = [];
|
||||
|
||||
if (orgs.length > 0) {
|
||||
for (const org of orgs) {
|
||||
if (org.isAdmin) {
|
||||
try {
|
||||
const summary = await API.post<ReservationPeriodsSummary>("schedules-enabled/reservation-periods/summary", {
|
||||
companyId: org.id || org._id,
|
||||
sessionUser: SessionInfo.userId
|
||||
});
|
||||
|
||||
if (summary && summary.status !== "open") {
|
||||
reservationAlerts.push({ org, summary });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error checking reservation periods", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (org.onboardingCompleted) continue;
|
||||
|
||||
const isNameMissing = !org.name || org.name.trim() === "";
|
||||
@@ -239,6 +266,7 @@ export default function AssistantHome() {
|
||||
}
|
||||
|
||||
setIncompleteOrgs(newIncompleteOrgs);
|
||||
setReservationPeriodAlerts(reservationAlerts);
|
||||
} else {
|
||||
setIncompleteOrgs([{
|
||||
org: null,
|
||||
@@ -246,6 +274,7 @@ export default function AssistantHome() {
|
||||
currentStepName: "Detalles del Negocio",
|
||||
flowToTrigger: "onboarding-org"
|
||||
}]);
|
||||
setReservationPeriodAlerts([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching organizations:", error);
|
||||
@@ -255,6 +284,7 @@ export default function AssistantHome() {
|
||||
currentStepName: "Detalles del Negocio",
|
||||
flowToTrigger: "onboarding-org"
|
||||
}]);
|
||||
setReservationPeriodAlerts([]);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -273,7 +303,7 @@ export default function AssistantHome() {
|
||||
}, [SessionInfo.metrics, SessionInfo.userId, router]);
|
||||
|
||||
const isSetupIncomplete = () => {
|
||||
return incompleteOrgs.length > 0 || incompleteCollaborators.length > 0;
|
||||
return incompleteOrgs.length > 0 || incompleteCollaborators.length > 0 || reservationPeriodAlerts.length > 0;
|
||||
};
|
||||
|
||||
const handleStartFlow = (flowId: string, companyId?: string) => {
|
||||
@@ -397,6 +427,46 @@ export default function AssistantHome() {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{reservationPeriodAlerts.map(({ org, summary }, index) => {
|
||||
const orgId = org.id || org._id;
|
||||
const reservationPeriodCopy = summary.status === "missing"
|
||||
? {
|
||||
title: "Falta abrir el período de reservas",
|
||||
description: "Uno o más colaboradores todavía no tienen un período abierto para recibir reservas. Abrilo para que tus clientes puedan sacar turnos.",
|
||||
}
|
||||
: summary.status === "expired"
|
||||
? {
|
||||
title: "El período de reservas está vencido",
|
||||
description: "Uno o más colaboradores tienen el período de reservas vencido. Extendelo para que tus clientes puedan volver a reservar turnos.",
|
||||
}
|
||||
: {
|
||||
title: "El período de reservas está por finalizar",
|
||||
description: "El período abierto para reservas de uno o más colaboradores está por llegar a su fin. Extendelo para que tus clientes puedan seguir reservando turnos.",
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={`reservation-period-${orgId || index}`} className={style.onboardingCard}>
|
||||
<div className={style.onboardingContent}>
|
||||
<div className={style.onboardingIcon}>
|
||||
<EventBusyIcon fontSize="large" style={{ color: "#fff" }} />
|
||||
</div>
|
||||
<div className={style.onboardingTexts} style={{ flex: 1 }}>
|
||||
<h3>{reservationPeriodCopy.title}</h3>
|
||||
<p>{reservationPeriodCopy.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<AssistantButton
|
||||
text={isLoadingFlow ? "Cargando..." : "Extender período"}
|
||||
onClick={() => {
|
||||
if (!isLoadingFlow) {
|
||||
handleStartFlow("open-reservation-periods", orgId);
|
||||
}
|
||||
}}
|
||||
style={{ width: "100%", marginTop: "20px", height: "48px" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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";
|
||||
import { ReservationPeriodDuration, ReservationPeriodsSummary } from "@models/SchedulesEnabled.type";
|
||||
|
||||
interface StepEngineProps {
|
||||
flowId: string;
|
||||
@@ -143,6 +144,18 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
const [isFullDayDisabled, setIsFullDayDisabled] = useState<boolean>(true);
|
||||
const [disableStartTime, setDisableStartTime] = useState<string>("");
|
||||
const [disableEndTime, setDisableEndTime] = useState<string>("");
|
||||
const [reservationPeriodScope, setReservationPeriodScope] = useState<"employee" | "all" | null>(null);
|
||||
const reservationPeriodOptions: Array<ReservationPeriodDuration & { label: string }> = [
|
||||
{ unit: "week", value: 1, label: "1 semana" },
|
||||
{ unit: "week", value: 2, label: "2 semanas" },
|
||||
{ unit: "week", value: 3, label: "3 semanas" },
|
||||
{ unit: "month", value: 1, label: "1 mes" },
|
||||
{ unit: "month", value: 2, label: "2 meses" },
|
||||
];
|
||||
const [reservationPeriodDuration, setReservationPeriodDuration] = useState<ReservationPeriodDuration | null>({ unit: "month", value: 1 });
|
||||
const [reservationPeriodSummary, setReservationPeriodSummary] = useState<ReservationPeriodsSummary | null>(null);
|
||||
const [reservationPeriodEmployeesLoading, setReservationPeriodEmployeesLoading] = useState(false);
|
||||
const [reservationPeriodEmployeesError, setReservationPeriodEmployeesError] = useState<string | null>(null);
|
||||
|
||||
// State for New Collaborator Flow
|
||||
const [inviteEmail, setInviteEmail] = useState("");
|
||||
@@ -265,18 +278,56 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
}).catch(console.error);
|
||||
}
|
||||
}).catch(console.error);
|
||||
} else if (flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule") {
|
||||
} else if (flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule" || flowId === "open-reservation-periods") {
|
||||
if (flowId === "open-reservation-periods") {
|
||||
setCurrentStepIndex(0);
|
||||
setReservationPeriodScope(null);
|
||||
setTargetEmployeeId(null);
|
||||
setReservationPeriodDuration({ unit: "month", value: 1 });
|
||||
setReservationPeriodSummary(null);
|
||||
setAllEmployees([]);
|
||||
setReservationPeriodEmployeesError(null);
|
||||
setReservationPeriodEmployeesLoading(true);
|
||||
}
|
||||
|
||||
API.post<any[]>("employees/get-by-company", { companyId: org.id || org._id })
|
||||
.then(empRes => {
|
||||
if (empRes && empRes.length > 0) {
|
||||
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);
|
||||
const availableEmployees = flowId === "open-reservation-periods"
|
||||
? (empRes || []).filter((employee: any) => !employee.removed)
|
||||
: (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);
|
||||
}
|
||||
if (flowId === "open-reservation-periods") {
|
||||
if (availableEmployees.length === 0) {
|
||||
setReservationPeriodEmployeesError("No encontramos colaboradores activos para extender el período de reservas.");
|
||||
return;
|
||||
}
|
||||
if (availableEmployees.length === 1) {
|
||||
setReservationPeriodScope("employee");
|
||||
setTargetEmployeeId(availableEmployees[0].id || availableEmployees[0]._id || availableEmployees[0].employeeId);
|
||||
}
|
||||
}
|
||||
}).catch(console.error);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (flowId === "open-reservation-periods") {
|
||||
setReservationPeriodEmployeesError("No pudimos cargar los colaboradores. Reintentá antes de extender el período.");
|
||||
}
|
||||
}).finally(() => {
|
||||
if (flowId === "open-reservation-periods") {
|
||||
setReservationPeriodEmployeesLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (flowId === "open-reservation-periods") {
|
||||
API.post<ReservationPeriodsSummary>("schedules-enabled/reservation-periods/summary", {
|
||||
companyId: org.id || org._id,
|
||||
sessionUser: SessionInfo.userId
|
||||
}).then(setReservationPeriodSummary).catch(console.error);
|
||||
}
|
||||
} else if (flowId === "new-collaborator") {
|
||||
setCurrentStepIndex(1);
|
||||
} else if (flowId === "public-visibility") {
|
||||
@@ -342,7 +393,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
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") {
|
||||
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" || flowId === "open-reservation-periods") {
|
||||
setIsResuming(true);
|
||||
API.post<any[]>("companies/get-by-user", { sessionUser: SessionInfo.userId })
|
||||
.then(response => {
|
||||
@@ -363,7 +414,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 === "public-link" || 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 === "open-reservation-periods" || (flowId === "onboarding-org" && action === "new-org")) {
|
||||
setCurrentStepIndex(0);
|
||||
}
|
||||
}, [flowId, SessionInfo.metrics.organizationsCount, SessionInfo.userId, router]);
|
||||
@@ -4556,6 +4607,253 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
);
|
||||
};
|
||||
|
||||
const handleExtendReservationPeriods = async () => {
|
||||
if (!createdCompanyId || !reservationPeriodScope || !reservationPeriodDuration || isCreating) return;
|
||||
if (reservationPeriodEmployeesLoading || reservationPeriodEmployeesError || allEmployees.length === 0) return;
|
||||
if (reservationPeriodScope === "employee" && !targetEmployeeId) return;
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await API.post<ReservationPeriodsSummary>("schedules-enabled/reservation-periods/extend", {
|
||||
companyId: createdCompanyId,
|
||||
scope: reservationPeriodScope,
|
||||
employeeId: reservationPeriodScope === "employee" ? targetEmployeeId : undefined,
|
||||
duration: reservationPeriodDuration,
|
||||
sessionUser: SessionInfo.userId
|
||||
});
|
||||
setIsSuccess(true);
|
||||
} catch (error: any) {
|
||||
console.error("Error extendiendo período de reservas:", error);
|
||||
alert(error?.message || "Hubo un error al extender el período de reservas.");
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderReservationPeriodFlow = () => {
|
||||
if (reservationPeriodEmployeesLoading) {
|
||||
return (
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
title="Cargando colaboradores"
|
||||
description="Estamos buscando los colaboradores disponibles para extender el período de reservas."
|
||||
disableNext
|
||||
nextLabel="Cargando..."
|
||||
>
|
||||
<div />
|
||||
</QuestionCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (reservationPeriodEmployeesError || allEmployees.length === 0) {
|
||||
return (
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
title="No pudimos preparar la extensión"
|
||||
description={reservationPeriodEmployeesError || "No encontramos colaboradores activos para extender el período de reservas."}
|
||||
disableNext
|
||||
nextLabel="Extensión bloqueada"
|
||||
>
|
||||
<div />
|
||||
</QuestionCard>
|
||||
);
|
||||
}
|
||||
|
||||
const needsEmployeeSelection = allEmployees.length > 1 && reservationPeriodScope === "employee";
|
||||
const selectedEmployee = allEmployees.find((employee: any) => String(employee.id || employee._id || employee.employeeId) === String(targetEmployeeId));
|
||||
const reservationPeriodEmployees = reservationPeriodSummary?.employees || [];
|
||||
const getReservationPeriodEmployeeSummary = (employee: any) => {
|
||||
const employeeKey = employee.id || employee._id || employee.employeeId;
|
||||
return reservationPeriodEmployees.find(summary => String(summary.employeeId) === String(employeeKey));
|
||||
};
|
||||
const getReservationPeriodStatus = (employee: any) => {
|
||||
const summary = getReservationPeriodEmployeeSummary(employee);
|
||||
const daysRemaining = summary?.daysRemaining;
|
||||
const safeDaysRemaining = typeof daysRemaining === "number" ? Math.max(daysRemaining, 0) : 0;
|
||||
const progress = Math.min(Math.round((safeDaysRemaining / 30) * 100), 100);
|
||||
const color = !summary?.isOpen || safeDaysRemaining === 0
|
||||
? "#ef4444"
|
||||
: safeDaysRemaining <= 7
|
||||
? "#f59e0b"
|
||||
: "#22c55e";
|
||||
|
||||
let label = "Sin período abierto";
|
||||
if (summary?.isOpen && typeof daysRemaining === "number") {
|
||||
if (daysRemaining < 0) {
|
||||
label = "Vencido";
|
||||
} else if (daysRemaining === 1) {
|
||||
label = "1 día restante";
|
||||
} else {
|
||||
label = `${daysRemaining} días restantes`;
|
||||
}
|
||||
} else if (typeof daysRemaining === "number" && daysRemaining < 0) {
|
||||
label = "Vencido";
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
color,
|
||||
progress,
|
||||
openUntilLabel: summary?.currentOpenUntil ? `Abierto hasta ${dayjs(summary.currentOpenUntil).format("D [de] MMMM")}` : null
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{allEmployees.length > 1 && (
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
title="Elegí a quién extenderle el período"
|
||||
description="Podés abrir reservas para todos los colaboradores o para uno específico."
|
||||
onNext={handleNext}
|
||||
disableNext={!reservationPeriodScope || (needsEmployeeSelection && !targetEmployeeId)}
|
||||
nextLabel="Siguiente"
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setReservationPeriodScope("all");
|
||||
setTargetEmployeeId(null);
|
||||
}}
|
||||
style={{
|
||||
padding: "18px", borderRadius: "12px", cursor: "pointer",
|
||||
background: reservationPeriodScope === "all" ? "var(--wine-red)" : "rgba(255,255,255,0.05)",
|
||||
border: `2px solid ${reservationPeriodScope === "all" ? "var(--wine-red)" : "rgba(255,255,255,0.1)"}`,
|
||||
color: "white", textAlign: "center", transition: "all 0.2s ease"
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: "0 0 5px 0", fontSize: "16px", fontWeight: reservationPeriodScope === "all" ? "bold" : "normal" }}>Todos los colaboradores</h4>
|
||||
<p style={{ margin: 0, fontSize: "13px", color: "rgba(255,255,255,0.75)" }}>Extiende el período de reservas para todo el equipo.</p>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setReservationPeriodScope("employee")}
|
||||
style={{
|
||||
padding: "18px", borderRadius: "12px", cursor: "pointer",
|
||||
background: reservationPeriodScope === "employee" ? "var(--wine-red)" : "rgba(255,255,255,0.05)",
|
||||
border: `2px solid ${reservationPeriodScope === "employee" ? "var(--wine-red)" : "rgba(255,255,255,0.1)"}`,
|
||||
color: "white", textAlign: "center", transition: "all 0.2s ease"
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: "0 0 5px 0", fontSize: "16px", fontWeight: reservationPeriodScope === "employee" ? "bold" : "normal" }}>Un colaborador específico</h4>
|
||||
<p style={{ margin: 0, fontSize: "13px", color: "rgba(255,255,255,0.75)" }}>Elige una persona del equipo.</p>
|
||||
</div>
|
||||
{needsEmployeeSelection && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginTop: "8px" }}>
|
||||
{allEmployees.map((employee: any) => {
|
||||
const employeeKey = employee.id || employee._id || employee.employeeId;
|
||||
const isSelected = String(targetEmployeeId) === String(employeeKey);
|
||||
const periodStatus = getReservationPeriodStatus(employee);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={employeeKey}
|
||||
onClick={() => setTargetEmployeeId(employeeKey)}
|
||||
style={{
|
||||
padding: "12px 14px", borderRadius: "10px", cursor: "pointer",
|
||||
background: isSelected ? "rgba(255,42,127,0.18)" : "rgba(255,255,255,0.04)",
|
||||
border: `1px solid ${isSelected ? "var(--wine-red)" : "rgba(255,255,255,0.1)"}`,
|
||||
color: "white"
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px", alignItems: "flex-start" }}>
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
{employee.fullName || `${employee.firstName || ""} ${employee.lastName || ""}`.trim() || employee.email || "Colaborador"}
|
||||
</span>
|
||||
<span style={{ color: periodStatus.color, fontSize: "12px", fontWeight: 700, whiteSpace: "nowrap" }}>
|
||||
{periodStatus.label}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ height: "7px", borderRadius: "999px", background: "rgba(255,255,255,0.12)", overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
width: `${periodStatus.progress}%`,
|
||||
minWidth: periodStatus.progress > 0 ? "8px" : 0,
|
||||
height: "100%",
|
||||
borderRadius: "999px",
|
||||
background: periodStatus.color,
|
||||
transition: "width 0.2s ease"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{periodStatus.openUntilLabel && (
|
||||
<span style={{ color: "rgba(255,255,255,0.62)", fontSize: "12px" }}>
|
||||
{periodStatus.openUntilLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
)}
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === (allEmployees.length > 1 ? 1 : 0)}
|
||||
title="Elegí cuánto extender"
|
||||
description="Abriremos un nuevo bloque continuo desde el último día actualmente disponible."
|
||||
onNext={handleNext}
|
||||
disableNext={!reservationPeriodDuration}
|
||||
nextLabel="Siguiente"
|
||||
>
|
||||
<div style={{ display: "flex", gap: "10px", flexWrap: "wrap", width: "100%", justifyContent: "center" }}>
|
||||
{reservationPeriodOptions.map(option => {
|
||||
const isSelected = reservationPeriodDuration?.unit === option.unit && reservationPeriodDuration.value === option.value;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${option.unit}-${option.value}`}
|
||||
onClick={() => setReservationPeriodDuration({ unit: option.unit, value: option.value })}
|
||||
style={{
|
||||
padding: "12px 24px", borderRadius: "12px", cursor: "pointer",
|
||||
background: isSelected ? "var(--wine-red)" : "rgba(255,255,255,0.05)",
|
||||
border: `2px solid ${isSelected ? "var(--wine-red)" : "rgba(255,255,255,0.1)"}`,
|
||||
color: "white", fontWeight: isSelected ? "bold" : "normal",
|
||||
transition: "all 0.2s ease", fontSize: "16px"
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === (allEmployees.length > 1 ? 2 : 1)}
|
||||
title="Confirmar extensión"
|
||||
description="Revisá la selección antes de abrir el nuevo período de reservas."
|
||||
onNext={handleExtendReservationPeriods}
|
||||
disableNext={isCreating || reservationPeriodEmployeesLoading || Boolean(reservationPeriodEmployeesError) || allEmployees.length === 0 || !reservationPeriodScope || !reservationPeriodDuration || (reservationPeriodScope === "employee" && !targetEmployeeId)}
|
||||
nextLabel={isCreating ? "Extendiendo..." : "Extender período"}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%", maxWidth: "500px", margin: "0 auto", background: "rgba(255,255,255,0.05)", borderRadius: "12px", padding: "18px", border: "1px solid rgba(255,255,255,0.1)", color: "white" }}>
|
||||
<div>
|
||||
<div style={{ color: "rgba(255,255,255,0.65)", fontSize: "13px" }}>Organización</div>
|
||||
<div style={{ fontWeight: 700 }}>{selectedOrganization?.name || orgName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: "rgba(255,255,255,0.65)", fontSize: "13px" }}>Aplicar a</div>
|
||||
<div style={{ fontWeight: 700 }}>{reservationPeriodScope === "all" ? "Todos los colaboradores" : (selectedEmployee?.fullName || selectedEmployee?.email || "Colaborador seleccionado")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: "rgba(255,255,255,0.65)", fontSize: "13px" }}>Duración</div>
|
||||
<div style={{ fontWeight: 700 }}>{reservationPeriodOptions.find(option => option.unit === reservationPeriodDuration?.unit && option.value === reservationPeriodDuration.value)?.label}</div>
|
||||
</div>
|
||||
{reservationPeriodSummary?.worstDaysRemaining !== null && reservationPeriodSummary?.worstDaysRemaining !== undefined && (
|
||||
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: "13px" }}>
|
||||
Menor disponibilidad actual: {reservationPeriodSummary.worstDaysRemaining} días restantes.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFlow = () => {
|
||||
if (allOrganizations.length > 1 && !createdCompanyId && action !== "new-org") {
|
||||
return (
|
||||
@@ -4598,6 +4896,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
if (flowId === "notifications") return renderNotificationsFlow();
|
||||
if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow();
|
||||
if (flowId === "disable-schedule") return renderDisableScheduleFlow();
|
||||
if (flowId === "open-reservation-periods") return renderReservationPeriodFlow();
|
||||
if (flowId === "override-schedule") return renderOverrideScheduleFlow();
|
||||
if (flowId === "update-schedule") return renderUpdateScheduleFlow();
|
||||
if (isAddingAnotherService || flowId === "onboarding-service") return renderOnboardingServiceFlow();
|
||||
@@ -4617,6 +4916,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
if (flowId === "setup-collaborator") return "El colaborador ha sido configurado exitosamente y ya puede comenzar a recibir reservas.";
|
||||
if (flowId === "notifications") return "La configuración de notificaciones se ha guardado exitosamente.";
|
||||
if (flowId === "whatsapp-bot") return "Tu bot de WhatsApp está configurado y funcionando. Tus clientes recibirán notificaciones automáticas por WhatsApp.";
|
||||
if (flowId === "open-reservation-periods") return "El período de reservas fue extendido exitosamente.";
|
||||
if (action) return "Tus cambios se han guardado exitosamente.";
|
||||
return "Has configurado exitosamente tu negocio. Todo está listo para que empieces a recibir reservas y gestionar tu agenda como un profesional.";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user