From 33743f12b97994f549da9a1521dc19b1351ad1af Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Sat, 22 Aug 2026 12:34:09 -0300 Subject: [PATCH] feat: add CollaboratorPicker component and integrate into CollaboratorScheduleSummaryFlow - Implemented CollaboratorPicker for selecting employees with support for single and multiple selections. - Created CollaboratorScheduleSummaryFlow to display collaborator schedules and exceptions. - Added ServicePublicationFlow for managing service visibility on public pages. - Introduced CSS styles for ServicePrivate component to enhance UI. --- .../AdminAssistant.Interface.ts | 45 ++ .../Models/AdminAssistant/AdminAssistant.ts | 98 +++ .../Models/Schedules/Schedules.Interface.ts | 29 + server/src/Models/Schedules/Schedules.ts | 169 +++++ .../SchedulesDisabled.Adapter.Mongoose.ts | 6 + .../SchedulesDisabled.Interface.ts | 2 + .../SchedulesDisabled/SchedulesDisabled.ts | 11 + .../AdminAssistant.Controller.ts | 29 + .../AdminAssistant/AdminAssistant.Service.ts | 16 + .../src/api/Schedules/Schedules.Controller.ts | 24 + server/src/api/Schedules/Schedules.Service.ts | 8 + .../6a899a56edf89c333b2e626f/image.png | Bin 0 -> 23358 bytes .../AssistantHome/AssistantHome.tsx | 30 + .../QuestionCard/QuestionCard.module.css | 13 + .../components/QuestionCard/QuestionCard.tsx | 2 +- .../components/StepEngine/StepEngine.tsx | 576 ++++++++++++++++-- .../components/CollaboratorPicker.tsx | 62 ++ .../StepEngine/components/SuccessScreen.tsx | 2 + .../flows/CollaboratorScheduleSummaryFlow.tsx | 234 +++++++ .../flows/OnboardingServiceFlow.tsx | 254 +++++++- .../flows/ServicePublicationFlow.tsx | 99 +++ .../StepEngine/flows/UpdateScheduleFlow.tsx | 455 +++++++------- .../StepEngine/helpers/schedules.ts | 20 +- .../ServicePrivate/ServicePrivate.module.css | 217 +++++++ .../ServicePrivate/ServicePrivate.tsx | 31 +- 25 files changed, 2145 insertions(+), 287 deletions(-) create mode 100644 server/src/Models/AdminAssistant/AdminAssistant.Interface.ts create mode 100644 server/src/Models/AdminAssistant/AdminAssistant.ts create mode 100644 server/src/api/AdminAssistant/AdminAssistant.Controller.ts create mode 100644 server/src/api/AdminAssistant/AdminAssistant.Service.ts create mode 100644 server/uploads/services/6a899a56edf89c333b2e626f/image.png create mode 100644 txclient/src/app/admin/assistant/components/StepEngine/components/CollaboratorPicker.tsx create mode 100644 txclient/src/app/admin/assistant/components/StepEngine/flows/CollaboratorScheduleSummaryFlow.tsx create mode 100644 txclient/src/app/admin/assistant/components/StepEngine/flows/ServicePublicationFlow.tsx create mode 100644 txclient/src/app/components/ServicePrivate/ServicePrivate.module.css diff --git a/server/src/Models/AdminAssistant/AdminAssistant.Interface.ts b/server/src/Models/AdminAssistant/AdminAssistant.Interface.ts new file mode 100644 index 0000000..558412b --- /dev/null +++ b/server/src/Models/AdminAssistant/AdminAssistant.Interface.ts @@ -0,0 +1,45 @@ +import { ScheduleItem } from "../Schedules/Schedules.Interface"; +import { ISchedulesDisabled } from "../SchedulesDisabled/SchedulesDisabled.Interface"; +import { ISchedulesOverrides } from "../SchedulesOverrides/SchedulesOverrides.Interface"; + +export type CollaboratorScheduleDetailsParams = { + companyId: string; + employeeId: string; + sessionUser: string; + fromDate?: Date; +}; + +export type CollaboratorScheduleDetailsCollaborator = { + employeeId: string; + fullName: string; + email: string; + avatar: string; + userId: string; +}; + +export type CollaboratorWeeklySchedule = { + companyId: string; + employeeId: string; + weekDay: number; + scheduleId: string; + schedules: ScheduleItem[]; +}; + +export type CollaboratorScheduleServiceSummary = { + id: string; + name: string; +}; + +export type CollaboratorScheduleDetailsView = { + collaborator: CollaboratorScheduleDetailsCollaborator; + servicesById: Record; + weeklySchedules: CollaboratorWeeklySchedule[]; + futureDisabledSchedules: ISchedulesDisabled[]; + futureOverrides: ISchedulesOverrides[]; +}; + +export interface IAdminAssistantManager { + getCollaboratorScheduleDetails( + data: CollaboratorScheduleDetailsParams + ): Promise; +} diff --git a/server/src/Models/AdminAssistant/AdminAssistant.ts b/server/src/Models/AdminAssistant/AdminAssistant.ts new file mode 100644 index 0000000..cd6018c --- /dev/null +++ b/server/src/Models/AdminAssistant/AdminAssistant.ts @@ -0,0 +1,98 @@ +import dayjs from "dayjs"; +import EmployeesList from "../Employees/Employee"; +import ServicesList from "../Services/Service"; +import SchedulesList from "../Schedules/Schedules"; +import SchedulesDisabledList from "../SchedulesDisabled/SchedulesDisabled"; +import SchedulesOverridesList from "../SchedulesOverrides/SchedulesOverrides"; +import UsersManager from "../Users/Users"; +import getAvatar from "../../helpers/getAvatar"; +import { joinStrings } from "../../helpers/String"; +import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check"; +import { + CollaboratorScheduleDetailsParams, + CollaboratorScheduleDetailsView, + IAdminAssistantManager, +} from "./AdminAssistant.Interface"; + +class AdminAssistantManager implements IAdminAssistantManager { + public async getCollaboratorScheduleDetails( + data: CollaboratorScheduleDetailsParams + ): Promise { + await validateSessionUser({ sessionUser: data.sessionUser }); + await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser }); + + const employee = await EmployeesList.employees.findOne({ + _id: data.employeeId, + companyId: data.companyId, + }); + + if (!employee) { + throw new Error("El colaborador no existe o no pertenece a la organización."); + } + + const user = await UsersManager.users.findOne({ _id: String(employee.userId) }); + const snapshot = employee.profileSnapshot || {}; + const fullName = user + ? joinStrings([user.firstName, user.lastName], " ") + : joinStrings([snapshot.firstName, snapshot.lastName], " "); + const email = user?.email || snapshot.email || ""; + const avatar = user + ? getAvatar(String(user.id), user.avatar, fullName) + : getAvatar(String(employee.userId), snapshot.avatar || "", fullName); + const fromDate = data.fromDate ? dayjs(data.fromDate).startOf("day").toDate() : dayjs().startOf("day").toDate(); + + const weeklySchedules = await SchedulesList.find({ + companyId: data.companyId, + employeeId: data.employeeId, + }); + const futureDisabledSchedules = await SchedulesDisabledList.find({ + companyId: data.companyId, + employeeId: data.employeeId, + endDate: { $gte: fromDate } as any, + sessionUser: data.sessionUser, + }); + const futureOverrides = await SchedulesOverridesList.find({ + companyId: data.companyId, + employeeId: data.employeeId, + date: { $gte: fromDate } as any, + }); + const companyServices = await ServicesList.findByCompanyId({ companyId: data.companyId }); + const servicesById = companyServices.reduce((index, service) => { + index[service.id] = { + id: service.id, + name: service.name, + }; + return index; + }, {} as CollaboratorScheduleDetailsView["servicesById"]); + + return { + collaborator: { + employeeId: String(employee.id || employee._id), + fullName: fullName || email || "Colaborador", + email, + avatar, + userId: String(employee.userId), + }, + servicesById, + weeklySchedules: weeklySchedules + .sort((a, b) => a.weekDay - b.weekDay) + .map((schedule) => ({ + companyId: String(schedule.companyId), + employeeId: String(schedule.employeeId), + scheduleId: String(schedule.id || (schedule as any)._id || ""), + weekDay: schedule.weekDay, + schedules: schedule.schedules || [], + })), + futureDisabledSchedules: futureDisabledSchedules.sort((a, b) => + new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ), + futureOverrides: futureOverrides.sort((a, b) => + new Date(a.date).getTime() - new Date(b.date).getTime() + ), + }; + } +} + +const AdminAssistantList = new AdminAssistantManager(); + +export default AdminAssistantList; diff --git a/server/src/Models/Schedules/Schedules.Interface.ts b/server/src/Models/Schedules/Schedules.Interface.ts index 2ee5243..f59e80c 100644 --- a/server/src/Models/Schedules/Schedules.Interface.ts +++ b/server/src/Models/Schedules/Schedules.Interface.ts @@ -8,6 +8,34 @@ export type ScheduleItem = { serviceIds?: string[]; }; +export type ScheduleConflictStrategy = "reject" | "overwrite-conflicts"; + +export type ScheduleConflictDetail = { + employeeId: string; + employeeName: string; + weekDay: number; + weekDayLabel: string; + attempted: ScheduleItem; + existing: ScheduleItem; + message: string; +}; + +export type ScheduleApplyParams = { + companyId: string; + employeeIds: string[]; + weekDays: number[]; + schedules: ScheduleItem[]; + sessionUser: string; + conflictStrategy: ScheduleConflictStrategy; +}; + +export type ScheduleApplyResult = { + valid: boolean; + applied: boolean; + conflicts: ScheduleConflictDetail[]; + message: string; +}; + export type FindSchedulesParams = { _id?: string; companyId?: string; @@ -100,6 +128,7 @@ export interface ISchedulesManager { findAllSchedulesByCollaborator(data: FindSchedulesParams): Promise; delete(data: DeleteScheduleParams): Promise; isAvailable(data: AvailableSchedulesParams): Promise; + applyScheduleChanges(data: ScheduleApplyParams): Promise; deleteSchedulesByCompany(data: DeleteScheulesByCompanyParams): Promise; deleteSchedulesByEmployee(data: DeleteSchedulesByEmployeeParams): Promise; } diff --git a/server/src/Models/Schedules/Schedules.ts b/server/src/Models/Schedules/Schedules.ts index e0469b2..827cd13 100644 --- a/server/src/Models/Schedules/Schedules.ts +++ b/server/src/Models/Schedules/Schedules.ts @@ -13,6 +13,9 @@ import { DeleteScheulesByCompanyParams, DeleteSchedulesByEmployeeParams, ScheduleItem, + ScheduleApplyParams, + ScheduleApplyResult, + ScheduleConflictDetail, } from "./Schedules.Interface"; import { EmployeeRoles } from "../Employees/Employees.Interface"; import UsersManager from "../Users/Users"; @@ -24,6 +27,9 @@ import { isNull } from "../../helpers/IsNull"; import getAvatar from "../../helpers/getAvatar"; import { NotificationsManager } from "../Notifications/Notifications"; import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface"; +import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check"; + +const WEEK_DAY_LABELS = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; class SchedulesManager implements ISchedulesManager { schedules: SchedulesAdapterMongoose; @@ -173,6 +179,169 @@ class SchedulesManager implements ISchedulesManager { return await this.schedules.find(filters); } + private parseTimeToMinutes(time: string): number { + const [hours, minutes] = time.split(":").map((part) => Number(part)); + + if (!Number.isInteger(hours) || !Number.isInteger(minutes)) { + throw new Error("El horario indicado no es válido"); + } + + return hours * 60 + minutes; + } + + private hasTimeOverlap(first: ScheduleItem, second: ScheduleItem): boolean { + return this.parseTimeToMinutes(first.from) < this.parseTimeToMinutes(second.to) + && this.parseTimeToMinutes(second.from) < this.parseTimeToMinutes(first.to); + } + + private getServiceScope(schedule: ScheduleItem): "all" | "specific" { + return schedule.serviceScope || "all"; + } + + private hasServiceOverlap(first: ScheduleItem, second: ScheduleItem): boolean { + const firstScope = this.getServiceScope(first); + const secondScope = this.getServiceScope(second); + + if (firstScope === "all" || secondScope === "all") { + return true; + } + + const secondServices = new Set(second.serviceIds || []); + return (first.serviceIds || []).some((serviceId) => secondServices.has(serviceId)); + } + + private hasScheduleConflict(first: ScheduleItem, second: ScheduleItem): boolean { + if (first.disabled || second.disabled) { + return false; + } + + return this.hasTimeOverlap(first, second) && this.hasServiceOverlap(first, second); + } + + private formatSchedule(schedule: ScheduleItem): string { + if (this.getServiceScope(schedule) === "all") { + return `${schedule.from} a ${schedule.to} para todos los servicios`; + } + + return `${schedule.from} a ${schedule.to} para servicios específicos`; + } + + private getEmployeeName(employee: any): string { + const snapshotName = [employee.profileSnapshot?.firstName, employee.profileSnapshot?.lastName].filter(Boolean).join(" ").trim(); + return snapshotName || employee.fullName || employee.name || String(employee._id); + } + + private buildConflict(employee: any, weekDay: number, attempted: ScheduleItem, existing: ScheduleItem): ScheduleConflictDetail { + const employeeId = String(employee._id); + const employeeName = this.getEmployeeName(employee); + const weekDayLabel = WEEK_DAY_LABELS[weekDay] || String(weekDay); + + return { + employeeId, + employeeName, + weekDay, + weekDayLabel, + attempted, + existing, + message: `${employeeName} ya tiene un horario el ${weekDayLabel} de ${this.formatSchedule(existing)} que se superpone con ${this.formatSchedule(attempted)}.`, + }; + } + + private async findScheduleDocument(companyId: string, employeeId: string, weekDay: number) { + return this.schedules.findOne({ companyId, employeeId, weekDay }); + } + + public async applyScheduleChanges(data: ScheduleApplyParams): Promise { + await validateSessionUser({ sessionUser: data.sessionUser }); + await validatePermissionsByCompany({ companyId: data.companyId, sessionUser: data.sessionUser }); + + if (!Array.isArray(data.employeeIds) || data.employeeIds.length === 0) { + throw new Error("Tenés que seleccionar al menos un colaborador."); + } + + if (!Array.isArray(data.weekDays) || data.weekDays.length === 0 || data.weekDays.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) { + throw new Error("Tenés que seleccionar días válidos entre 0 y 6."); + } + + if (data.conflictStrategy !== "reject" && data.conflictStrategy !== "overwrite-conflicts") { + throw new Error("La estrategia de conflictos no es válida."); + } + + const employees = await Promise.all(data.employeeIds.map(async (employeeId) => { + const employee = await EmployeesList.employees.findOne({ companyId: data.companyId, _id: employeeId }); + + if (!employee) { + throw new Error("El empleado no existe o no pertenece a la compañia."); + } + + return employee; + })); + + const conflicts: ScheduleConflictDetail[] = []; + + for (const employee of employees) { + const employeeId = String(employee._id); + + for (const weekDay of data.weekDays) { + const scheduleDocument = await this.findScheduleDocument(data.companyId, employeeId, weekDay); + const existingSchedules = scheduleDocument?.schedules || []; + + for (const attempted of data.schedules) { + const existing = existingSchedules.find((schedule) => this.hasScheduleConflict(attempted, schedule)); + + if (existing) { + conflicts.push(this.buildConflict(employee, weekDay, attempted, existing)); + } + } + } + } + + if (conflicts.length > 0 && data.conflictStrategy === "reject") { + return { + valid: false, + applied: false, + conflicts, + message: "Encontramos horarios superpuestos. Revisalos antes de guardar o elegí sobrescribir solo esos conflictos.", + }; + } + + for (const employee of employees) { + const employeeId = String(employee._id); + + for (const weekDay of data.weekDays) { + const scheduleDocument = await this.findScheduleDocument(data.companyId, employeeId, weekDay); + const existingSchedules = scheduleDocument?.schedules || []; + const nextSchedules = data.conflictStrategy === "overwrite-conflicts" + ? existingSchedules.filter((existing) => !data.schedules.some((attempted) => this.hasScheduleConflict(attempted, existing))) + : existingSchedules; + + const schedules = [...nextSchedules, ...data.schedules]; + + if (scheduleDocument) { + scheduleDocument.schedules = schedules; + await scheduleDocument.save(); + } else { + await this.schedules.create({ + companyId: data.companyId, + employeeId, + weekDay, + schedules, + sessionUser: data.sessionUser, + }); + } + } + } + + return { + valid: conflicts.length === 0, + applied: true, + conflicts, + message: conflicts.length > 0 + ? "Se sobrescribieron solo los horarios en conflicto." + : "Los horarios se guardaron correctamente.", + }; + } + private isScheduleAvailableForService(schedule: ScheduleItem, serviceId?: string): boolean { if (!serviceId || !schedule.serviceScope || schedule.serviceScope === "all") { return true; diff --git a/server/src/Models/SchedulesDisabled/SchedulesDisabled.Adapter.Mongoose.ts b/server/src/Models/SchedulesDisabled/SchedulesDisabled.Adapter.Mongoose.ts index 9045aa9..1e0a13d 100644 --- a/server/src/Models/SchedulesDisabled/SchedulesDisabled.Adapter.Mongoose.ts +++ b/server/src/Models/SchedulesDisabled/SchedulesDisabled.Adapter.Mongoose.ts @@ -32,6 +32,12 @@ export class SchedulesDisabledAdapterMongoose implements ISchedulesDisabledAdapt await this.schedulesDisabledList.deleteOne({ _id: id }).exec(); } + public async find( + filters: Omit + ): Promise { + return this.schedulesDisabledList.find(filters).exec(); + } + public async findOne( filters: Omit ): Promise { diff --git a/server/src/Models/SchedulesDisabled/SchedulesDisabled.Interface.ts b/server/src/Models/SchedulesDisabled/SchedulesDisabled.Interface.ts index 6cc0f4c..cc49e60 100644 --- a/server/src/Models/SchedulesDisabled/SchedulesDisabled.Interface.ts +++ b/server/src/Models/SchedulesDisabled/SchedulesDisabled.Interface.ts @@ -90,6 +90,7 @@ export interface SchedulesDisabledByCollaboratorView { export interface ISchedulesDisabledAdapter { create(data: CreateSchedulesDisabledParams): Promise; delete(id: string): Promise; + find(filters: Omit): Promise; findOne(filters: FindSchedulesDisabledParams): Promise; } @@ -98,6 +99,7 @@ export interface ISchedulesDisabledManager { createSchedulesDisabled(data: CreateSchedulesDisabledParams): Promise; disableSchedule(data: DisableScheduleParams): Promise; enableSchedule(data: DisableScheduleParams): Promise; + find(data: FindSchedulesDisabledParams): Promise; findOne(data: FindSchedulesDisabledParams): Promise; findSchedulesDisabledByCollaborator( data: FindSchedulesDisabledParams diff --git a/server/src/Models/SchedulesDisabled/SchedulesDisabled.ts b/server/src/Models/SchedulesDisabled/SchedulesDisabled.ts index 744721f..bd188da 100644 --- a/server/src/Models/SchedulesDisabled/SchedulesDisabled.ts +++ b/server/src/Models/SchedulesDisabled/SchedulesDisabled.ts @@ -271,6 +271,17 @@ class SchedulesDisabledManager implements ISchedulesDisabledManager { return await this.schedulesDisabled.findOne(data); } + public async find(data: FindSchedulesDisabledParams): Promise { + await validateSessionUser({ sessionUser: data.sessionUser }); + await validatePermissionsByCompany({ + sessionUser: data.sessionUser, + companyId: data.companyId, + }); + + const { sessionUser, ...filters } = data; + return await this.schedulesDisabled.find(filters); + } + public async delete(data: DeleteSchedulesDisabledParams): Promise { await validateSessionUser({ sessionUser: data.sessionUser }); diff --git a/server/src/api/AdminAssistant/AdminAssistant.Controller.ts b/server/src/api/AdminAssistant/AdminAssistant.Controller.ts new file mode 100644 index 0000000..1f6eb06 --- /dev/null +++ b/server/src/api/AdminAssistant/AdminAssistant.Controller.ts @@ -0,0 +1,29 @@ +import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa"; +import { ApiValidationError } from "../../Models/Server.Error.model"; +import { + CollaboratorScheduleDetailsParams, + CollaboratorScheduleDetailsView, +} from "../../Models/AdminAssistant/AdminAssistant.Interface"; +import { authenticateMiddleware } from "../../middleware/authentication"; +import { AdminAssistantService } from "./AdminAssistant.Service"; + +@Route("admin-assistant/collaborator-schedule-details") +@Middlewares(authenticateMiddleware) +export class GetCollaboratorScheduleDetailsController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + public async getCollaboratorScheduleDetails( + @Body() requestBody: CollaboratorScheduleDetailsParams + ): Promise { + try { + const details = await new AdminAssistantService().getCollaboratorScheduleDetails(requestBody); + this.setStatus(200); + return details; + } catch (e) { + const errorOccurred: Error = e as Error; + this.setStatus(500); + return new ApiValidationError(500, errorOccurred.message); + } + } +} diff --git a/server/src/api/AdminAssistant/AdminAssistant.Service.ts b/server/src/api/AdminAssistant/AdminAssistant.Service.ts new file mode 100644 index 0000000..238cc76 --- /dev/null +++ b/server/src/api/AdminAssistant/AdminAssistant.Service.ts @@ -0,0 +1,16 @@ +import { connect } from "mongoose"; +import AdminAssistantList from "../../Models/AdminAssistant/AdminAssistant"; +import { + CollaboratorScheduleDetailsParams, + CollaboratorScheduleDetailsView, +} from "../../Models/AdminAssistant/AdminAssistant.Interface"; + +export class AdminAssistantService { + public async getCollaboratorScheduleDetails( + data: CollaboratorScheduleDetailsParams + ): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + + return await AdminAssistantList.getCollaboratorScheduleDetails(data); + } +} diff --git a/server/src/api/Schedules/Schedules.Controller.ts b/server/src/api/Schedules/Schedules.Controller.ts index d5d8be2..1bde90f 100644 --- a/server/src/api/Schedules/Schedules.Controller.ts +++ b/server/src/api/Schedules/Schedules.Controller.ts @@ -10,6 +10,8 @@ import { PaginateSchedulesResults, FindSchedulesParams, CollaboratorSchedulesView, + ScheduleApplyParams, + ScheduleApplyResult, } from "../../Models/Schedules/Schedules.Interface"; import { authenticateMiddleware } from "../../middleware/authentication"; import { ApiVoidResult } from "../../Models/Api.VoidResult.type"; @@ -36,6 +38,7 @@ export class CreateScheduleController extends Controller { } @Route("schedules/get-all-by-collaborator") +@Middlewares(authenticateMiddleware) export class GetAllSchedulsByCollaboratoreController extends Controller { @Response(500, "Ha ocurrido un error") @SuccessResponse(200, "El empleado fue creado con exito") @@ -81,6 +84,27 @@ export class UpdateScheduleController extends Controller { } } +@Route("schedules/apply") +@Middlewares(authenticateMiddleware) +export class ApplyScheduleController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + public async applyScheduleChanges( + @Body() requestBody: ScheduleApplyParams + ): Promise { + try { + const result = await new SchedulesService().applyScheduleChanges(requestBody); + this.setStatus(200); + return result; + } catch (e) { + const errorOccurred: Error = e as Error; + this.setStatus(500); + return new ApiValidationError(500, errorOccurred.message); + } + } +} + @Route("schedules/delete") @Middlewares(authenticateMiddleware) export class DeleteScheduleController extends Controller { diff --git a/server/src/api/Schedules/Schedules.Service.ts b/server/src/api/Schedules/Schedules.Service.ts index 27b53d2..0ec02a2 100644 --- a/server/src/api/Schedules/Schedules.Service.ts +++ b/server/src/api/Schedules/Schedules.Service.ts @@ -9,6 +9,8 @@ import { PaginateSchedulesResults, CollaboratorSchedulesView, FindSchedulesParams, + ScheduleApplyParams, + ScheduleApplyResult, } from "../../Models/Schedules/Schedules.Interface"; export class SchedulesService { @@ -26,6 +28,12 @@ export class SchedulesService { await ScheduleList.updateSchedule(data); } + public async applyScheduleChanges(data: ScheduleApplyParams): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + + return await ScheduleList.applyScheduleChanges(data); + } + public async deleteSchedule(data: DeleteScheduleParams): Promise { await connect(`${process.env.DATABASE_CONNECTION}`); await ScheduleList.delete(data); diff --git a/server/uploads/services/6a899a56edf89c333b2e626f/image.png b/server/uploads/services/6a899a56edf89c333b2e626f/image.png new file mode 100644 index 0000000000000000000000000000000000000000..e05b6f7fdfb40697f6a3e887c0616dd975edf1f1 GIT binary patch literal 23358 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 ZJvD=?!JuYZko%W2H^>yyp*S)}fB
{topAccessory &&
{topAccessory}
} -

