diff --git a/server/src/Models/SchedulesEnabled/SchedulesEnabled.ts b/server/src/Models/SchedulesEnabled/SchedulesEnabled.ts index d9b0b01..92b7b14 100644 --- a/server/src/Models/SchedulesEnabled/SchedulesEnabled.ts +++ b/server/src/Models/SchedulesEnabled/SchedulesEnabled.ts @@ -25,11 +25,17 @@ import { joinStrings } from "../../helpers/String"; import getAvatar from "../../helpers/getAvatar"; import { NotificationsManager } from "../Notifications/Notifications"; import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import "dayjs/locale/es"; import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface"; import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check"; +dayjs.extend(utc); +dayjs.extend(timezone); dayjs.locale("es"); +const SCHEDULE_BOUNDARY_TIMEZONE = "America/Argentina/Buenos_Aires"; const NEAR_ENDING_THRESHOLD_DAYS = 14; const ALLOWED_RESERVATION_PERIOD_DURATIONS = [ { unit: "week", value: 1 }, @@ -87,8 +93,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const schedulesEnabledCheck = await this.schedulesEnabled.findOne({ employeeId: data.employeeId, companyId: data.companyId, - startDate: dayjs(data.startDate).startOf("day").toDate(), - endDate: dayjs(data.endDate).endOf("day").toDate(), + startDate: this.normalizeScheduleStartDate(data.startDate), + endDate: this.normalizeScheduleEndDate(data.endDate), }); if (schedulesEnabledCheck) { @@ -98,8 +104,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const newEmplyeesEnabled = await this.schedulesEnabled.create({ employeeId: data.employeeId, companyId: data.companyId, - startDate: dayjs(data.startDate).startOf("day").toDate(), - endDate: dayjs(data.endDate).endOf("day").toDate(), + startDate: this.normalizeScheduleStartDate(data.startDate), + endDate: this.normalizeScheduleEndDate(data.endDate), sessionUser: data.sessionUser, }); @@ -139,7 +145,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const employeeCheck = await EmployeesList.employees.findOne({ companyId: data.companyId, _id: employeeId, - }); + removed: { $ne: true }, + } as any); if (!employeeCheck) { continue; @@ -148,8 +155,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const schedulesEnabledCheck = await this.schedulesEnabled.findOne({ employeeId: employeeId, companyId: data.companyId, - startDate: dayjs(data.startDate).startOf("day").toDate(), - endDate: dayjs(data.endDate).endOf("day").toDate(), + startDate: this.normalizeScheduleStartDate(data.startDate), + endDate: this.normalizeScheduleEndDate(data.endDate), }); if (schedulesEnabledCheck) { @@ -159,8 +166,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { await this.schedulesEnabled.create({ employeeId: employeeId, companyId: data.companyId, - startDate: dayjs(data.startDate).startOf("day").toDate(), - endDate: dayjs(data.endDate).endOf("day").toDate(), + startDate: this.normalizeScheduleStartDate(data.startDate), + endDate: this.normalizeScheduleEndDate(data.endDate), sessionUser: data.sessionUser, }); @@ -199,7 +206,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const employeeCheck = await EmployeesList.employees.findOne({ companyId: data.companyId, _id: employeeId, - }); + removed: { $ne: true }, + } as any); if (!employeeCheck) { continue; @@ -208,8 +216,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const schedulesEnabledCheck = await this.schedulesEnabled.findOne({ employeeId: employeeId, companyId: data.companyId, - startDate: dayjs(data.startDate).startOf("day").toDate(), - endDate: dayjs(data.endDate).endOf("day").toDate(), + startDate: this.normalizeScheduleStartDate(data.startDate), + endDate: this.normalizeScheduleEndDate(data.endDate), }); if (schedulesEnabledCheck) { @@ -243,7 +251,12 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { } public async find(data: FindSchedulesEnabledParams): Promise { - let employeeFilter: any = { companyId: data.companyId }; + const rangeStart = this.normalizeScheduleStartDate(data.startDate); + const rangeEnd = this.normalizeScheduleEndDate(data.endDate); + let employeeFilter: any = { + companyId: data.companyId, + removed: { $ne: true }, + }; if (data.employeeId) { employeeFilter = { ...employeeFilter, @@ -258,8 +271,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const schedulesEnabled = await this.schedulesEnabled.schedulesEnabledList.findOne({ employeeId: employee.id, companyId: data.companyId, - startDate: dayjs(data.startDate).startOf("day").toDate(), - endDate: dayjs(data.endDate).endOf("day").toDate(), + startDate: { $lte: rangeStart }, + endDate: { $gte: rangeEnd }, }); const employUser = await UsersManager.users.findOne({ @@ -360,32 +373,32 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { const today = dayjs().startOf("day"); const summary = await this.buildReservationPeriodsSummary(data.companyId); - const operations = targetEmployees.map(employee => { + const operations = targetEmployees.flatMap(employee => { const employeeSummary = summary.employees.find(item => String(item.employeeId) === String(employee.id)); - const startDate = employeeSummary?.currentOpenUntil && dayjs(employeeSummary.currentOpenUntil).isAfter(today) - ? dayjs(employeeSummary.currentOpenUntil).add(1, "day").startOf("day") - : today; - const endDate = startDate.add(Number(duration.value), duration.unit).subtract(1, "day").endOf("day"); + const startDate = employeeSummary?.currentOpenUntil && !dayjs(employeeSummary.currentOpenUntil).endOf("day").isBefore(today) + ? this.getNextWeekStartAfter(dayjs(employeeSummary.currentOpenUntil)) + : this.getWeekStart(today); + const weekRanges = this.buildReservationPeriodWeekRanges(startDate, duration); - return { + return weekRanges.map(range => ({ updateOne: { filter: { employeeId: employee.id, companyId: data.companyId, - startDate: startDate.toDate(), - endDate: endDate.toDate(), + startDate: this.normalizeScheduleStartDate(range.startDate), + endDate: this.normalizeScheduleEndDate(range.endDate), }, update: { $setOnInsert: { employeeId: employee.id, companyId: data.companyId, - startDate: startDate.toDate(), - endDate: endDate.toDate(), + startDate: this.normalizeScheduleStartDate(range.startDate), + endDate: this.normalizeScheduleEndDate(range.endDate), }, }, upsert: true, }, - }; + })); }); await this.schedulesEnabled.schedulesEnabledList.bulkWrite(operations, { ordered: true }); @@ -393,6 +406,58 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager { return this.buildReservationPeriodsSummary(data.companyId); } + private getWeekStart(date: dayjs.Dayjs): dayjs.Dayjs { + return date.startOf("day").subtract((date.day() + 6) % 7, "day"); + } + + private getNextWeekStartAfter(date: dayjs.Dayjs): dayjs.Dayjs { + const nextDay = date.add(1, "day").startOf("day"); + const weekStart = this.getWeekStart(nextDay); + + return weekStart.isBefore(nextDay) ? weekStart.add(1, "week") : weekStart; + } + + private buildReservationPeriodWeekRanges( + startWeek: dayjs.Dayjs, + duration: { unit: "week" | "month"; value: number } + ): { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs }[] { + const ranges: { startDate: dayjs.Dayjs; endDate: dayjs.Dayjs }[] = []; + const targetEnd = duration.unit === "week" + ? startWeek.add(Number(duration.value), "week").subtract(1, "day").endOf("day") + : startWeek.add(Number(duration.value), "month").subtract(1, "day").endOf("day"); + let currentWeekStart = startWeek.startOf("day"); + + while (!currentWeekStart.isAfter(targetEnd)) { + ranges.push({ + startDate: currentWeekStart, + endDate: currentWeekStart.add(6, "day").endOf("day"), + }); + currentWeekStart = currentWeekStart.add(1, "week"); + } + + return ranges; + } + + private normalizeScheduleStartDate(date: string | Date | dayjs.Dayjs): Date { + return dayjs.tz(this.getScheduleDateOnly(date), SCHEDULE_BOUNDARY_TIMEZONE).startOf("day").toDate(); + } + + private normalizeScheduleEndDate(date: string | Date | dayjs.Dayjs): Date { + return dayjs.tz(this.getScheduleDateOnly(date), SCHEDULE_BOUNDARY_TIMEZONE).endOf("day").toDate(); + } + + private getScheduleDateOnly(date: string | Date | dayjs.Dayjs): string { + if (typeof date === "string") { + return date.slice(0, 10); + } + + if (dayjs.isDayjs(date)) { + return date.format("YYYY-MM-DD"); + } + + return date.toISOString().slice(0, 10); + } + private async buildReservationPeriodsSummary(companyId: string): Promise { const today = dayjs().startOf("day"); const employees = await EmployeesList.findByCompanyId({ companyId }); diff --git a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/schedules/page.tsx b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/schedules/page.tsx index 09ba01d..1fa8ee8 100644 --- a/txclient/src/app/admin/(organization-profile)/org/profile/[id]/schedules/page.tsx +++ b/txclient/src/app/admin/(organization-profile)/org/profile/[id]/schedules/page.tsx @@ -12,14 +12,12 @@ import { disableSchedule, enableSchedule, loadSchedulesEnabled } from "../Org.Se import WeekInput from "@core/app/components/WeekInput/WeekInput"; import dayjs from "dayjs"; import "dayjs/locale/es"; -import weekday from "dayjs/plugin/weekday"; -import "dayjs/locale/es"; import { SchedulesEnabledView } from "@models/SchedulesEnabled.type"; import Avatar from "@components/Avatar/Avatar"; import Button from "@core/app/components/Button/Button"; import { useConfirmStore } from "@core/Store/Confirm.Store"; +import { getMondayWeekStart } from "@core/helpers/week"; -dayjs.extend(weekday); dayjs.locale("es"); type UrlData = { @@ -34,7 +32,7 @@ export default function Schedules() { const SessionInfo = useSessionStore(); const alert = useAlert(); const confirm = useConfirmStore(); - const [startDate, setStartDate] = useState(dayjs().weekday(0)); + const [startDate, setStartDate] = useState(getMondayWeekStart()); const [collaborators, setCollaborators] = useState([]); useEffect(() => { diff --git a/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx b/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx index a55aa5f..39016cb 100644 --- a/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx +++ b/txclient/src/app/admin/assistant/components/StepEngine/StepEngine.tsx @@ -12,9 +12,8 @@ import ReactAvatar from "react-avatar"; import API from "@services/Api.Service"; import dayjs from "dayjs"; import "dayjs/locale/es"; -import weekday from "dayjs/plugin/weekday"; +import { getMondayWeekStart, getSundayWeekEnd } from "@core/helpers/week"; -dayjs.extend(weekday); dayjs.locale("es"); import categories from "@models/Categories.type"; @@ -2002,8 +2001,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm try { // Hacemos un loop de 8 semanas, habilitando bloques exactos de lunes a domingo for (let i = 0; i < 8; i++) { - const weekStart = dayjs().weekday(i * 7).format("YYYY-MM-DD"); - const weekEnd = dayjs().weekday(i * 7 + 6).format("YYYY-MM-DD"); + const weekStartDate = getMondayWeekStart().add(i, "week"); + const weekStart = weekStartDate.format("YYYY-MM-DD"); + const weekEnd = getSundayWeekEnd(weekStartDate).format("YYYY-MM-DD"); await API.post("schedules-enabled/enable", { companyId: createdCompanyId, diff --git a/txclient/src/app/components/WeekInput/WeekInput.tsx b/txclient/src/app/components/WeekInput/WeekInput.tsx index eeedfc7..b8a9a6b 100644 --- a/txclient/src/app/components/WeekInput/WeekInput.tsx +++ b/txclient/src/app/components/WeekInput/WeekInput.tsx @@ -5,10 +5,9 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowBackwardIcon from "@mui/icons-material/ArrowBackIos"; import HouseIcon from "@mui/icons-material/House"; import dayjs from "dayjs"; -import weekday from "dayjs/plugin/weekday"; import "dayjs/locale/es"; +import { getMondayWeekStart, getSundayWeekEnd } from "@core/helpers/week"; -dayjs.extend(weekday); dayjs.locale("es"); export interface WeekInputProps { @@ -25,15 +24,15 @@ const WeekInput = (props: WeekInputProps) => { const { setStartDate } = props; const [fechas, setFechas] = useState({ currentDate: dayjs().clone(), - lunes: dayjs().clone().weekday(0), - domingo: dayjs().clone().weekday(6), + lunes: getMondayWeekStart(), + domingo: getSundayWeekEnd(), }); const updateFechas = (fecha: dayjs.Dayjs) => { setFechas({ currentDate: dayjs().clone(), - lunes: fecha.clone().weekday(0), - domingo: fecha.clone().weekday(6), + lunes: getMondayWeekStart(fecha), + domingo: getSundayWeekEnd(fecha), }); //setStartDate(fechas.lunes.clone()); }; @@ -53,8 +52,8 @@ const WeekInput = (props: WeekInputProps) => { const fecha = dayjs().clone(); setFechas({ currentDate: fecha, - lunes: fecha.clone().weekday(0), - domingo: fecha.clone().weekday(6), + lunes: getMondayWeekStart(fecha), + domingo: getSundayWeekEnd(fecha), }); }; diff --git a/txclient/src/helpers/week.ts b/txclient/src/helpers/week.ts new file mode 100644 index 0000000..229c524 --- /dev/null +++ b/txclient/src/helpers/week.ts @@ -0,0 +1,9 @@ +import dayjs from "dayjs"; + +export const getMondayWeekStart = (date: dayjs.Dayjs = dayjs()): dayjs.Dayjs => { + return date.startOf("day").subtract((date.day() + 6) % 7, "day"); +}; + +export const getSundayWeekEnd = (date: dayjs.Dayjs = dayjs()): dayjs.Dayjs => { + return getMondayWeekStart(date).add(6, "day"); +};