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.
This commit is contained in:
@@ -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<string, CollaboratorScheduleServiceSummary>;
|
||||
weeklySchedules: CollaboratorWeeklySchedule[];
|
||||
futureDisabledSchedules: ISchedulesDisabled[];
|
||||
futureOverrides: ISchedulesOverrides[];
|
||||
};
|
||||
|
||||
export interface IAdminAssistantManager {
|
||||
getCollaboratorScheduleDetails(
|
||||
data: CollaboratorScheduleDetailsParams
|
||||
): Promise<CollaboratorScheduleDetailsView>;
|
||||
}
|
||||
@@ -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<CollaboratorScheduleDetailsView> {
|
||||
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;
|
||||
@@ -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<CollaboratorSchedulesView>;
|
||||
delete(data: DeleteScheduleParams): Promise<void>;
|
||||
isAvailable(data: AvailableSchedulesParams): Promise<boolean>;
|
||||
applyScheduleChanges(data: ScheduleApplyParams): Promise<ScheduleApplyResult>;
|
||||
deleteSchedulesByCompany(data: DeleteScheulesByCompanyParams): Promise<void>;
|
||||
deleteSchedulesByEmployee(data: DeleteSchedulesByEmployeeParams): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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<ScheduleApplyResult> {
|
||||
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;
|
||||
|
||||
@@ -32,6 +32,12 @@ export class SchedulesDisabledAdapterMongoose implements ISchedulesDisabledAdapt
|
||||
await this.schedulesDisabledList.deleteOne({ _id: id }).exec();
|
||||
}
|
||||
|
||||
public async find(
|
||||
filters: Omit<FindSchedulesDisabledParams, "sessionUser">
|
||||
): Promise<ISchedulesDisabledDocument[]> {
|
||||
return this.schedulesDisabledList.find(filters).exec();
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
filters: Omit<FindSchedulesDisabledParams, "sessionUser">
|
||||
): Promise<ISchedulesDisabledDocument | null> {
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface SchedulesDisabledByCollaboratorView {
|
||||
export interface ISchedulesDisabledAdapter {
|
||||
create(data: CreateSchedulesDisabledParams): Promise<ISchedulesDisabled>;
|
||||
delete(id: string): Promise<void>;
|
||||
find(filters: Omit<FindSchedulesDisabledParams, "sessionUser">): Promise<ISchedulesDisabled[]>;
|
||||
findOne(filters: FindSchedulesDisabledParams): Promise<ISchedulesDisabled | null>;
|
||||
}
|
||||
|
||||
@@ -98,6 +99,7 @@ export interface ISchedulesDisabledManager {
|
||||
createSchedulesDisabled(data: CreateSchedulesDisabledParams): Promise<ISchedulesDisabled>;
|
||||
disableSchedule(data: DisableScheduleParams): Promise<void>;
|
||||
enableSchedule(data: DisableScheduleParams): Promise<void>;
|
||||
find(data: FindSchedulesDisabledParams): Promise<ISchedulesDisabled[]>;
|
||||
findOne(data: FindSchedulesDisabledParams): Promise<ISchedulesDisabled | null>;
|
||||
findSchedulesDisabledByCollaborator(
|
||||
data: FindSchedulesDisabledParams
|
||||
|
||||
@@ -271,6 +271,17 @@ class SchedulesDisabledManager implements ISchedulesDisabledManager {
|
||||
return await this.schedulesDisabled.findOne(data);
|
||||
}
|
||||
|
||||
public async find(data: FindSchedulesDisabledParams): Promise<ISchedulesDisabled[]> {
|
||||
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<void> {
|
||||
await validateSessionUser({ sessionUser: data.sessionUser });
|
||||
|
||||
|
||||
@@ -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<ApiValidationError>(500, "Ha ocurrido un error")
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post()
|
||||
public async getCollaboratorScheduleDetails(
|
||||
@Body() requestBody: CollaboratorScheduleDetailsParams
|
||||
): Promise<CollaboratorScheduleDetailsView | ApiValidationError> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CollaboratorScheduleDetailsView> {
|
||||
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||
|
||||
return await AdminAssistantList.getCollaboratorScheduleDetails(data);
|
||||
}
|
||||
}
|
||||
@@ -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<ApiValidationError>(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<ApiValidationError>(500, "Ha ocurrido un error")
|
||||
@SuccessResponse(200, "Done")
|
||||
@Post()
|
||||
public async applyScheduleChanges(
|
||||
@Body() requestBody: ScheduleApplyParams
|
||||
): Promise<ScheduleApplyResult | ApiValidationError> {
|
||||
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 {
|
||||
|
||||
@@ -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<ScheduleApplyResult> {
|
||||
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||
|
||||
return await ScheduleList.applyScheduleChanges(data);
|
||||
}
|
||||
|
||||
public async deleteSchedule(data: DeleteScheduleParams): Promise<void> {
|
||||
await connect(`${process.env.DATABASE_CONNECTION}`);
|
||||
await ScheduleList.delete(data);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -9,6 +9,7 @@ import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
|
||||
import EventBusyIcon from '@mui/icons-material/EventBusy';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
||||
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import EventRepeatIcon from "@mui/icons-material/EventRepeat";
|
||||
@@ -20,6 +21,8 @@ import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
|
||||
import QrCode2Icon from '@mui/icons-material/QrCode2';
|
||||
import ReceiptLongIcon from '@mui/icons-material/ReceiptLong';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import PublicOffIcon from '@mui/icons-material/PublicOff';
|
||||
import API from "@services/Api.Service";
|
||||
import { ReservationPeriodsSummary } from "@models/SchedulesEnabled.type";
|
||||
|
||||
@@ -134,6 +137,24 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
|
||||
keywords: ["nuevo servicio", "prestacion", "ofrecer", "crear servicio", "precio", "duracion"],
|
||||
icon: AddCircleOutlineIcon
|
||||
},
|
||||
{
|
||||
id: "publish-services",
|
||||
flowId: "publish-services",
|
||||
title: "Publicar Servicios",
|
||||
description: "Hacer visibles uno o más servicios en la página pública.",
|
||||
category: "Servicios",
|
||||
keywords: ["publicar", "servicios", "visible", "publico", "mostrar", "habilitar reservas"],
|
||||
icon: PublicIcon
|
||||
},
|
||||
{
|
||||
id: "unpublish-services",
|
||||
flowId: "unpublish-services",
|
||||
title: "Retirar Servicios",
|
||||
description: "Quitar uno o más servicios de la página pública.",
|
||||
category: "Servicios",
|
||||
keywords: ["despublicar", "ocultar", "servicios", "privado", "no mostrar", "quitar publicacion"],
|
||||
icon: PublicOffIcon
|
||||
},
|
||||
{
|
||||
id: "update-schedule",
|
||||
flowId: "update-schedule",
|
||||
@@ -178,6 +199,15 @@ const AVAILABLE_FLOWS: FlowDefinition[] = [
|
||||
category: "Colaboradores",
|
||||
keywords: ["invitar", "colaborador", "profesional", "empleado", "staff", "equipo"],
|
||||
icon: GroupAddIcon
|
||||
},
|
||||
{
|
||||
id: "collaborator-schedule-summary",
|
||||
flowId: "collaborator-schedule-summary",
|
||||
title: "Ver horarios de colaborador",
|
||||
description: "Consultá horarios, excepciones y restricciones configuradas.",
|
||||
category: "Colaboradores",
|
||||
keywords: ["horarios", "colaborador", "agenda", "restricciones", "excepciones", "cerrada", "consulta"],
|
||||
icon: ManageSearchIcon
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -72,6 +72,19 @@
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(128, 0, 32, 0.3);
|
||||
}
|
||||
|
||||
.nextButton:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.nextButton:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pressEnterText {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function QuestionCard({
|
||||
<div className={classNames(style.cardContainer, style.animateEnter)}>
|
||||
<div className={style.contentWrapper}>
|
||||
{topAccessory && <div className={style.topAccessory}>{topAccessory}</div>}
|
||||
<h1 className={style.title}>{title}</h1>
|
||||
{title && <h1 className={style.title}>{title}</h1>}
|
||||
{description && <p className={style.description}>{description}</p>}
|
||||
|
||||
<div className={style.inputArea}>
|
||||
|
||||
@@ -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<number>(1);
|
||||
const [serviceImage, setServiceImage] = useState<File | null>(null);
|
||||
const [createdServiceId, setCreatedServiceId] = useState<string | null>(null);
|
||||
const [serviceCreateError, setServiceCreateError] = useState<string>("");
|
||||
const [serviceAssignmentError, setServiceAssignmentError] = useState<string>("");
|
||||
const [serviceAssignmentMode, setServiceAssignmentMode] = useState<"options" | "collaborators">("options");
|
||||
const [serviceAssignmentWarning, setServiceAssignmentWarning] = useState<string>("");
|
||||
const [isLoadingServiceCollaborators, setIsLoadingServiceCollaborators] = useState(false);
|
||||
const [selectedServiceCollaboratorIds, setSelectedServiceCollaboratorIds] = useState<string[]>([]);
|
||||
const [employeeId, setEmployeeId] = useState<string | null>(null);
|
||||
const [doesProvideServices, setDoesProvideServices] = useState<boolean | null>(null);
|
||||
|
||||
// State for Schedule Flow
|
||||
const [workingDaysMode, setWorkingDaysMode] = useState<string>(""); // "mon-fri", "mon-sat", "mon-sun", "custom"
|
||||
const [specificWeekDay, setSpecificWeekDay] = useState<number | null>(null);
|
||||
const [openTime, setOpenTime] = useState<string>("09:00");
|
||||
const [closeTime, setCloseTime] = useState<string>("18:00");
|
||||
const [hasBreak, setHasBreak] = useState<boolean | null>(null);
|
||||
@@ -116,7 +128,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
const [isSuccess, setIsSuccess] = useState<boolean>(false);
|
||||
const [isAddingAnotherService, setIsAddingAnotherService] = useState<boolean>(false);
|
||||
|
||||
const [isResuming, setIsResuming] = useState(false);
|
||||
const [isResuming, setIsResuming] = useState(true);
|
||||
|
||||
const [headerImage, setHeaderImage] = useState<File | null>(null);
|
||||
const [headerColor, setHeaderColor] = useState<string>("#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<CompanyServiceView[]>([]);
|
||||
const [selectedPublicationServiceIds, setSelectedPublicationServiceIds] = useState<string[]>([]);
|
||||
const [publicationLoading, setPublicationLoading] = useState(false);
|
||||
const [publicationMessage, setPublicationMessage] = useState("");
|
||||
const [collaboratorScheduleDetails, setCollaboratorScheduleDetails] = useState<CollaboratorScheduleDetails | null>(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<string | null>(null);
|
||||
const [selectedScheduleEmployeeIds, setSelectedScheduleEmployeeIds] = useState<string[]>([]);
|
||||
const [allEmployees, setAllEmployees] = useState<any[]>([]);
|
||||
const [overrideDate, setOverrideDate] = useState<string>("");
|
||||
const [allOrganizations, setAllOrganizations] = useState<any[]>([]);
|
||||
@@ -163,6 +183,11 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
const [reservationPeriodSummary, setReservationPeriodSummary] = useState<ReservationPeriodsSummary | null>(null);
|
||||
const [reservationPeriodEmployeesLoading, setReservationPeriodEmployeesLoading] = useState(false);
|
||||
const [reservationPeriodEmployeesError, setReservationPeriodEmployeesError] = useState<string | null>(null);
|
||||
const [scheduleServiceScope, setScheduleServiceScope] = useState<"all" | "specific">("all");
|
||||
const [scheduleServiceIds, setScheduleServiceIds] = useState<string[]>([]);
|
||||
const [scheduleAvailableServices, setScheduleAvailableServices] = useState<any[]>([]);
|
||||
const [scheduleConflictMessage, setScheduleConflictMessage] = useState<string>("");
|
||||
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<CompanyServiceView[]>("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<any[]>("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<any[]>("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<any[]>("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<any>("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<any[]>("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<any>("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<any>("employees/update-collaborator-services", {
|
||||
companyId: createdCompanyId,
|
||||
employeeId: finalEmployeeId,
|
||||
services: [{ serviceId: createdServiceId, active: true }],
|
||||
sessionUser: SessionInfo.userId
|
||||
});
|
||||
|
||||
const schedulesRes = await API.post<any>("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<any[]>("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<any>("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<any>("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<any>("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<any>("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<any>("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 = () => (
|
||||
<UpdateScheduleFlow
|
||||
const renderUpdateScheduleFlow = () => {
|
||||
const scheduleTargetEmployees = getUpdateScheduleTargetEmployees();
|
||||
const scheduleTargetEmployeeIds = scheduleTargetEmployees.map((employee) => String(getEmployeeId(employee))).filter(Boolean);
|
||||
const canUseSpecificScheduleServices = scheduleTargetEmployeeIds.length > 0;
|
||||
|
||||
return <UpdateScheduleFlow
|
||||
currentStepIndex={currentStepIndex}
|
||||
scheduleScope={scheduleScope}
|
||||
setScheduleScope={setScheduleScope}
|
||||
setScheduleScope={handleSetUpdateScheduleScope}
|
||||
targetEmployeeId={targetEmployeeId}
|
||||
setTargetEmployeeId={setTargetEmployeeId}
|
||||
selectedScheduleEmployeeIds={selectedScheduleEmployeeIds}
|
||||
handleToggleScheduleEmployee={handleToggleScheduleEmployee}
|
||||
allEmployees={allEmployees}
|
||||
handleNext={handleNext}
|
||||
workingDaysMode={workingDaysMode}
|
||||
setWorkingDaysMode={setWorkingDaysMode}
|
||||
specificWeekDay={specificWeekDay}
|
||||
setSpecificWeekDay={setSpecificWeekDay}
|
||||
openTime={openTime}
|
||||
setOpenTime={setOpenTime}
|
||||
closeTime={closeTime}
|
||||
@@ -1384,8 +1762,20 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
setBreakEnd={setBreakEnd}
|
||||
isCreating={isCreating}
|
||||
handleUpdateScheduleSubmit={handleUpdateScheduleSubmit}
|
||||
/>
|
||||
);
|
||||
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<any>("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 = () => (
|
||||
<PublicVisibilityFlow
|
||||
isSelectedOrganizationPaidPlan={isSelectedOrganizationPaidPlan}
|
||||
@@ -1768,6 +2197,59 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
/>
|
||||
);
|
||||
|
||||
const renderServicePublicationFlow = () => (
|
||||
<ServicePublicationFlow
|
||||
mode={flowId === "publish-services" ? "publish" : "unpublish"}
|
||||
currentStepIndex={currentStepIndex}
|
||||
services={publicationServices}
|
||||
selectedServiceIds={selectedPublicationServiceIds}
|
||||
toggleService={togglePublicationService}
|
||||
handleSave={handleSaveServicePublication}
|
||||
isCreating={isCreating}
|
||||
isLoading={publicationLoading}
|
||||
message={publicationMessage}
|
||||
selectedOrganization={selectedOrganization}
|
||||
orgName={orgName}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleLoadCollaboratorScheduleDetails = async () => {
|
||||
if (!createdCompanyId || !targetEmployeeId || collaboratorScheduleLoading) return;
|
||||
|
||||
setCollaboratorScheduleLoading(true);
|
||||
setCollaboratorScheduleMessage("");
|
||||
try {
|
||||
const details = await API.post<CollaboratorScheduleDetails>("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 = () => (
|
||||
<CollaboratorScheduleSummaryFlow
|
||||
currentStepIndex={currentStepIndex}
|
||||
employees={allEmployees}
|
||||
selectedEmployeeId={targetEmployeeId}
|
||||
setSelectedEmployeeId={setTargetEmployeeId}
|
||||
details={collaboratorScheduleDetails}
|
||||
loading={collaboratorScheduleLoading}
|
||||
message={collaboratorScheduleMessage}
|
||||
selectedOrganization={selectedOrganization}
|
||||
orgName={orgName}
|
||||
loadDetails={handleLoadCollaboratorScheduleDetails}
|
||||
goToAssistantHome={() => 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 (
|
||||
<QuestionCard
|
||||
isActive={true}
|
||||
title="Necesitás una organización"
|
||||
description="Para continuar con este flujo primero tenés que crear o tener asociada una organización. Sin una organización no podemos aplicar esta configuración."
|
||||
onNext={() => router.push("/admin/assistant")}
|
||||
nextLabel="Volver al inicio del asistente"
|
||||
>
|
||||
<div style={{ color: "rgba(255,255,255,0.75)", textAlign: "center", lineHeight: 1.6 }}>
|
||||
Creá una organización desde el inicio del asistente y después volvé a intentar esta acción.
|
||||
</div>
|
||||
</QuestionCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (allOrganizations.length > 1 && !createdCompanyId && action !== "new-org") {
|
||||
return <OrganizationPicker allOrganizations={allOrganizations} loadOrganizationContext={loadOrganizationContext} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={style.engineContainer}>
|
||||
<div className={style.header}>
|
||||
@@ -1960,7 +2462,7 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
||||
<span>Volver</span>
|
||||
</div>
|
||||
<div className={style.flowTitle}>
|
||||
Configuración de Negocio
|
||||
{!shouldHideFlowTitle && "Configuración de Negocio"}
|
||||
</div>
|
||||
<div className={style.progressIndicator}>
|
||||
{/* Placeholder for progress dots or percentage */}
|
||||
|
||||
+62
@@ -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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%' }}>
|
||||
{employees.map((employee: any) => {
|
||||
const employeeId = getEmployeeId(employee);
|
||||
const isSelected = multiple ? selectedIds.includes(employeeId) : selectedId === employeeId;
|
||||
const label = getCollaboratorLabel(employee);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={employeeId}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '12px 14px',
|
||||
borderRadius: '12px',
|
||||
border: `1px solid ${isSelected ? 'var(--wine-red)' : 'rgba(255,255,255,0.16)'}`,
|
||||
background: isSelected ? 'rgba(128, 0, 64, 0.3)' : 'rgba(255,255,255,0.05)',
|
||||
color: 'white',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type={multiple ? "checkbox" : "radio"}
|
||||
checked={isSelected}
|
||||
disabled={disabled}
|
||||
onChange={() => {
|
||||
if (disabled) return;
|
||||
if (multiple) {
|
||||
onToggle?.(employee);
|
||||
} else if (employeeId) {
|
||||
onSelect?.(employeeId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '15px' }}>{label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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.";
|
||||
|
||||
+234
@@ -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<string, { id: string; name: string }>;
|
||||
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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(prev => !prev)}
|
||||
style={{ ...cardStyle, width: "100%", textAlign: "left", cursor: "pointer" }}
|
||||
>
|
||||
<span style={{ display: "flex", justifyContent: "space-between", gap: "12px", alignItems: "center" }}>
|
||||
<span style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
<strong>{title}</strong>
|
||||
<span style={mutedStyle}>{subtitle}</span>
|
||||
</span>
|
||||
<span style={{ color: "rgba(255,255,255,0.72)", fontSize: "13px" }}>{open ? "Ocultar" : "Ver detalle"}</span>
|
||||
</span>
|
||||
{open && (
|
||||
<span style={{ display: "block", marginTop: "12px", paddingTop: "12px", borderTop: "1px solid rgba(255,255,255,0.1)" }}>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<QuestionCard
|
||||
isActive={true}
|
||||
title="Elegí el colaborador"
|
||||
description="Seleccioná a quién querés consultar. Esta acción es sólo de lectura."
|
||||
onNext={loadDetails}
|
||||
disableNext={!selectedEmployeeId || loading || employees.length === 0}
|
||||
nextLabel={loading ? "Consultando..." : "Ver horarios"}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "14px", width: "100%" }}>
|
||||
<div style={{ ...mutedStyle, textAlign: "center" }}>
|
||||
Organización: <strong style={{ color: "white" }}>{selectedOrganization?.name || orgName || "seleccionada"}</strong>
|
||||
</div>
|
||||
{message && <div style={cardStyle}>{message}</div>}
|
||||
{employees.length === 0 ? (
|
||||
<div style={{ ...cardStyle, ...mutedStyle, textAlign: "center" }}>No encontramos colaboradores activos en esta organización.</div>
|
||||
) : (
|
||||
<CollaboratorPicker employees={employees} selectedId={selectedEmployeeId} onSelect={setSelectedEmployeeId} disabled={loading} />
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QuestionCard
|
||||
isActive={true}
|
||||
title=""
|
||||
onNext={goToAssistantHome}
|
||||
nextLabel="Volver al asistente"
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px", width: "100%", paddingTop: "150px" }}>
|
||||
{message && <div style={cardStyle}>{message}</div>}
|
||||
{!details ? (
|
||||
<div style={{ ...cardStyle, ...mutedStyle, textAlign: "center" }}>No hay datos para mostrar.</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ ...cardStyle, display: "flex", alignItems: "center", justifyContent: "center", gap: "12px", textAlign: "left", padding: "18px 14px", marginTop: "4px" }}>
|
||||
<img
|
||||
src={details.collaborator.avatar}
|
||||
alt={details.collaborator.fullName}
|
||||
style={{ width: "46px", height: "46px", borderRadius: "50%", objectFit: "cover", border: "2px solid rgba(255,255,255,0.18)", flex: "0 0 auto" }}
|
||||
/>
|
||||
<div>
|
||||
<strong style={{ display: "block", fontSize: "18px", lineHeight: 1.35 }}>{details.collaborator.fullName}</strong>
|
||||
<div style={mutedStyle}>{details.collaborator.email || "Sin email registrado"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<h3 style={{ margin: 0, fontSize: "16px", color: "white" }}>Horarios semanales</h3>
|
||||
{configuredWeeklySchedules.length === 0 ? (
|
||||
<div style={{ ...cardStyle, ...mutedStyle }}>Este colaborador no tiene horarios semanales configurados.</div>
|
||||
) : configuredWeeklySchedules.map(day => (
|
||||
<div key={day.scheduleId || day.weekDay} style={cardStyle}>
|
||||
<strong>{weekDays[day.weekDay] || `Día ${day.weekDay}`}</strong>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px", marginTop: "8px" }}>
|
||||
{day.schedules.map((schedule, index) => (
|
||||
<span key={`${day.weekDay}-${index}`} style={mutedStyle}>{formatSchedule(schedule, details.servicesById)}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<h3 style={{ margin: 0, fontSize: "16px", color: "white" }}>Excepciones futuras</h3>
|
||||
{details.futureOverrides.length === 0 ? (
|
||||
<div style={{ ...cardStyle, ...mutedStyle }}>No hay excepciones futuras configuradas.</div>
|
||||
) : details.futureOverrides.map((override, index) => (
|
||||
<ExpandableItem key={override.id || override._id || index} title={formatDate(override.date)} subtitle={`${override.schedules.length} bloque(s) configurado(s)`}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
||||
{override.schedules.map((schedule, scheduleIndex) => (
|
||||
<span key={scheduleIndex} style={mutedStyle}>{formatSchedule(schedule, details.servicesById)}</span>
|
||||
))}
|
||||
</div>
|
||||
</ExpandableItem>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
<h3 style={{ margin: 0, fontSize: "16px", color: "white" }}>Restricciones / agenda cerrada</h3>
|
||||
{details.futureDisabledSchedules.length === 0 ? (
|
||||
<div style={{ ...cardStyle, ...mutedStyle }}>No hay cierres de agenda futuros configurados.</div>
|
||||
) : details.futureDisabledSchedules.map((disabled, index) => (
|
||||
<ExpandableItem
|
||||
key={disabled.id || disabled._id || index}
|
||||
title={`${formatDate(disabled.startDate)} al ${formatDate(disabled.endDate)}`}
|
||||
subtitle="Agenda cerrada para este colaborador"
|
||||
>
|
||||
<div style={mutedStyle}>Durante este período no se muestran turnos disponibles para el colaborador.</div>
|
||||
</ExpandableItem>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
);
|
||||
}
|
||||
+242
-12
@@ -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 (
|
||||
<>
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
@@ -75,7 +110,7 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
|
||||
title="2. Duración del Servicio"
|
||||
description={`Elige cuánto tiempo tomará. Las opciones están basadas en tu fracción de agenda (${fraction} min).`}
|
||||
onNext={handleNext}
|
||||
disableNext={!serviceLength}
|
||||
disableNext={!serviceLength || (isCustomDuration && !isValidCustomDuration)}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap', width: '100%', justifyContent: 'center' }}>
|
||||
{durationOptions.map(val => {
|
||||
@@ -88,15 +123,15 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
|
||||
return (
|
||||
<div
|
||||
key={val}
|
||||
onClick={() => 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)
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
onClick={selectCustomDuration}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
background: isCustomDuration ? 'var(--wine-red)' : 'rgba(255,255,255,0.05)',
|
||||
border: `2px solid ${isCustomDuration ? 'var(--wine-red)' : 'rgba(255,255,255,0.1)'}`,
|
||||
color: 'white',
|
||||
fontWeight: isCustomDuration ? 'bold' : 'normal',
|
||||
transition: 'all 0.2s ease',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
Otro
|
||||
</div>
|
||||
</div>
|
||||
{isCustomDuration && (
|
||||
<div style={{ width: '100%', marginTop: '16px' }}>
|
||||
<QuestionInput
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
placeholder="Duración en minutos"
|
||||
value={customDurationValue}
|
||||
onChange={(e) => updateCustomDuration(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (isValidCustomDuration) {
|
||||
handleKeyDown(e, handleNext);
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '13px', marginTop: '8px', textAlign: 'center' }}>
|
||||
Ingresá la duración total del servicio expresada en minutos.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
@@ -113,7 +185,7 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
|
||||
title="3. Precio y Cupos"
|
||||
description="¿Cuánto cuesta y a cuántas personas puedes atender a la vez en este servicio?"
|
||||
onNext={handleCreateService}
|
||||
disableNext={isCreating || !servicePrice}
|
||||
disableNext={isCreating}
|
||||
nextLabel={isCreating ? "Guardando..." : "Crear Servicio"}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', width: '100%' }}>
|
||||
@@ -122,7 +194,8 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
|
||||
<div style={{ position: 'relative' }}>
|
||||
<span style={{ position: 'absolute', left: '15px', top: '50%', transform: 'translateY(-50%)', color: 'white', fontSize: '18px' }}>$</span>
|
||||
<input
|
||||
type="number"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
placeholder="0.00"
|
||||
value={servicePrice}
|
||||
onChange={(e) => setServicePrice(e.target.value)}
|
||||
@@ -163,6 +236,20 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{serviceCreateError && (
|
||||
<div style={{
|
||||
padding: '14px 16px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255, 99, 132, 0.12)',
|
||||
border: '1px solid rgba(255, 99, 132, 0.35)',
|
||||
color: '#fecdd3',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.5,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{serviceCreateError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
@@ -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")}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', width: '100%', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
@@ -216,8 +303,151 @@ export default function OnboardingServiceFlow(props: OnboardingServiceFlowProps)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{serviceCreateError && (
|
||||
<div style={{
|
||||
padding: '14px 16px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255, 99, 132, 0.12)',
|
||||
border: '1px solid rgba(255, 99, 132, 0.35)',
|
||||
color: '#fecdd3',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.5,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{serviceCreateError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
{isStandaloneNewService && (
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 5}
|
||||
title="¿Quién brinda este servicio?"
|
||||
description="Asigná este servicio a un colaborador para que pueda recibir reservas. Si todavía no querés hacerlo, podés configurarlo después."
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px', width: '100%' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAssignCreatedServiceToMe}
|
||||
disabled={isCreating}
|
||||
style={{
|
||||
padding: '14px 18px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--wine-red)',
|
||||
background: 'var(--wine-red)',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
cursor: isCreating ? 'not-allowed' : 'pointer',
|
||||
opacity: isCreating ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{isCreating ? "Asignando..." : "Yo"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShowCollaboratorAssignment}
|
||||
disabled={isCreating || isLoadingServiceCollaborators}
|
||||
style={{
|
||||
padding: '14px 18px',
|
||||
borderRadius: '12px',
|
||||
border: serviceAssignmentMode === "collaborators" ? '1px solid var(--wine-red)' : '1px solid rgba(255,255,255,0.2)',
|
||||
background: serviceAssignmentMode === "collaborators" ? 'rgba(128, 0, 64, 0.35)' : 'rgba(255,255,255,0.05)',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
cursor: isCreating || isLoadingServiceCollaborators ? 'not-allowed' : 'pointer',
|
||||
opacity: isCreating || isLoadingServiceCollaborators ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{isLoadingServiceCollaborators ? "Cargando colaboradores..." : "Uno o más colaboradores"}
|
||||
</button>
|
||||
{serviceAssignmentMode === "collaborators" && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%' }}>
|
||||
{serviceAssignmentCollaborators.length === 0 && !isLoadingServiceCollaborators && (
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', textAlign: 'center' }}>
|
||||
No encontramos colaboradores activos para esta organización.
|
||||
</div>
|
||||
)}
|
||||
<CollaboratorPicker
|
||||
employees={serviceAssignmentCollaborators}
|
||||
selectedIds={selectedServiceCollaboratorIds}
|
||||
multiple
|
||||
disabled={isCreating}
|
||||
onToggle={handleToggleServiceCollaborator}
|
||||
/>
|
||||
{serviceAssignmentCollaborators.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAssignCreatedServiceToCollaborators}
|
||||
disabled={isCreating || selectedServiceCollaboratorIds.length === 0}
|
||||
style={{
|
||||
padding: '14px 18px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--wine-red)',
|
||||
background: 'var(--wine-red)',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
cursor: isCreating || selectedServiceCollaboratorIds.length === 0 ? 'not-allowed' : 'pointer',
|
||||
opacity: isCreating || selectedServiceCollaboratorIds.length === 0 ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{isCreating ? "Asignando..." : "Asignar servicio"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!serviceAssignmentWarning && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFinishWithoutAssignment}
|
||||
disabled={isCreating}
|
||||
style={{
|
||||
padding: '14px 18px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
cursor: isCreating ? 'not-allowed' : 'pointer',
|
||||
opacity: isCreating ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
Lo configuro después
|
||||
</button>
|
||||
<div style={{
|
||||
padding: '12px 14px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(245, 158, 11, 0.12)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.35)',
|
||||
color: '#fde68a',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.5,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
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.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{serviceAssignmentError && (
|
||||
<div style={{
|
||||
padding: '12px 14px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255, 99, 132, 0.12)',
|
||||
border: '1px solid rgba(255, 99, 132, 0.35)',
|
||||
color: '#fecdd3',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.5,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{serviceAssignmentError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
+99
@@ -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 (
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
title={title}
|
||||
description={description}
|
||||
onNext={handleSave}
|
||||
disableNext={isLoading || isCreating || selectedServiceIds.length === 0 || services.length === 0}
|
||||
nextLabel={isLoading ? "Cargando servicios..." : nextLabel}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "14px", width: "100%" }}>
|
||||
<div style={{ color: "rgba(255,255,255,0.72)", fontSize: "14px", textAlign: "center" }}>
|
||||
Organización: <strong style={{ color: "white" }}>{selectedOrganization?.name || orgName || "seleccionada"}</strong>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div style={{ padding: "12px 14px", borderRadius: "12px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.82)", textAlign: "center", lineHeight: 1.4 }}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && services.length === 0 && (
|
||||
<div style={{ padding: "18px", borderRadius: "14px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.1)", color: "rgba(255,255,255,0.75)", textAlign: "center", lineHeight: 1.5 }}>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{services.map(service => {
|
||||
const selected = selectedServiceIds.includes(service.id);
|
||||
return (
|
||||
<button
|
||||
key={service.id}
|
||||
type="button"
|
||||
onClick={() => toggleService(service.id)}
|
||||
disabled={isCreating}
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "14px",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
borderRadius: "14px",
|
||||
border: `2px solid ${selected ? "var(--wine-red)" : "rgba(255,255,255,0.1)"}`,
|
||||
background: selected ? "rgba(255,42,127,0.18)" : "rgba(255,255,255,0.05)",
|
||||
color: "white",
|
||||
cursor: isCreating ? "not-allowed" : "pointer",
|
||||
textAlign: "left",
|
||||
transition: "all 0.2s ease"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getServiceImage(service.image)}
|
||||
alt={service.name}
|
||||
style={{ width: "62px", height: "62px", objectFit: "cover", borderRadius: "12px", background: "rgba(255,255,255,0.08)", flex: "0 0 auto" }}
|
||||
/>
|
||||
<span style={{ display: "flex", flexDirection: "column", gap: "4px", flex: 1 }}>
|
||||
<span style={{ fontWeight: 800, fontSize: "15px" }}>{service.name}</span>
|
||||
<span style={{ color: "rgba(255,255,255,0.68)", fontSize: "13px", lineHeight: 1.35 }}>{service.description || "Sin descripción"}</span>
|
||||
</span>
|
||||
<span style={{ width: "22px", height: "22px", borderRadius: "50%", border: `2px solid ${selected ? "var(--wine-red)" : "rgba(255,255,255,0.35)"}`, background: selected ? "var(--wine-red)" : "transparent", flex: "0 0 auto" }} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
);
|
||||
}
|
||||
+231
-224
@@ -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 (
|
||||
<>
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
title="Actualizar Horarios"
|
||||
description="¿A quiénes quieres aplicar esta nueva rutina de horarios?"
|
||||
onNext={() => {
|
||||
if (scheduleScope === "specific" && !targetEmployeeId) {
|
||||
alert("Por favor selecciona un colaborador.");
|
||||
return;
|
||||
}
|
||||
handleNext();
|
||||
}}
|
||||
disableNext={!scheduleScope || (scheduleScope === "specific" && !targetEmployeeId)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%' }}>
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 5px 0' }}>Solo a mí</h4>
|
||||
<p style={{ margin: 0, fontSize: '12px', opacity: 0.7 }}>Aplica esta rutina únicamente a tus horarios.</p>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 5px 0' }}>A un colaborador en particular</h4>
|
||||
<p style={{ margin: 0, fontSize: '12px', opacity: 0.7 }}>Aplica esta rutina a un integrante específico de tu equipo.</p>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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'
|
||||
}}
|
||||
>
|
||||
<h4 style={{ margin: '0 0 5px 0' }}>A todos mis colaboradores</h4>
|
||||
<p style={{ margin: 0, fontSize: '12px', opacity: 0.7 }}>Sobrescribe masivamente los horarios de todo tu equipo.</p>
|
||||
</div>
|
||||
|
||||
{scheduleScope === "specific" && (
|
||||
<div style={{ marginTop: '15px', width: '100%' }}>
|
||||
<label style={{ color: 'white', display: 'block', marginBottom: '8px', fontSize: '14px' }}>Selecciona el colaborador:</label>
|
||||
<select
|
||||
value={targetEmployeeId || ""}
|
||||
onChange={(e) => setTargetEmployeeId(e.target.value)}
|
||||
style={{
|
||||
width: '100%', padding: '12px', borderRadius: '8px',
|
||||
background: 'rgba(255,255,255,0.1)', color: 'white', border: '1px solid rgba(255,255,255,0.2)',
|
||||
outline: 'none', fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
<option value="" disabled style={{ color: 'black' }}>-- Seleccionar --</option>
|
||||
{allEmployees.map((emp: any) => (
|
||||
<option key={emp.id || emp._id || emp.employeeId} value={emp.id || emp._id || emp.employeeId} style={{ color: 'black' }}>
|
||||
{emp.firstName} {emp.lastName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ background: "rgba(255, 193, 7, 0.14)", border: "1px solid rgba(255, 193, 7, 0.45)", borderRadius: "12px", padding: "12px", color: "#fff4cf", whiteSpace: "pre-line" }}>
|
||||
{scheduleConflictMessage}
|
||||
</div>
|
||||
{pendingScheduleOverwrite && (
|
||||
<div style={{ display: "flex", gap: "10px", justifyContent: "center" }}>
|
||||
<button type="button" onClick={() => { setScheduleConflictMessage(""); setPendingScheduleOverwrite(false); }} style={{ padding: "10px 14px", borderRadius: "10px", border: "1px solid rgba(255,255,255,0.25)", background: "transparent", color: "white" }}>Cancelar / Editar</button>
|
||||
<button type="button" onClick={() => handleUpdateScheduleSubmit("overwrite-conflicts")} style={{ padding: "10px 14px", borderRadius: "10px", border: "0", background: "var(--wine-red)", color: "white" }}>Sobrescribir conflictos</button>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 1}
|
||||
title="Días de Trabajo"
|
||||
description="¿Qué días aplicará esta rutina?"
|
||||
onNext={handleNext}
|
||||
disableNext={!workingDaysMode}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%' }}>
|
||||
{[
|
||||
{ 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 => (
|
||||
<div
|
||||
key={mode.id}
|
||||
onClick={() => 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}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 2}
|
||||
title="Horario Comercial"
|
||||
description="¿A qué hora empieza y termina la jornada laboral?"
|
||||
onNext={handleNext}
|
||||
disableNext={!openTime || !closeTime}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '20px', width: '100%', justifyContent: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<label style={{ color: 'white', fontSize: '12px', opacity: 0.7 }}>Apertura</label>
|
||||
<input
|
||||
type="time"
|
||||
value={openTime}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<label style={{ color: 'white', fontSize: '12px', opacity: 0.7 }}>Cierre</label>
|
||||
<input
|
||||
type="time"
|
||||
value={closeTime}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 3}
|
||||
title="Descansos"
|
||||
description="¿Tienen un horario de corte al mediodía o descanso?"
|
||||
onNext={() => {
|
||||
if (hasBreak) {
|
||||
handleNext();
|
||||
} else {
|
||||
handleUpdateScheduleSubmit();
|
||||
}
|
||||
}}
|
||||
disableNext={hasBreak === null}
|
||||
nextLabel={hasBreak ? "Siguiente" : (isCreating ? "Guardando..." : "Guardar Horarios")}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '15px', width: '100%', justifyContent: 'center' }}>
|
||||
<div
|
||||
onClick={() => 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
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 4}
|
||||
title="Horario de Descanso"
|
||||
description="¿De qué hora a qué hora no estarán disponibles?"
|
||||
onNext={handleUpdateScheduleSubmit}
|
||||
disableNext={!breakStart || !breakEnd || isCreating}
|
||||
nextLabel={isCreating ? "Guardando..." : "Guardar Horarios"}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '20px', width: '100%', justifyContent: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<label style={{ color: 'white', fontSize: '12px', opacity: 0.7 }}>Inicio descanso</label>
|
||||
<input
|
||||
type="time"
|
||||
value={breakStart}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<label style={{ color: 'white', fontSize: '12px', opacity: 0.7 }}>Fin descanso</label>
|
||||
<input
|
||||
type="time"
|
||||
value={breakEnd}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuestionCard
|
||||
isActive={currentStepIndex === 0}
|
||||
title="Actualizar Horarios"
|
||||
description="¿A quiénes querés aplicar esta nueva rutina de horarios?"
|
||||
onNext={handleNext}
|
||||
disableNext={!scheduleScope || (scheduleScope === "specific" && selectedScheduleEmployeeIds.length === 0)}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
|
||||
<div onClick={() => setScheduleScope("me")} style={optionStyle(scheduleScope === "me")}>
|
||||
<h4 style={{ margin: "0 0 5px 0" }}>Solo a mí</h4>
|
||||
<p style={{ margin: 0, fontSize: "12px", opacity: 0.7 }}>Aplica esta rutina únicamente a tus horarios.</p>
|
||||
</div>
|
||||
<div onClick={() => setScheduleScope("specific")} style={optionStyle(scheduleScope === "specific")}>
|
||||
<h4 style={{ margin: "0 0 5px 0" }}>A uno o más colaboradores</h4>
|
||||
<p style={{ margin: 0, fontSize: "12px", opacity: 0.7 }}>Aplica esta rutina a uno o varios integrantes específicos de tu equipo.</p>
|
||||
</div>
|
||||
<div onClick={() => setScheduleScope("all")} style={optionStyle(scheduleScope === "all")}>
|
||||
<h4 style={{ margin: "0 0 5px 0" }}>A todos mis colaboradores</h4>
|
||||
<p style={{ margin: 0, fontSize: "12px", opacity: 0.7 }}>Actualiza masivamente los horarios de todo tu equipo.</p>
|
||||
</div>
|
||||
|
||||
{scheduleScope === "specific" && (
|
||||
<div style={{ marginTop: "15px", width: "100%" }}>
|
||||
<label style={{ color: "white", display: "block", marginBottom: "8px", fontSize: "14px" }}>Seleccioná uno o más colaboradores:</label>
|
||||
<CollaboratorPicker employees={allEmployees} multiple selectedIds={selectedScheduleEmployeeIds} onToggle={handleToggleScheduleEmployee} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard isActive={currentStepIndex === 1} title="Días de Trabajo" description="¿Qué días aplicará esta rutina?" onNext={handleNext} disableNext={!workingDaysMode}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={mode.id} onClick={() => setWorkingDaysMode(mode.id)} style={optionStyle(workingDaysMode === mode.id)}>
|
||||
{mode.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard isActive={currentStepIndex === weekDayStep && isSpecificDay} title="Día específico" description="Elegí el día que querés configurar." onNext={handleNext} disableNext={specificWeekDay === null}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))", gap: "10px", width: "100%" }}>
|
||||
{weekDays.map((day) => (
|
||||
<div key={day.id} onClick={() => setSpecificWeekDay(day.id)} style={optionStyle(specificWeekDay === day.id)}>
|
||||
{day.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard isActive={currentStepIndex === timeStep} title="Horario Comercial" description="¿A qué hora empieza y termina la jornada laboral?" onNext={handleNext} disableNext={!openTime || !closeTime}>
|
||||
<div style={{ display: "flex", gap: "20px", width: "100%", justifyContent: "center" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||
<label style={{ color: "white", fontSize: "12px", opacity: 0.7 }}>Apertura</label>
|
||||
<input type="time" value={openTime} onChange={(e) => 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" }} />
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||
<label style={{ color: "white", fontSize: "12px", opacity: 0.7 }}>Cierre</label>
|
||||
<input type="time" value={closeTime} onChange={(e) => 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" }} />
|
||||
</div>
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard isActive={currentStepIndex === breakStep} title="Descansos" description="¿Tienen un horario de corte al mediodía o descanso?" onNext={continueAfterBreak} disableNext={hasBreak === null} nextLabel={hasBreak ? "Siguiente" : (isSpecificDay ? "Siguiente" : isCreating ? "Guardando..." : "Guardar Horarios")}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
|
||||
<div style={{ display: "flex", gap: "15px", width: "100%", justifyContent: "center" }}>
|
||||
<div onClick={() => setHasBreak(true)} style={{ ...optionStyle(hasBreak === true), flex: 1 }}>Sí, hay descanso</div>
|
||||
<div onClick={() => setHasBreak(false)} style={{ ...optionStyle(hasBreak === false), flex: 1 }}>No, horario corrido</div>
|
||||
</div>
|
||||
{renderConflictActions()}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard isActive={currentStepIndex === breakTimeStep && hasBreak === true} title="Horario de Descanso" description="¿De qué hora a qué hora no estarán disponibles?" onNext={isSpecificDay ? handleNext : handleUpdateScheduleSubmit} disableNext={!breakStart || !breakEnd || isCreating} nextLabel={isSpecificDay ? "Siguiente" : isCreating ? "Guardando..." : "Guardar Horarios"}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "12px", width: "100%" }}>
|
||||
<div style={{ display: "flex", gap: "20px", width: "100%", justifyContent: "center" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||
<label style={{ color: "white", fontSize: "12px", opacity: 0.7 }}>Inicio descanso</label>
|
||||
<input type="time" value={breakStart} onChange={(e) => 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" }} />
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
|
||||
<label style={{ color: "white", fontSize: "12px", opacity: 0.7 }}>Fin descanso</label>
|
||||
<input type="time" value={breakEnd} onChange={(e) => 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" }} />
|
||||
</div>
|
||||
</div>
|
||||
{renderConflictActions()}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
|
||||
<QuestionCard isActive={currentStepIndex === serviceScopeStep && isSpecificDay} title="Servicios del horario" description="¿Este horario aplica a todos los servicios o solo a algunos?" onNext={() => handleUpdateScheduleSubmit()} disableNext={isCreating || (scheduleServiceScope === "specific" && (scheduleServiceIds.length === 0 || hasNoCommonServices))} nextLabel={isCreating ? "Guardando..." : "Guardar Horarios"}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px", width: "100%" }}>
|
||||
{!canUseSpecificScheduleServices && (
|
||||
<p style={{ color: "#ffd8a8", margin: 0, fontSize: "14px" }}>Seleccioná al menos un colaborador para configurar servicios específicos.</p>
|
||||
)}
|
||||
<div onClick={() => handleServiceScopeSelect("all")} style={optionStyle(scheduleServiceScope === "all")}>Todos los servicios</div>
|
||||
<div onClick={() => canUseSpecificScheduleServices && handleServiceScopeSelect("specific")} style={{ ...optionStyle(scheduleServiceScope === "specific"), opacity: canUseSpecificScheduleServices ? 1 : 0.5, cursor: canUseSpecificScheduleServices ? "pointer" : "not-allowed" }}>Servicios específicos</div>
|
||||
|
||||
{scheduleServiceScope === "specific" && canUseSpecificScheduleServices && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
{hasNoCommonServices && (
|
||||
<p style={{ color: "#ffd8a8", margin: 0, fontSize: "14px" }}>Los colaboradores seleccionados no tienen servicios activos en común. Usá todos los servicios o cambiá la selección.</p>
|
||||
)}
|
||||
{scheduleAvailableServices.map((service: any) => (
|
||||
<label key={service.id || service._id} style={{ color: "white", display: "flex", gap: "10px", alignItems: "center" }}>
|
||||
<input type="checkbox" checked={scheduleServiceIds.includes(service.id || service._id)} onChange={() => toggleService(service.id || service._id)} />
|
||||
{service.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderConflictActions()}
|
||||
</div>
|
||||
</QuestionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,28 @@
|
||||
import Link from "next/link";
|
||||
import styles from "./ServicePrivate.module.css";
|
||||
|
||||
export default function ServicePrivate() {
|
||||
return (
|
||||
<div className="card error-card">
|
||||
<h2 style={{ color: "var(--wine-red)" }}>
|
||||
Este servicio aun no ha sido publicado!
|
||||
</h2>
|
||||
<p>
|
||||
Este servicio no ha sido publicado. Por favor, intenta volver a
|
||||
cargar la página o regresa más tarde.
|
||||
</p>
|
||||
<div className={styles.page}>
|
||||
<section className={styles.card} aria-labelledby="service-private-title">
|
||||
<div className={styles.illustrationPanel} aria-hidden="true">
|
||||
<img src="/error.png" alt="" className={styles.illustration} />
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<span className={styles.badge}>Servicio no disponible</span>
|
||||
<h1 id="service-private-title">Este servicio todavía no está publicado</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link href="/" className={styles.primaryAction}>Ir al inicio</Link>
|
||||
<Link href="/landing" className={styles.secondaryAction}>Explorar servicios</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user