{title}

+ {title &&

{title}

} {description &&

{description}

}
diff --git a/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx b/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx index 7689485..940fc23 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx @@ -14,6 +14,7 @@ dayjs.locale("es"); import { NotificationChannel, ReminderRule } from "@models/NotificationPreferences.model"; import { OrganizationClientView } from "@models/Clients.model"; import { ReservationPeriodDuration, ReservationPeriodsSummary } from "@models/SchedulesEnabled.type"; +import { CompanyServiceView, SERVICE_PUBLISHED_STATUS } from "@models/Service.model"; import OnboardingOrgFlow from "./flows/OnboardingOrgFlow"; import OnboardingServiceFlow from "./flows/OnboardingServiceFlow"; import OnboardingScheduleFlow from "./flows/OnboardingScheduleFlow"; @@ -23,6 +24,8 @@ import UpdateScheduleFlow from "./flows/UpdateScheduleFlow"; import OverrideScheduleFlow from "./flows/OverrideScheduleFlow"; import DisableScheduleFlow from "./flows/DisableScheduleFlow"; import PublicVisibilityFlow from "./flows/PublicVisibilityFlow"; +import ServicePublicationFlow from "./flows/ServicePublicationFlow"; +import CollaboratorScheduleSummaryFlow, { CollaboratorScheduleDetails } from "./flows/CollaboratorScheduleSummaryFlow"; import NotificationsFlow from "./flows/NotificationsFlow"; import NewCollaboratorFlow from "./flows/NewCollaboratorFlow"; import SetupCollaboratorFlow from "./flows/SetupCollaboratorFlow"; @@ -31,6 +34,8 @@ import ArcaOnboardingFlow from "./flows/ArcaOnboardingFlow"; import ArcaTestConnectionFlow, { ArcaWsaaTestResult, ArcaWsfeDiagnosticsResult } from "./flows/ArcaTestConnectionFlow"; import SuccessScreen from "./components/SuccessScreen"; import OrganizationPicker from "./components/OrganizationPicker"; +import QuestionCard from "../QuestionCard/QuestionCard"; +import { getCollaboratorLabel } from "./components/CollaboratorPicker"; import { reservationPeriodOptions } from "./constants/reservationPeriods"; import { NOTIF_CHANNELS, NOTIF_REMINDER_PRESETS } from "./constants/notifications"; import { getEmployeeId, getEmployeeUserId, getTargetEmployees } from "./helpers/employees"; @@ -96,11 +101,18 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const [serviceLimit, setServiceLimit] = useState(1); const [serviceImage, setServiceImage] = useState(null); const [createdServiceId, setCreatedServiceId] = useState(null); + const [serviceCreateError, setServiceCreateError] = useState(""); + const [serviceAssignmentError, setServiceAssignmentError] = useState(""); + const [serviceAssignmentMode, setServiceAssignmentMode] = useState<"options" | "collaborators">("options"); + const [serviceAssignmentWarning, setServiceAssignmentWarning] = useState(""); + const [isLoadingServiceCollaborators, setIsLoadingServiceCollaborators] = useState(false); + const [selectedServiceCollaboratorIds, setSelectedServiceCollaboratorIds] = useState([]); const [employeeId, setEmployeeId] = useState(null); const [doesProvideServices, setDoesProvideServices] = useState(null); // State for Schedule Flow const [workingDaysMode, setWorkingDaysMode] = useState(""); // "mon-fri", "mon-sat", "mon-sun", "custom" + const [specificWeekDay, setSpecificWeekDay] = useState(null); const [openTime, setOpenTime] = useState("09:00"); const [closeTime, setCloseTime] = useState("18:00"); const [hasBreak, setHasBreak] = useState(null); @@ -116,7 +128,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const [isSuccess, setIsSuccess] = useState(false); const [isAddingAnotherService, setIsAddingAnotherService] = useState(false); - const [isResuming, setIsResuming] = useState(false); + const [isResuming, setIsResuming] = useState(true); const [headerImage, setHeaderImage] = useState(null); const [headerColor, setHeaderColor] = useState("#250525"); @@ -133,6 +145,13 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const [servicesShowPublicScores, setServicesShowPublicScores] = useState(true); const [servicesShowPublicOpinions, setServicesShowPublicOpinions] = useState(true); const [publicLinkCopied, setPublicLinkCopied] = useState(false); + const [publicationServices, setPublicationServices] = useState([]); + const [selectedPublicationServiceIds, setSelectedPublicationServiceIds] = useState([]); + const [publicationLoading, setPublicationLoading] = useState(false); + const [publicationMessage, setPublicationMessage] = useState(""); + const [collaboratorScheduleDetails, setCollaboratorScheduleDetails] = useState(null); + const [collaboratorScheduleLoading, setCollaboratorScheduleLoading] = useState(false); + const [collaboratorScheduleMessage, setCollaboratorScheduleMessage] = useState(""); // State for Notifications Flow const [notificationsScope, setNotificationsScope] = useState<"org" | "client" | null>(null); @@ -149,6 +168,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm // State for standalone update schedule flow const [scheduleScope, setScheduleScope] = useState<"me" | "specific" | "all" | null>(null); const [targetEmployeeId, setTargetEmployeeId] = useState(null); + const [selectedScheduleEmployeeIds, setSelectedScheduleEmployeeIds] = useState([]); const [allEmployees, setAllEmployees] = useState([]); const [overrideDate, setOverrideDate] = useState(""); const [allOrganizations, setAllOrganizations] = useState([]); @@ -163,6 +183,11 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm const [reservationPeriodSummary, setReservationPeriodSummary] = useState(null); const [reservationPeriodEmployeesLoading, setReservationPeriodEmployeesLoading] = useState(false); const [reservationPeriodEmployeesError, setReservationPeriodEmployeesError] = useState(null); + const [scheduleServiceScope, setScheduleServiceScope] = useState<"all" | "specific">("all"); + const [scheduleServiceIds, setScheduleServiceIds] = useState([]); + const [scheduleAvailableServices, setScheduleAvailableServices] = useState([]); + const [scheduleConflictMessage, setScheduleConflictMessage] = useState(""); + const [pendingScheduleOverwrite, setPendingScheduleOverwrite] = useState(false); // State for New Collaborator Flow const [inviteEmail, setInviteEmail] = useState(""); @@ -208,6 +233,49 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm return !employee.removed && (isCurrentUser || employee.guestOk === true); }; + const requiresOrganizationContext = () => { + return flowId === "onboarding-service" + || flowId === "onboarding-schedule" + || flowId === "setup-collaborator" + || flowId === "public-visibility" + || flowId === "publish-services" + || flowId === "unpublish-services" + || flowId === "collaborator-schedule-summary" + || flowId === "public-link" + || flowId === "notifications" + || flowId === "whatsapp-bot" + || flowId === "arca-onboarding" + || flowId === "arca-test-connection" + || flowId === "update-schedule" + || flowId === "override-schedule" + || flowId === "disable-schedule" + || flowId === "open-reservation-periods"; + }; + + const loadPublicationServices = async (companyId: string) => { + const publishMode = flowId === "publish-services"; + setPublicationLoading(true); + setPublicationMessage(""); + setSelectedPublicationServiceIds([]); + + try { + const services = await API.post("services/get-by-company", { companyId }); + const filteredServices = (services || []).filter(service => { + if (publishMode) { + return service.published !== SERVICE_PUBLISHED_STATUS.PUBLISHED && !service.banned; + } + return service.published === SERVICE_PUBLISHED_STATUS.PUBLISHED; + }); + setPublicationServices(filteredServices); + } catch (error: any) { + console.error("Error cargando servicios para publicación:", error); + setPublicationServices([]); + setPublicationMessage(error?.message || "No pudimos cargar los servicios. Reintentá en unos segundos."); + } finally { + setPublicationLoading(false); + } + }; + const loadOrganizationContext = (org: any) => { setCreatedCompanyId(org.id || org._id); setSelectedOrganization(org); @@ -299,7 +367,24 @@ 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" || flowId === "open-reservation-periods") { + } else if (flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule" || flowId === "open-reservation-periods" || flowId === "collaborator-schedule-summary") { + if (flowId === "collaborator-schedule-summary") { + setCurrentStepIndex(0); + setAllEmployees([]); + setTargetEmployeeId(null); + setCollaboratorScheduleDetails(null); + setCollaboratorScheduleMessage(""); + } + + if (flowId === "update-schedule") { + setSelectedScheduleEmployeeIds([]); + setScheduleServiceScope("all"); + setScheduleServiceIds([]); + setScheduleAvailableServices([]); + setScheduleConflictMessage(""); + setPendingScheduleOverwrite(false); + } + if (flowId === "open-reservation-periods") { setCurrentStepIndex(0); setReservationPeriodScope(null); @@ -313,7 +398,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm API.post("employees/get-by-company", { companyId: org.id || org._id }) .then(empRes => { - const availableEmployees = flowId === "open-reservation-periods" + const availableEmployees = flowId === "open-reservation-periods" || flowId === "collaborator-schedule-summary" ? (empRes || []).filter((employee: any) => !employee.removed) : (empRes || []).filter(canManageEmployeeSchedules); setAllEmployees(availableEmployees); @@ -360,6 +445,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm setServicesShowPublicScores(services.every(service => service.showPublicScores !== false)); setServicesShowPublicOpinions(services.every(service => service.showPublicOpinions !== false)); }).catch(console.error); + } else if (flowId === "publish-services" || flowId === "unpublish-services") { + setCurrentStepIndex(0); + loadPublicationServices(org.id || org._id); } else if (flowId === "public-link") { setCurrentStepIndex(0); setPublicLinkCopied(false); @@ -448,7 +536,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 === "arca-onboarding" || flowId === "arca-test-connection" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule" || flowId === "open-reservation-periods") { + if (flowId === "new-collaborator" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "publish-services" || flowId === "unpublish-services" || flowId === "collaborator-schedule-summary" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId === "arca-onboarding" || flowId === "arca-test-connection" || flowId.startsWith("onboarding") || flowId === "update-schedule" || flowId === "override-schedule" || flowId === "disable-schedule" || flowId === "open-reservation-periods") { setIsResuming(true); API.post("companies/get-by-user", { sessionUser: SessionInfo.userId }) .then(response => { @@ -469,7 +557,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 === "arca-onboarding" || flowId === "arca-test-connection" || flowId === "open-reservation-periods" || (flowId === "onboarding-org" && action === "new-org")) { + if (flowId === "onboarding-service" || flowId === "onboarding-schedule" || flowId === "setup-collaborator" || flowId === "public-visibility" || flowId === "publish-services" || flowId === "unpublish-services" || flowId === "collaborator-schedule-summary" || flowId === "public-link" || flowId === "notifications" || flowId === "whatsapp-bot" || flowId === "arca-onboarding" || flowId === "arca-test-connection" || flowId === "open-reservation-periods" || (flowId === "onboarding-org" && action === "new-org")) { setCurrentStepIndex(0); } }, [flowId, SessionInfo.metrics.organizationsCount, SessionInfo.userId, router]); @@ -1060,15 +1148,55 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm ); const handleCreateService = async () => { - if (!createdCompanyId || !serviceName || !serviceDescription || !serviceLength || isCreating) return; + setServiceCreateError(""); + const defaultOrganization = selectedOrganization || (allOrganizations.length === 1 ? allOrganizations[0] : null); + let companyId = createdCompanyId || defaultOrganization?.id || defaultOrganization?._id; + const normalizedPrice = typeof servicePrice === "string" ? servicePrice.replace(",", ".") : servicePrice; + const parsedPrice = Number(normalizedPrice); + + if (isCreating) return; + + if (!companyId) { + try { + const organizations = await API.post("companies/get-by-user", { sessionUser: SessionInfo.userId }); + const availableOrganizations = organizations || []; + setAllOrganizations(availableOrganizations); + + if (availableOrganizations.length === 1) { + const organization = availableOrganizations[0]; + companyId = organization.id || organization._id; + loadOrganizationContext(organization); + } + } catch (error) { + console.error("Error cargando organizaciones para crear servicio:", error); + setServiceCreateError("No pudimos cargar tus organizaciones para crear el servicio. Reintentá en unos segundos."); + return; + } + } + + if (!companyId) { + setServiceCreateError("Para crear un servicio primero necesitás tener una organización asociada. Volvé al inicio del asistente y creá una organización."); + return; + } + + if (!serviceName || !serviceDescription || !serviceLength) { + setServiceCreateError("Completá el nombre, la descripción y la duración antes de crear el servicio."); + return; + } + + if (servicePrice === "" || Number.isNaN(parsedPrice)) { + setServiceCreateError("Ingresá el precio del servicio. Si es gratis, cargá 0."); + return; + } + setIsCreating(true); try { const service = await API.post("services/create", { - companyId: createdCompanyId, + companyId, name: serviceName, description: serviceDescription, length: serviceLength, - originalPrice: Number(servicePrice) || 0, + originalPrice: parsedPrice, limit: serviceLimit, sessionUser: SessionInfo.userId }); @@ -1080,7 +1208,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm handleNext(); // Move to Step 4 (Service Image) } catch (error: any) { console.error("Error creando servicio:", error); - alert(error?.message || "Hubo un error al guardar el servicio."); + setServiceCreateError(error?.message || "Hubo un error al guardar el servicio."); } finally { setIsCreating(false); } @@ -1098,6 +1226,8 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm .then(res => { setPreviewServices(res || []); }); + } else if (isStandaloneAction && action === "new-service") { + setCurrentStepIndex(5); } else if (isStandaloneAction) { handleActionSuccess(); } else { @@ -1123,7 +1253,139 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm finalizeService(); } catch (error: any) { console.error("Error subiendo imagen del servicio:", error); - alert(error?.message || "Hubo un error al subir la imagen. Puedes intentarlo más tarde desde tu panel."); + setServiceCreateError(error?.message || "Hubo un error al subir la imagen. Podés intentarlo más tarde desde tu panel."); + } finally { + setIsCreating(false); + } + }; + + const handleAssignCreatedServiceToMe = async () => { + if (!createdCompanyId || !createdServiceId || isCreating) return; + + setIsCreating(true); + setServiceAssignmentError(""); + setServiceAssignmentWarning(""); + try { + const employees = await API.post("employees/get-by-company", { companyId: createdCompanyId }); + const currentEmployee = (employees || []).find((employee: any) => String(getEmployeeUserId(employee)) === String(SessionInfo.userId)); + let finalEmployeeId = currentEmployee ? getEmployeeId(currentEmployee) : null; + + if (!finalEmployeeId) { + const newEmp = await API.post("employees/create", { + companyId: createdCompanyId, + userId: SessionInfo.userId, + roles: ["owner", "employee"], + sessionUser: SessionInfo.userId + }); + finalEmployeeId = newEmp?.id || newEmp?._id; + } + + if (!finalEmployeeId) { + throw new Error("No pudimos inicializar tu perfil de colaborador."); + } + + setEmployeeId(finalEmployeeId); + await API.post("employees/update-collaborator-services", { + companyId: createdCompanyId, + employeeId: finalEmployeeId, + services: [{ serviceId: createdServiceId, active: true }], + sessionUser: SessionInfo.userId + }); + + const schedulesRes = await API.post("schedules/get-all-by-collaborator", { + companyId: createdCompanyId, + employeeId: finalEmployeeId + }); + + if (schedulesRes?.data?.length > 0) { + handleActionSuccess(); + return; + } + + router.push(`/admin/assistant?flow=update-schedule&companyId=${createdCompanyId}`); + } catch (error: any) { + console.error("Error asignando servicio al colaborador:", error); + setServiceAssignmentError(error?.message || "No pudimos asignarte el servicio. Reintentá en unos segundos."); + } finally { + setIsCreating(false); + } + }; + + const loadServiceAssignmentCollaborators = async () => { + if (!createdCompanyId || isCreating || isLoadingServiceCollaborators) return; + + setServiceAssignmentMode("collaborators"); + setServiceAssignmentError(""); + setServiceAssignmentWarning(""); + setIsLoadingServiceCollaborators(true); + try { + const employees = await API.post("employees/get-by-company", { companyId: createdCompanyId }); + const activeEmployees = (employees || []).filter((employee: any) => !employee.removed); + setAllEmployees(activeEmployees); + } catch (error: any) { + console.error("Error cargando colaboradores:", error); + setServiceAssignmentError(error?.message || "No pudimos cargar los colaboradores. Reintentá en unos segundos."); + } finally { + setIsLoadingServiceCollaborators(false); + } + }; + + const toggleServiceCollaboratorSelection = (employee: any) => { + const id = getEmployeeId(employee); + if (!id || isCreating) return; + + setSelectedServiceCollaboratorIds(prev => ( + prev.includes(id) + ? prev.filter(selectedId => selectedId !== id) + : [...prev, id] + )); + }; + + const handleAssignCreatedServiceToCollaborators = async () => { + if (!createdCompanyId || !createdServiceId || isCreating) return; + if (selectedServiceCollaboratorIds.length === 0) { + setServiceAssignmentError("Seleccioná al menos un colaborador para asignar el servicio."); + return; + } + + const selectedEmployees = allEmployees.filter((employee: any) => selectedServiceCollaboratorIds.includes(getEmployeeId(employee))); + if (selectedEmployees.length === 0) { + setServiceAssignmentError("No encontramos los colaboradores seleccionados. Volvé a cargarlos e intentá de nuevo."); + return; + } + + setIsCreating(true); + setServiceAssignmentError(""); + setServiceAssignmentWarning(""); + try { + await Promise.all(selectedEmployees.map((employee: any) => API.post("employees/update-collaborator-services", { + companyId: createdCompanyId, + employeeId: getEmployeeId(employee), + services: [{ serviceId: createdServiceId, active: true }], + sessionUser: SessionInfo.userId + }))); + + const scheduleResults = await Promise.all(selectedEmployees.map(async (employee: any) => { + const employeeId = getEmployeeId(employee); + const schedulesRes = await API.post("schedules/get-all-by-collaborator", { + companyId: createdCompanyId, + employeeId + }); + return { employee, hasSchedules: (schedulesRes?.data || []).length > 0 }; + })); + + const missingSchedules = scheduleResults.filter(result => !result.hasSchedules); + if (missingSchedules.length > 0) { + const names = missingSchedules.map(({ employee }: any) => getCollaboratorLabel(employee)).join(", "); + setServiceAssignmentWarning(`Servicio asignado. Todavía necesitás cargar horarios para: ${names}. Hasta entonces no van a poder recibir reservas.`); + handleActionSuccess(); + return; + } + + handleActionSuccess(); + } catch (error: any) { + console.error("Error asignando servicio a colaboradores:", error); + setServiceAssignmentError(error?.message || "No pudimos asignar el servicio a los colaboradores. Reintentá en unos segundos."); } finally { setIsCreating(false); } @@ -1147,9 +1409,22 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm setServicePrice={setServicePrice} serviceLimit={serviceLimit} setServiceLimit={setServiceLimit} + serviceCreateError={serviceCreateError} handleUploadServiceImage={handleUploadServiceImage} serviceImage={serviceImage} setServiceImage={setServiceImage} + isStandaloneNewService={isStandaloneAction && action === "new-service"} + handleAssignCreatedServiceToMe={handleAssignCreatedServiceToMe} + handleShowCollaboratorAssignment={loadServiceAssignmentCollaborators} + handleToggleServiceCollaborator={toggleServiceCollaboratorSelection} + handleAssignCreatedServiceToCollaborators={handleAssignCreatedServiceToCollaborators} + handleFinishWithoutAssignment={handleActionSuccess} + serviceAssignmentError={serviceAssignmentError} + serviceAssignmentWarning={serviceAssignmentWarning} + serviceAssignmentMode={serviceAssignmentMode} + serviceAssignmentCollaborators={allEmployees} + selectedServiceCollaboratorIds={selectedServiceCollaboratorIds} + isLoadingServiceCollaborators={isLoadingServiceCollaborators} /> ); @@ -1321,57 +1596,160 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm ); - const handleUpdateScheduleSubmit = async () => { + const formatScheduleConflicts = (conflicts: any[]) => { + if (!conflicts || conflicts.length === 0) return ""; + + return conflicts.map((conflict) => conflict.message).join("\n"); + }; + + const getUpdateScheduleTargetEmployees = () => { + if (scheduleScope === "specific") { + return allEmployees.filter((employee: any) => selectedScheduleEmployeeIds.includes(String(getEmployeeId(employee)))); + } + + return getTargetEmployees(allEmployees, scheduleScope, targetEmployeeId, SessionInfo.userId); + }; + + const loadScheduleServicesForEmployees = async (employeeIdsToLoad: string[]) => { + if (!createdCompanyId || employeeIdsToLoad.length === 0) { + setScheduleAvailableServices([]); + return; + } + + const results = await Promise.all(employeeIdsToLoad.map((employeeIdToLoad) => API.post("employees/get-all-services", { + companyId: createdCompanyId, + employeeId: employeeIdToLoad, + sessionUser: SessionInfo.userId + }))); + const serviceGroups = results.map((result) => (result?.services || []).filter((service: any) => service.active)); + + if (serviceGroups.length === 0) { + setScheduleAvailableServices([]); + return; + } + + const commonServiceIds = serviceGroups.slice(1).reduce((commonIds, services) => { + const currentIds = new Set(services.map((service: any) => String(service.id || service._id))); + return commonIds.filter((serviceId: string) => currentIds.has(serviceId)); + }, serviceGroups[0].map((service: any) => String(service.id || service._id))); + const commonServiceIdSet = new Set(commonServiceIds); + const commonServices = serviceGroups[0].filter((service: any) => commonServiceIdSet.has(String(service.id || service._id))); + + setScheduleAvailableServices(commonServices); + setScheduleServiceIds((currentIds) => currentIds.filter((serviceId) => commonServiceIdSet.has(String(serviceId)))); + }; + + const handleToggleScheduleEmployee = (employee: any) => { + const employeeIdToToggle = String(getEmployeeId(employee)); + + setSelectedScheduleEmployeeIds((currentIds) => { + const nextIds = currentIds.includes(employeeIdToToggle) + ? currentIds.filter((id) => id !== employeeIdToToggle) + : [...currentIds, employeeIdToToggle]; + + setScheduleServiceScope("all"); + setScheduleServiceIds([]); + setScheduleAvailableServices([]); + setScheduleConflictMessage(""); + + return nextIds; + }); + }; + + const handleSetUpdateScheduleScope = (scope: "me" | "specific" | "all") => { + setScheduleScope(scope); + setScheduleServiceScope("all"); + setScheduleServiceIds([]); + setScheduleAvailableServices([]); + setScheduleConflictMessage(""); + setPendingScheduleOverwrite(false); + }; + + const handleUpdateScheduleSubmit = async (conflictStrategy: "reject" | "overwrite-conflicts" = "reject") => { if (!createdCompanyId || isCreating || !scheduleScope) return; setIsCreating(true); + setScheduleConflictMessage(""); + setPendingScheduleOverwrite(false); try { - const targetEmployees = getTargetEmployees(allEmployees, scheduleScope, targetEmployeeId, SessionInfo.userId); + const targetEmployees = getUpdateScheduleTargetEmployees(); if (targetEmployees.length === 0) { - alert("No se encontró ningún colaborador seleccionado."); + setScheduleConflictMessage("Seleccioná al menos un colaborador para actualizar sus horarios."); setIsCreating(false); return; } - const workingDays = getWorkingDays(workingDaysMode); + const employeeIds = targetEmployees.map((employee) => getEmployeeId(employee)).filter(Boolean); + const weekDays = workingDaysMode === "specific-day" && specificWeekDay !== null + ? [specificWeekDay] + : getWorkingDays(workingDaysMode); + const canUseSpecificServices = workingDaysMode === "specific-day" && employeeIds.length > 0; + const effectiveServiceScope = canUseSpecificServices ? scheduleServiceScope : "all"; + const schedules = buildScheduleBlocks( + openTime, + closeTime, + hasBreak, + breakStart, + breakEnd, + effectiveServiceScope, + effectiveServiceScope === "specific" ? scheduleServiceIds : [] + ); - for (const emp of targetEmployees) { - const empId = getEmployeeId(emp); - if (!empId) continue; - - for (let i = 0; i <= 6; i++) { - const isWorkingDay = workingDays.includes(i); - const schedules = isWorkingDay ? buildScheduleBlocks(openTime, closeTime, hasBreak, breakStart, breakEnd) : []; - - await API.post("schedules/update", { - companyId: createdCompanyId, - employeeId: empId, - weekDay: i, - schedules: schedules, - sessionUser: SessionInfo.userId - }); - } + if (weekDays.length === 0) { + throw new Error("Seleccioná al menos un día de trabajo."); } + + if (effectiveServiceScope === "specific" && scheduleServiceIds.length === 0) { + throw new Error("Seleccioná al menos un servicio."); + } + + if (effectiveServiceScope === "specific" && scheduleAvailableServices.length === 0) { + throw new Error("Los colaboradores seleccionados no tienen servicios activos en común."); + } + + const result = await API.post("schedules/apply", { + companyId: createdCompanyId, + employeeIds, + weekDays, + schedules, + sessionUser: SessionInfo.userId, + conflictStrategy + }); + + if (result?.valid === false && !result?.applied) { + setScheduleConflictMessage(formatScheduleConflicts(result.conflicts) || result.message); + setPendingScheduleOverwrite(true); + return; + } + handleActionSuccess(); } catch (error: any) { console.error("Error actualizando horarios:", error); - alert("Hubo un error al guardar los horarios: " + error?.message); + setScheduleConflictMessage("Hubo un error al guardar los horarios: " + error?.message); } finally { setIsCreating(false); } }; - const renderUpdateScheduleFlow = () => ( - { + const scheduleTargetEmployees = getUpdateScheduleTargetEmployees(); + const scheduleTargetEmployeeIds = scheduleTargetEmployees.map((employee) => String(getEmployeeId(employee))).filter(Boolean); + const canUseSpecificScheduleServices = scheduleTargetEmployeeIds.length > 0; + + return - ); + scheduleServiceScope={scheduleServiceScope} + setScheduleServiceScope={setScheduleServiceScope} + scheduleServiceIds={scheduleServiceIds} + setScheduleServiceIds={setScheduleServiceIds} + scheduleAvailableServices={scheduleAvailableServices} + loadScheduleServicesForEmployees={loadScheduleServicesForEmployees} + scheduleTargetEmployeeIds={scheduleTargetEmployeeIds} + canUseSpecificScheduleServices={canUseSpecificScheduleServices} + scheduleConflictMessage={scheduleConflictMessage} + pendingScheduleOverwrite={pendingScheduleOverwrite} + setScheduleConflictMessage={setScheduleConflictMessage} + setPendingScheduleOverwrite={setPendingScheduleOverwrite} + />; + }; @@ -1747,6 +2137,45 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm } }; + const togglePublicationService = (serviceId: string) => { + if (isCreating) return; + setSelectedPublicationServiceIds(prev => ( + prev.includes(serviceId) + ? prev.filter(selectedId => selectedId !== serviceId) + : [...prev, serviceId] + )); + }; + + const handleSaveServicePublication = async () => { + if (!createdCompanyId || isCreating) return; + if (selectedPublicationServiceIds.length === 0) { + setPublicationMessage("Seleccioná al menos un servicio para continuar."); + return; + } + + const nextStatus = flowId === "publish-services" + ? SERVICE_PUBLISHED_STATUS.PUBLISHED + : SERVICE_PUBLISHED_STATUS.PRIVATE; + + setIsCreating(true); + setPublicationMessage(""); + try { + await Promise.all(selectedPublicationServiceIds.map(serviceId => + API.post("services/set-published-status", { + serviceId, + published: nextStatus, + sessionUser: SessionInfo.userId + }) + )); + setIsSuccess(true); + } catch (error: any) { + console.error("Error actualizando publicación de servicios:", error); + setPublicationMessage(error?.message || "No pudimos guardar el cambio de publicación. Reintentá en unos segundos."); + } finally { + setIsCreating(false); + } + }; + const renderPublicVisibilityFlow = () => ( ); + const renderServicePublicationFlow = () => ( + + ); + + const handleLoadCollaboratorScheduleDetails = async () => { + if (!createdCompanyId || !targetEmployeeId || collaboratorScheduleLoading) return; + + setCollaboratorScheduleLoading(true); + setCollaboratorScheduleMessage(""); + try { + const details = await API.post("admin-assistant/collaborator-schedule-details", { + companyId: createdCompanyId, + employeeId: targetEmployeeId, + sessionUser: SessionInfo.userId, + }); + setCollaboratorScheduleDetails(details); + setCurrentStepIndex(1); + } catch (error: any) { + console.error("Error consultando horarios del colaborador:", error); + setCollaboratorScheduleMessage(error?.message || "No pudimos cargar los horarios del colaborador. Reintentá en unos segundos."); + } finally { + setCollaboratorScheduleLoading(false); + } + }; + + const renderCollaboratorScheduleSummaryFlow = () => ( + router.push("/admin/assistant")} + /> + ); + const getSelectedOrganizationSlug = () => { return buildSelectedOrganizationSlug(selectedOrganization, orgName); }; @@ -1905,13 +2387,31 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm /> ); const renderFlow = () => { + if (requiresOrganizationContext() && !isResuming && allOrganizations.length === 0 && !createdCompanyId) { + return ( + router.push("/admin/assistant")} + nextLabel="Volver al inicio del asistente" + > +
+ Creá una organización desde el inicio del asistente y después volvé a intentar esta acción. +
+
+ ); + } + if (allOrganizations.length > 1 && !createdCompanyId && action !== "new-org") { return ; } if (flowId === "setup-collaborator") return renderSetupCollaboratorFlow(); if (flowId === "new-collaborator") return renderNewCollaboratorFlow(); + if (flowId === "collaborator-schedule-summary") return renderCollaboratorScheduleSummaryFlow(); if (flowId === "public-visibility") return renderPublicVisibilityFlow(); + if (flowId === "publish-services" || flowId === "unpublish-services") return renderServicePublicationFlow(); if (flowId === "public-link") return renderPublicLinkFlow(); if (flowId === "notifications") return renderNotificationsFlow(); if (flowId === "whatsapp-bot") return renderWhatsAppBotFlow(); @@ -1952,6 +2452,8 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm ); } + const shouldHideFlowTitle = flowId === "collaborator-schedule-summary" && currentStepIndex === 1; + return (
@@ -1960,7 +2462,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm Volver
- Configuración de Negocio + {!shouldHideFlowTitle && "Configuración de Negocio"}
{/* Placeholder for progress dots or percentage */} diff --git a/txclient/src/app/admin/assistant/components/StepEngine/components/CollaboratorPicker.tsx b/txclient/src/app/admin/assistant/components/StepEngine/components/CollaboratorPicker.tsx new file mode 100644 index 0000000..4789f76 --- /dev/null +++ b/txclient/src/app/admin/assistant/components/StepEngine/components/CollaboratorPicker.tsx @@ -0,0 +1,62 @@ +import React from "react"; + +type Props = { + employees: any[]; + selectedIds?: string[]; + selectedId?: string | null; + multiple?: boolean; + disabled?: boolean; + onToggle?: (employee: any) => void; + onSelect?: (employeeId: string) => void; +}; + +const getEmployeeId = (employee: any) => employee.id || employee._id || employee.employeeId; + +export const getCollaboratorLabel = (employee: any) => { + const fullName = employee.fullName || [employee.firstName, employee.lastName].filter(Boolean).join(" ").trim(); + return fullName || employee.email || employee.userId?.email || employee.userId?.name || "Colaborador"; +}; + +export default function CollaboratorPicker({ employees, selectedIds = [], selectedId = null, multiple = false, disabled = false, onToggle, onSelect }: Props) { + return ( +
+ {employees.map((employee: any) => { + const employeeId = getEmployeeId(employee); + const isSelected = multiple ? selectedIds.includes(employeeId) : selectedId === employeeId; + const label = getCollaboratorLabel(employee); + + return ( + + ); + })} +
+ ); +} diff --git a/txclient/src/app/admin/assistant/components/StepEngine/components/SuccessScreen.tsx b/txclient/src/app/admin/assistant/components/StepEngine/components/SuccessScreen.tsx index 03b5118..4e3d092 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/components/SuccessScreen.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/components/SuccessScreen.tsx @@ -14,6 +14,8 @@ export default function SuccessScreen(props: Props) { if (flowId === "override-schedule" || flowId === "disable-schedule") return "El horario especial ha sido guardado exitosamente."; if (flowId === "new-collaborator") return "El colaborador fue invitado exitosamente."; if (flowId === "setup-collaborator") return "El colaborador ha sido configurado exitosamente y ya puede comenzar a recibir reservas."; + if (flowId === "publish-services") return "Los servicios seleccionados ya están publicados."; + if (flowId === "unpublish-services") return "Los servicios seleccionados dejaron de estar publicados."; 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."; diff --git a/txclient/src/app/admin/assistant/components/StepEngine/flows/CollaboratorScheduleSummaryFlow.tsx b/txclient/src/app/admin/assistant/components/StepEngine/flows/CollaboratorScheduleSummaryFlow.tsx new file mode 100644 index 0000000..71a1cee --- /dev/null +++ b/txclient/src/app/admin/assistant/components/StepEngine/flows/CollaboratorScheduleSummaryFlow.tsx @@ -0,0 +1,234 @@ +import React from "react"; +import dayjs from "dayjs"; +import QuestionCard from "../../QuestionCard/QuestionCard"; +import CollaboratorPicker from "../components/CollaboratorPicker"; + +type ScheduleItem = { + from: string; + to: string; + disabled: boolean; + serviceScope?: "all" | "specific"; + serviceIds?: string[]; +}; + +type WeeklySchedule = { + scheduleId: string; + weekDay: number; + schedules: ScheduleItem[]; +}; + +type DisabledSchedule = { + id?: string; + _id?: string; + startDate: Date | string; + endDate: Date | string; +}; + +type ScheduleOverride = { + id?: string; + _id?: string; + date: Date | string; + schedules: ScheduleItem[]; +}; + +export type CollaboratorScheduleDetails = { + collaborator: { + employeeId: string; + fullName: string; + email: string; + avatar: string; + userId: string; + }; + servicesById: Record; + weeklySchedules: WeeklySchedule[]; + futureDisabledSchedules: DisabledSchedule[]; + futureOverrides: ScheduleOverride[]; +}; + +type Props = { + currentStepIndex: number; + employees: any[]; + selectedEmployeeId: string | null; + setSelectedEmployeeId: (employeeId: string) => void; + details: CollaboratorScheduleDetails | null; + loading: boolean; + message: string; + selectedOrganization?: any; + orgName: string; + loadDetails: () => void; + goToAssistantHome: () => void; +}; + +const weekDays = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"]; +const weekDayDisplayOrder = [1, 2, 3, 4, 5, 6, 0]; + +const cardStyle: React.CSSProperties = { + padding: "14px", + borderRadius: "14px", + background: "rgba(255,255,255,0.05)", + border: "1px solid rgba(255,255,255,0.12)", + color: "white", +}; + +const mutedStyle: React.CSSProperties = { + color: "rgba(255,255,255,0.7)", + fontSize: "14px", + lineHeight: 1.45, +}; + +const formatDate = (date: Date | string) => dayjs(date).format("DD/MM/YYYY"); + +const formatSchedule = (schedule: ScheduleItem, servicesById: CollaboratorScheduleDetails["servicesById"] = {}) => { + const selectedServices = (schedule.serviceIds || []) + .map(serviceId => servicesById[serviceId]?.name || `Servicio ${serviceId}`) + .filter(Boolean); + const scope = schedule.serviceScope === "specific" + ? selectedServices.length > 0 ? selectedServices.join(", ") : "servicios específicos sin nombre disponible" + : "todos los servicios"; + return `${schedule.from} a ${schedule.to}${schedule.disabled ? " (deshabilitado)" : ""} · ${scope}`; +}; + +function ExpandableItem({ title, subtitle, children }: { title: string; subtitle: string; children: React.ReactNode }) { + const [open, setOpen] = React.useState(false); + + return ( + + ); +} + +export default function CollaboratorScheduleSummaryFlow(props: Props) { + const { + currentStepIndex, + employees, + selectedEmployeeId, + setSelectedEmployeeId, + details, + loading, + message, + selectedOrganization, + orgName, + loadDetails, + goToAssistantHome, + } = props; + const configuredWeeklySchedules = details?.weeklySchedules + .filter(day => day.schedules.length > 0) + .sort((a, b) => weekDayDisplayOrder.indexOf(a.weekDay) - weekDayDisplayOrder.indexOf(b.weekDay)) || []; + + if (currentStepIndex === 0) { + return ( + +
+
+ Organización: {selectedOrganization?.name || orgName || "seleccionada"} +
+ {message &&
{message}
} + {employees.length === 0 ? ( +
No encontramos colaboradores activos en esta organización.
+ ) : ( + + )} +
+
+ ); + } + + return ( + +
+ {message &&
{message}
} + {!details ? ( +
No hay datos para mostrar.
+ ) : ( + <> +
+ {details.collaborator.fullName} +
+ {details.collaborator.fullName} +
{details.collaborator.email || "Sin email registrado"}
+
+
+ +
+

Horarios semanales

+ {configuredWeeklySchedules.length === 0 ? ( +
Este colaborador no tiene horarios semanales configurados.
+ ) : configuredWeeklySchedules.map(day => ( +
+ {weekDays[day.weekDay] || `Día ${day.weekDay}`} +
+ {day.schedules.map((schedule, index) => ( + {formatSchedule(schedule, details.servicesById)} + ))} +
+
+ ))} +
+ +
+

Excepciones futuras

+ {details.futureOverrides.length === 0 ? ( +
No hay excepciones futuras configuradas.
+ ) : details.futureOverrides.map((override, index) => ( + +
+ {override.schedules.map((schedule, scheduleIndex) => ( + {formatSchedule(schedule, details.servicesById)} + ))} +
+
+ ))} +
+ +
+

Restricciones / agenda cerrada

+ {details.futureDisabledSchedules.length === 0 ? ( +
No hay cierres de agenda futuros configurados.
+ ) : details.futureDisabledSchedules.map((disabled, index) => ( + +
Durante este período no se muestran turnos disponibles para el colaborador.
+
+ ))} +
+ + )} +
+
+ ); +} diff --git a/txclient/src/app/admin/assistant/components/StepEngine/flows/OnboardingServiceFlow.tsx b/txclient/src/app/admin/assistant/components/StepEngine/flows/OnboardingServiceFlow.tsx index ecc3b8c..adcd600 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/flows/OnboardingServiceFlow.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/flows/OnboardingServiceFlow.tsx @@ -1,6 +1,7 @@ import React from "react"; import QuestionCard from "../../QuestionCard/QuestionCard"; import QuestionInput from "../../QuestionCard/QuestionInput"; +import CollaboratorPicker from "../components/CollaboratorPicker"; type OnboardingServiceFlowProps = { heatMapFraction: any; @@ -19,17 +20,51 @@ type OnboardingServiceFlowProps = { setServicePrice: any; serviceLimit: any; setServiceLimit: any; + serviceCreateError: any; handleUploadServiceImage: any; serviceImage: any; setServiceImage: any; + isStandaloneNewService: any; + handleAssignCreatedServiceToMe: any; + handleShowCollaboratorAssignment: any; + handleToggleServiceCollaborator: any; + handleAssignCreatedServiceToCollaborators: any; + handleFinishWithoutAssignment: any; + serviceAssignmentError: any; + serviceAssignmentWarning: any; + serviceAssignmentMode: any; + serviceAssignmentCollaborators: any; + selectedServiceCollaboratorIds: any; + isLoadingServiceCollaborators: any; }; export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps) { - const { heatMapFraction, currentStepIndex, handleNext, serviceName, setServiceName, serviceDescription, setServiceDescription, handleKeyDown, serviceLength, setServiceLength, handleCreateService, isCreating, servicePrice, setServicePrice, serviceLimit, setServiceLimit, handleUploadServiceImage, serviceImage, setServiceImage } = props; - const fraction = heatMapFraction || 60; - const durationOptions = [fraction, fraction * 2, fraction * 3, fraction * 4]; + const { heatMapFraction, currentStepIndex, handleNext, serviceName, setServiceName, serviceDescription, setServiceDescription, handleKeyDown, serviceLength, setServiceLength, handleCreateService, isCreating, servicePrice, setServicePrice, serviceLimit, setServiceLimit, serviceCreateError, handleUploadServiceImage, serviceImage, setServiceImage, isStandaloneNewService, handleAssignCreatedServiceToMe, handleShowCollaboratorAssignment, handleToggleServiceCollaborator, handleAssignCreatedServiceToCollaborators, handleFinishWithoutAssignment, serviceAssignmentError, serviceAssignmentWarning, serviceAssignmentMode, serviceAssignmentCollaborators, selectedServiceCollaboratorIds, isLoadingServiceCollaborators } = props; + const [isCustomDuration, setIsCustomDuration] = React.useState(false); + const [customDurationValue, setCustomDurationValue] = React.useState(""); + const fraction = heatMapFraction || 60; + const durationOptions = [fraction, fraction * 2, fraction * 3, fraction * 4]; + const customDurationMinutes = Number(customDurationValue); + const isValidCustomDuration = Number.isInteger(customDurationMinutes) && customDurationMinutes > 0; - return ( + const selectDurationOption = (duration: number) => { + setIsCustomDuration(false); + setCustomDurationValue(""); + setServiceLength(duration); + }; + + const selectCustomDuration = () => { + setIsCustomDuration(true); + setServiceLength(isValidCustomDuration ? customDurationMinutes : null); + }; + + const updateCustomDuration = (value: string) => { + setCustomDurationValue(value); + const parsedValue = Number(value); + setServiceLength(Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null); + }; + + return ( <>
{durationOptions.map(val => { @@ -88,15 +123,15 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps) return (
setServiceLength(val)} + onClick={() => selectDurationOption(val)} style={{ padding: '12px 24px', borderRadius: '12px', cursor: 'pointer', - background: serviceLength === val ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${serviceLength === val ? 'var(--wine-red)' : 'rgba(255,255,255,0.1)'}`, + background: !isCustomDuration && serviceLength === val ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', + border: `2px solid ${!isCustomDuration && serviceLength === val ? 'var(--wine-red)' : 'rgba(255,255,255,0.1)'}`, color: 'white', - fontWeight: serviceLength === val ? 'bold' : 'normal', + fontWeight: !isCustomDuration && serviceLength === val ? 'bold' : 'normal', transition: 'all 0.2s ease', fontSize: '16px' }} @@ -105,7 +140,44 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
); })} +
+ Otro +
+ {isCustomDuration && ( +
+ updateCustomDuration(e.target.value)} + onKeyDown={(e) => { + if (isValidCustomDuration) { + handleKeyDown(e, handleNext); + } + }} + autoFocus + /> +
+ Ingresá la duración total del servicio expresada en minutos. +
+
+ )}
@@ -122,7 +194,8 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
$ setServicePrice(e.target.value)} @@ -163,6 +236,20 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps) ))}
+ {serviceCreateError && ( +
+ {serviceCreateError} +
+ )}
@@ -172,7 +259,7 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps) description="Sube una foto representativa para que los clientes sepan qué ofreces. (Opcional)" onNext={handleUploadServiceImage} disableNext={isCreating} - nextLabel={isCreating ? "Subiendo..." : (serviceImage ? "Subir y Finalizar" : "Omitir por ahora")} + nextLabel={isCreating ? "Subiendo..." : (serviceImage ? (isStandaloneNewService ? "Subir y continuar" : "Subir y Finalizar") : "Omitir por ahora")} >
+ {serviceCreateError && ( +
+ {serviceCreateError} +
+ )}
+ + {isStandaloneNewService && ( + +
+ + + {serviceAssignmentMode === "collaborators" && ( +
+ {serviceAssignmentCollaborators.length === 0 && !isLoadingServiceCollaborators && ( +
+ No encontramos colaboradores activos para esta organización. +
+ )} + + {serviceAssignmentCollaborators.length > 0 && ( + + )} +
+ )} + {!serviceAssignmentWarning && ( + <> + +
+ Si lo configurás después, el servicio no va a estar disponible para reservas hasta que lo asignes a un colaborador con horarios cargados. +
+ + )} + {serviceAssignmentError && ( +
+ {serviceAssignmentError} +
+ )} +
+
+ )} ); diff --git a/txclient/src/app/admin/assistant/components/StepEngine/flows/ServicePublicationFlow.tsx b/txclient/src/app/admin/assistant/components/StepEngine/flows/ServicePublicationFlow.tsx new file mode 100644 index 0000000..d0a2bf5 --- /dev/null +++ b/txclient/src/app/admin/assistant/components/StepEngine/flows/ServicePublicationFlow.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { CompanyServiceView } from "@models/Service.model"; +import { getServiceImage } from "@core/helpers/getServiceImage"; +import QuestionCard from "../../QuestionCard/QuestionCard"; + +type Props = { + mode: "publish" | "unpublish"; + currentStepIndex: number; + services: CompanyServiceView[]; + selectedServiceIds: string[]; + toggleService: (serviceId: string) => void; + handleSave: () => void; + isCreating: boolean; + isLoading: boolean; + message: string; + selectedOrganization?: any; + orgName: string; +}; + +export default function ServicePublicationFlow(props: Props) { + const { mode, currentStepIndex, services, selectedServiceIds, toggleService, handleSave, isCreating, isLoading, message, selectedOrganization, orgName } = props; + const isPublish = mode === "publish"; + const title = isPublish ? "Publicá tus servicios" : "Retirá servicios de la página pública"; + const description = isPublish + ? "Elegí uno o más servicios privados para mostrarlos en tu página pública. Los servicios bloqueados no se pueden publicar desde acá." + : "Elegí uno o más servicios publicados para dejar de mostrarlos en la página pública."; + const emptyMessage = isPublish + ? "No encontramos servicios privados disponibles para publicar. Puede que ya estén publicados o que estén bloqueados." + : "No encontramos servicios publicados para retirar."; + const nextLabel = isCreating + ? (isPublish ? "Publicando..." : "Quitando publicación...") + : (isPublish ? "Publicar servicios" : "Quitar publicación"); + + return ( + +
+
+ Organización: {selectedOrganization?.name || orgName || "seleccionada"} +
+ + {message && ( +
+ {message} +
+ )} + + {!isLoading && services.length === 0 && ( +
+ {emptyMessage} +
+ )} + + {services.map(service => { + const selected = selectedServiceIds.includes(service.id); + return ( + + ); + })} +
+
+ ); +} diff --git a/txclient/src/app/admin/assistant/components/StepEngine/flows/UpdateScheduleFlow.tsx b/txclient/src/app/admin/assistant/components/StepEngine/flows/UpdateScheduleFlow.tsx index 0cdf792..7525595 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/flows/UpdateScheduleFlow.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/flows/UpdateScheduleFlow.tsx @@ -1,234 +1,241 @@ -import React from "react"; import QuestionCard from "../../QuestionCard/QuestionCard"; +import CollaboratorPicker from "../components/CollaboratorPicker"; type Props = any; +const weekDays = [ + { id: 1, label: "Lunes" }, + { id: 2, label: "Martes" }, + { id: 3, label: "Miércoles" }, + { id: 4, label: "Jueves" }, + { id: 5, label: "Viernes" }, + { id: 6, label: "Sábado" }, + { id: 0, label: "Domingo" }, +]; + +const optionStyle = (active: boolean) => ({ + padding: "15px", + borderRadius: "12px", + cursor: "pointer", + background: active ? "var(--wine-red)" : "rgba(255,255,255,0.05)", + border: `2px solid ${active ? "var(--wine-red)" : "transparent"}`, + color: "white", + textAlign: "center" as const, + transition: "all 0.2s ease", +}); + export default function UpdateScheduleFlow(props: Props) { - const { currentStepIndex, scheduleScope, setScheduleScope, targetEmployeeId, setTargetEmployeeId, allEmployees, handleNext, workingDaysMode, setWorkingDaysMode, openTime, setOpenTime, closeTime, setCloseTime, hasBreak, setHasBreak, breakStart, setBreakStart, breakEnd, setBreakEnd, isCreating, handleUpdateScheduleSubmit, handleOverrideScheduleSubmit, disableDuration, setDisableDuration, disableStartDate, setDisableStartDate, disableEndDate, setDisableEndDate, isFullDayDisabled, setIsFullDayDisabled, disableStartTime, setDisableStartTime, disableEndTime, setDisableEndTime, handleDisableScheduleSubmit } = props; + const { + currentStepIndex, + scheduleScope, + setScheduleScope, + selectedScheduleEmployeeIds, + handleToggleScheduleEmployee, + allEmployees, + handleNext, + workingDaysMode, + setWorkingDaysMode, + specificWeekDay, + setSpecificWeekDay, + openTime, + setOpenTime, + closeTime, + setCloseTime, + hasBreak, + setHasBreak, + breakStart, + setBreakStart, + breakEnd, + setBreakEnd, + isCreating, + handleUpdateScheduleSubmit, + scheduleServiceScope, + setScheduleServiceScope, + scheduleServiceIds, + setScheduleServiceIds, + scheduleAvailableServices, + loadScheduleServicesForEmployees, + scheduleTargetEmployeeIds, + canUseSpecificScheduleServices, + scheduleConflictMessage, + pendingScheduleOverwrite, + setScheduleConflictMessage, + setPendingScheduleOverwrite, + } = props; + + const isSpecificDay = workingDaysMode === "specific-day"; + const weekDayStep = 2; + const timeStep = isSpecificDay ? 3 : 2; + const breakStep = isSpecificDay ? 4 : 3; + const breakTimeStep = isSpecificDay ? 5 : 4; + const serviceScopeStep = hasBreak ? 6 : 5; + const hasNoCommonServices = scheduleServiceScope === "specific" && scheduleAvailableServices.length === 0; + + const continueAfterBreak = () => { + if (isSpecificDay) { + handleNext(); + return; + } + + handleUpdateScheduleSubmit(); + }; + + const handleServiceScopeSelect = async (scope: "all" | "specific") => { + setScheduleConflictMessage(""); + setScheduleServiceScope(scope); + + if (scope === "specific" && canUseSpecificScheduleServices) { + await loadScheduleServicesForEmployees(scheduleTargetEmployeeIds); + } + }; + + const toggleService = (serviceId: string) => { + setScheduleServiceIds( + scheduleServiceIds.includes(serviceId) + ? scheduleServiceIds.filter((id: string) => id !== serviceId) + : [...scheduleServiceIds, serviceId] + ); + }; + + const renderConflictActions = () => { + if (!scheduleConflictMessage) return null; + return ( <> - { - if (scheduleScope === "specific" && !targetEmployeeId) { - alert("Por favor selecciona un colaborador."); - return; - } - handleNext(); - }} - disableNext={!scheduleScope || (scheduleScope === "specific" && !targetEmployeeId)} - > -
-
setScheduleScope("me")} - style={{ - padding: '15px', borderRadius: '12px', cursor: 'pointer', - background: scheduleScope === "me" ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${scheduleScope === "me" ? 'var(--wine-red)' : 'transparent'}`, - color: 'white', textAlign: 'center', transition: 'all 0.2s ease' - }} - > -

Solo a mí

-

Aplica esta rutina únicamente a tus horarios.

-
-
setScheduleScope("specific")} - style={{ - padding: '15px', borderRadius: '12px', cursor: 'pointer', - background: scheduleScope === "specific" ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${scheduleScope === "specific" ? 'var(--wine-red)' : 'transparent'}`, - color: 'white', textAlign: 'center', transition: 'all 0.2s ease' - }} - > -

A un colaborador en particular

-

Aplica esta rutina a un integrante específico de tu equipo.

-
-
setScheduleScope("all")} - style={{ - padding: '15px', borderRadius: '12px', cursor: 'pointer', - background: scheduleScope === "all" ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${scheduleScope === "all" ? 'var(--wine-red)' : 'transparent'}`, - color: 'white', textAlign: 'center', transition: 'all 0.2s ease' - }} - > -

A todos mis colaboradores

-

Sobrescribe masivamente los horarios de todo tu equipo.

-
- - {scheduleScope === "specific" && ( -
- - -
- )} +
+ {scheduleConflictMessage} +
+ {pendingScheduleOverwrite && ( +
+ +
- - - -
- {[ - { id: 'mon-fri', label: 'Lunes a Viernes' }, - { id: 'mon-sat', label: 'Lunes a Sábado' }, - { id: 'mon-sun', label: 'Todos los días' } - ].map(mode => ( -
setWorkingDaysMode(mode.id)} - style={{ - padding: '15px', - borderRadius: '12px', - cursor: 'pointer', - background: workingDaysMode === mode.id ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${workingDaysMode === mode.id ? 'var(--wine-red)' : 'transparent'}`, - color: 'white', - textAlign: 'center', - transition: 'all 0.2s ease' - }} - > - {mode.label} -
- ))} -
-
- - -
-
- - setOpenTime(e.target.value)} - style={{ - padding: '10px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', - background: 'rgba(255,255,255,0.1)', color: 'white', outline: 'none', - fontSize: '18px', fontFamily: 'monospace' - }} - /> -
-
- - setCloseTime(e.target.value)} - style={{ - padding: '10px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', - background: 'rgba(255,255,255,0.1)', color: 'white', outline: 'none', - fontSize: '18px', fontFamily: 'monospace' - }} - /> -
-
-
- - { - if (hasBreak) { - handleNext(); - } else { - handleUpdateScheduleSubmit(); - } - }} - disableNext={hasBreak === null} - nextLabel={hasBreak ? "Siguiente" : (isCreating ? "Guardando..." : "Guardar Horarios")} - > -
-
setHasBreak(true)} - style={{ - flex: 1, padding: '15px', borderRadius: '12px', cursor: 'pointer', - background: hasBreak === true ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${hasBreak === true ? 'var(--wine-red)' : 'transparent'}`, - color: 'white', textAlign: 'center' - }} - > - Sí, hay descanso -
-
setHasBreak(false)} - style={{ - flex: 1, padding: '15px', borderRadius: '12px', cursor: 'pointer', - background: hasBreak === false ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)', - border: `2px solid ${hasBreak === false ? 'var(--wine-red)' : 'transparent'}`, - color: 'white', textAlign: 'center' - }} - > - No, horario corrido -
-
-
- - -
-
- - setBreakStart(e.target.value)} - style={{ - padding: '10px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', - background: 'rgba(255,255,255,0.1)', color: 'white', outline: 'none', - fontSize: '18px', fontFamily: 'monospace' - }} - /> -
-
- - setBreakEnd(e.target.value)} - style={{ - padding: '10px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', - background: 'rgba(255,255,255,0.1)', color: 'white', outline: 'none', - fontSize: '18px', fontFamily: 'monospace' - }} - /> -
-
-
+ )} ); - } + }; + + return ( + <> + +
+
setScheduleScope("me")} style={optionStyle(scheduleScope === "me")}> +

Solo a mí

+

Aplica esta rutina únicamente a tus horarios.

+
+
setScheduleScope("specific")} style={optionStyle(scheduleScope === "specific")}> +

A uno o más colaboradores

+

Aplica esta rutina a uno o varios integrantes específicos de tu equipo.

+
+
setScheduleScope("all")} style={optionStyle(scheduleScope === "all")}> +

A todos mis colaboradores

+

Actualiza masivamente los horarios de todo tu equipo.

+
+ + {scheduleScope === "specific" && ( +
+ + +
+ )} +
+
+ + +
+ {[ + { id: "mon-fri", label: "Lunes a Viernes" }, + { id: "mon-sat", label: "Lunes a Sábado" }, + { id: "mon-sun", label: "Todos los días" }, + { id: "specific-day", label: "Día específico" }, + ].map((mode) => ( +
setWorkingDaysMode(mode.id)} style={optionStyle(workingDaysMode === mode.id)}> + {mode.label} +
+ ))} +
+
+ + +
+ {weekDays.map((day) => ( +
setSpecificWeekDay(day.id)} style={optionStyle(specificWeekDay === day.id)}> + {day.label} +
+ ))} +
+
+ + +
+
+ + setOpenTime(e.target.value)} style={{ padding: "10px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", color: "white", outline: "none", fontSize: "18px", fontFamily: "monospace" }} /> +
+
+ + setCloseTime(e.target.value)} style={{ padding: "10px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", color: "white", outline: "none", fontSize: "18px", fontFamily: "monospace" }} /> +
+
+
+ + +
+
+
setHasBreak(true)} style={{ ...optionStyle(hasBreak === true), flex: 1 }}>Sí, hay descanso
+
setHasBreak(false)} style={{ ...optionStyle(hasBreak === false), flex: 1 }}>No, horario corrido
+
+ {renderConflictActions()} +
+
+ + +
+
+
+ + setBreakStart(e.target.value)} style={{ padding: "10px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", color: "white", outline: "none", fontSize: "18px", fontFamily: "monospace" }} /> +
+
+ + setBreakEnd(e.target.value)} style={{ padding: "10px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", color: "white", outline: "none", fontSize: "18px", fontFamily: "monospace" }} /> +
+
+ {renderConflictActions()} +
+
+ + handleUpdateScheduleSubmit()} disableNext={isCreating || (scheduleServiceScope === "specific" && (scheduleServiceIds.length === 0 || hasNoCommonServices))} nextLabel={isCreating ? "Guardando..." : "Guardar Horarios"}> +
+ {!canUseSpecificScheduleServices && ( +

Seleccioná al menos un colaborador para configurar servicios específicos.

+ )} +
handleServiceScopeSelect("all")} style={optionStyle(scheduleServiceScope === "all")}>Todos los servicios
+
canUseSpecificScheduleServices && handleServiceScopeSelect("specific")} style={{ ...optionStyle(scheduleServiceScope === "specific"), opacity: canUseSpecificScheduleServices ? 1 : 0.5, cursor: canUseSpecificScheduleServices ? "pointer" : "not-allowed" }}>Servicios específicos
+ + {scheduleServiceScope === "specific" && canUseSpecificScheduleServices && ( +
+ {hasNoCommonServices && ( +

Los colaboradores seleccionados no tienen servicios activos en común. Usá todos los servicios o cambiá la selección.

+ )} + {scheduleAvailableServices.map((service: any) => ( + + ))} +
+ )} + + {renderConflictActions()} +
+
+ + ); +} diff --git a/txclient/src/app/admin/assistant/components/StepEngine/helpers/schedules.ts b/txclient/src/app/admin/assistant/components/StepEngine/helpers/schedules.ts index 5d948aa..860121a 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/helpers/schedules.ts +++ b/txclient/src/app/admin/assistant/components/StepEngine/helpers/schedules.ts @@ -1,4 +1,10 @@ -type ScheduleBlock = { from: string; to: string; disabled: boolean }; +type ScheduleBlock = { + from: string; + to: string; + disabled: boolean; + serviceScope?: "all" | "specific"; + serviceIds?: string[]; +}; export const getWorkingDays = (workingDaysMode: string, customDays: number[] = []) => { if (workingDaysMode === "mon-fri") return [1, 2, 3, 4, 5]; @@ -13,14 +19,18 @@ export const buildScheduleBlocks = ( closeTime: string, hasBreak: boolean | null, breakStart: string, - breakEnd: string + breakEnd: string, + serviceScope: "all" | "specific" = "all", + serviceIds: string[] = [] ): ScheduleBlock[] => { + const serviceData = serviceScope === "specific" ? { serviceScope, serviceIds } : { serviceScope: "all" as const, serviceIds: [] }; + if (hasBreak) { return [ - { from: openTime, to: breakStart, disabled: false }, - { from: breakEnd, to: closeTime, disabled: false } + { from: openTime, to: breakStart, disabled: false, ...serviceData }, + { from: breakEnd, to: closeTime, disabled: false, ...serviceData } ]; } - return [{ from: openTime, to: closeTime, disabled: false }]; + return [{ from: openTime, to: closeTime, disabled: false, ...serviceData }]; }; diff --git a/txclient/src/app/components/ServicePrivate/ServicePrivate.module.css b/txclient/src/app/components/ServicePrivate/ServicePrivate.module.css new file mode 100644 index 0000000..4cfb597 --- /dev/null +++ b/txclient/src/app/components/ServicePrivate/ServicePrivate.module.css @@ -0,0 +1,217 @@ +.page { + width: 100vw; + min-height: 100dvh; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: + radial-gradient(circle at 20% 20%, rgba(255, 42, 127, 0.18), transparent 34%), + linear-gradient(180deg, var(--wine-darkest) 0%, var(--wine-dark) 100%); +} + +.card { + width: min(860px, calc(100% - 48px)); + display: grid; + grid-template-columns: minmax(260px, 0.9fr) minmax(300px, 1fr); + gap: 28px; + align-items: center; + padding: clamp(22px, 3vw, 36px); + border-radius: 28px; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 18px 54px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.42); + backdrop-filter: blur(8px); +} + +.illustrationPanel { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + border-radius: 24px; + background: transparent; +} + +.illustration { + width: min(100%, 360px); + height: auto; + object-fit: contain; +} + +.content { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + max-width: 430px; +} + +.badge { + display: inline-flex; + padding: 8px 14px; + border-radius: 999px; + color: var(--wine-red); + background: rgba(255, 42, 127, 0.1); + font-size: 0.85rem; + font-weight: 800; + letter-spacing: 0.02em; +} + +.content h1 { + margin: 0; + color: var(--wine-darkest); + font-size: clamp(2rem, 4vw, 3.15rem); + line-height: 1.04; + letter-spacing: -0.04em; +} + +.content p { + margin: 0; + color: rgba(37, 5, 37, 0.72); + font-size: 1.05rem; + line-height: 1.65; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 8px; +} + +.primaryAction, +.secondaryAction { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 46px; + padding: 0 22px; + border-radius: 999px; + font-weight: 800; + text-decoration: none; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.primaryAction { + color: white; + background: var(--wine-red); + box-shadow: 0 12px 26px rgba(255, 42, 127, 0.28); +} + +.secondaryAction { + color: var(--wine-red); + background: rgba(255, 42, 127, 0.08); +} + +.primaryAction:hover, +.secondaryAction:hover { + transform: translateY(-2px); +} + +@media (max-width: 760px) { + .page { + align-items: flex-start; + padding: 88px 16px 24px; + } + + .card { + grid-template-columns: 1fr; + gap: 18px; + text-align: center; + width: min(420px, calc(100% - 32px)); + padding: 22px 20px 24px; + border-radius: 24px; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 14px 38px rgba(0, 0, 0, 0.18); + } + + .content { + align-items: center; + max-width: 100%; + } + + .illustrationPanel { + min-height: 0; + padding: 16px; + border-radius: 20px; + } + + .illustration { + width: min(100%, 260px); + max-height: 230px; + } + + .content { + gap: 12px; + } + + .content h1 { + max-width: 12ch; + font-size: clamp(1.8rem, 8vw, 2.35rem); + line-height: 1.05; + } + + .content p { + max-width: 27ch; + font-size: 0.98rem; + line-height: 1.55; + } + + .actions { + width: 100%; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 4px; + } + + .primaryAction, + .secondaryAction { + width: min(100%, 220px); + } +} + +@media (max-width: 420px) { + .page { + padding: 78px 12px 18px; + } + + .card { + width: min(390px, calc(100% - 24px)); + padding: 18px 18px 22px; + gap: 14px; + border-radius: 22px; + } + + .illustrationPanel { + padding: 12px; + } + + .illustration { + width: min(100%, 230px); + max-height: 205px; + } + + .badge { + padding: 7px 12px; + font-size: 0.78rem; + } + + .content h1 { + font-size: clamp(1.65rem, 7.5vw, 2rem); + } + + .content p { + font-size: 0.92rem; + } + + .primaryAction, + .secondaryAction { + min-height: 42px; + padding: 0 18px; + font-size: 0.92rem; + } +} diff --git a/txclient/src/app/components/ServicePrivate/ServicePrivate.tsx b/txclient/src/app/components/ServicePrivate/ServicePrivate.tsx index 91cd902..8927c25 100644 --- a/txclient/src/app/components/ServicePrivate/ServicePrivate.tsx +++ b/txclient/src/app/components/ServicePrivate/ServicePrivate.tsx @@ -1,13 +1,28 @@ +import Link from "next/link"; +import styles from "./ServicePrivate.module.css"; + export default function ServicePrivate() { return ( -
-

- Este servicio aun no ha sido publicado! -

-

- Este servicio no ha sido publicado. Por favor, intenta volver a - cargar la página o regresa más tarde. -

+
+
+ + +
+ Servicio no disponible +

Este servicio todavía no está publicado

+

+ El perfil público existe, pero el servicio aún no fue habilitado para recibir reservas. + Puede estar en preparación o haber sido retirado temporalmente. +

+ +
+ Ir al inicio + Explorar servicios +
+
+
); }