refactor: centralize week calculation logic and standardize schedule boundary date normalization across client and server

This commit is contained in:
2026-07-28 18:12:10 -03:00
parent ece3879c8d
commit f39ba44f59
5 changed files with 113 additions and 42 deletions
@@ -25,11 +25,17 @@ import { joinStrings } from "../../helpers/String";
import getAvatar from "../../helpers/getAvatar"; import getAvatar from "../../helpers/getAvatar";
import { NotificationsManager } from "../Notifications/Notifications"; import { NotificationsManager } from "../Notifications/Notifications";
import dayjs from "dayjs"; 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 { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check"; import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.locale("es"); dayjs.locale("es");
const SCHEDULE_BOUNDARY_TIMEZONE = "America/Argentina/Buenos_Aires";
const NEAR_ENDING_THRESHOLD_DAYS = 14; const NEAR_ENDING_THRESHOLD_DAYS = 14;
const ALLOWED_RESERVATION_PERIOD_DURATIONS = [ const ALLOWED_RESERVATION_PERIOD_DURATIONS = [
{ unit: "week", value: 1 }, { unit: "week", value: 1 },
@@ -87,8 +93,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const schedulesEnabledCheck = await this.schedulesEnabled.findOne({ const schedulesEnabledCheck = await this.schedulesEnabled.findOne({
employeeId: data.employeeId, employeeId: data.employeeId,
companyId: data.companyId, companyId: data.companyId,
startDate: dayjs(data.startDate).startOf("day").toDate(), startDate: this.normalizeScheduleStartDate(data.startDate),
endDate: dayjs(data.endDate).endOf("day").toDate(), endDate: this.normalizeScheduleEndDate(data.endDate),
}); });
if (schedulesEnabledCheck) { if (schedulesEnabledCheck) {
@@ -98,8 +104,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const newEmplyeesEnabled = await this.schedulesEnabled.create({ const newEmplyeesEnabled = await this.schedulesEnabled.create({
employeeId: data.employeeId, employeeId: data.employeeId,
companyId: data.companyId, companyId: data.companyId,
startDate: dayjs(data.startDate).startOf("day").toDate(), startDate: this.normalizeScheduleStartDate(data.startDate),
endDate: dayjs(data.endDate).endOf("day").toDate(), endDate: this.normalizeScheduleEndDate(data.endDate),
sessionUser: data.sessionUser, sessionUser: data.sessionUser,
}); });
@@ -139,7 +145,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const employeeCheck = await EmployeesList.employees.findOne({ const employeeCheck = await EmployeesList.employees.findOne({
companyId: data.companyId, companyId: data.companyId,
_id: employeeId, _id: employeeId,
}); removed: { $ne: true },
} as any);
if (!employeeCheck) { if (!employeeCheck) {
continue; continue;
@@ -148,8 +155,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const schedulesEnabledCheck = await this.schedulesEnabled.findOne({ const schedulesEnabledCheck = await this.schedulesEnabled.findOne({
employeeId: employeeId, employeeId: employeeId,
companyId: data.companyId, companyId: data.companyId,
startDate: dayjs(data.startDate).startOf("day").toDate(), startDate: this.normalizeScheduleStartDate(data.startDate),
endDate: dayjs(data.endDate).endOf("day").toDate(), endDate: this.normalizeScheduleEndDate(data.endDate),
}); });
if (schedulesEnabledCheck) { if (schedulesEnabledCheck) {
@@ -159,8 +166,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
await this.schedulesEnabled.create({ await this.schedulesEnabled.create({
employeeId: employeeId, employeeId: employeeId,
companyId: data.companyId, companyId: data.companyId,
startDate: dayjs(data.startDate).startOf("day").toDate(), startDate: this.normalizeScheduleStartDate(data.startDate),
endDate: dayjs(data.endDate).endOf("day").toDate(), endDate: this.normalizeScheduleEndDate(data.endDate),
sessionUser: data.sessionUser, sessionUser: data.sessionUser,
}); });
@@ -199,7 +206,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const employeeCheck = await EmployeesList.employees.findOne({ const employeeCheck = await EmployeesList.employees.findOne({
companyId: data.companyId, companyId: data.companyId,
_id: employeeId, _id: employeeId,
}); removed: { $ne: true },
} as any);
if (!employeeCheck) { if (!employeeCheck) {
continue; continue;
@@ -208,8 +216,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const schedulesEnabledCheck = await this.schedulesEnabled.findOne({ const schedulesEnabledCheck = await this.schedulesEnabled.findOne({
employeeId: employeeId, employeeId: employeeId,
companyId: data.companyId, companyId: data.companyId,
startDate: dayjs(data.startDate).startOf("day").toDate(), startDate: this.normalizeScheduleStartDate(data.startDate),
endDate: dayjs(data.endDate).endOf("day").toDate(), endDate: this.normalizeScheduleEndDate(data.endDate),
}); });
if (schedulesEnabledCheck) { if (schedulesEnabledCheck) {
@@ -243,7 +251,12 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
} }
public async find(data: FindSchedulesEnabledParams): Promise<SchedulesEnabledView[]> { public async find(data: FindSchedulesEnabledParams): Promise<SchedulesEnabledView[]> {
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) { if (data.employeeId) {
employeeFilter = { employeeFilter = {
...employeeFilter, ...employeeFilter,
@@ -258,8 +271,8 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const schedulesEnabled = await this.schedulesEnabled.schedulesEnabledList.findOne({ const schedulesEnabled = await this.schedulesEnabled.schedulesEnabledList.findOne({
employeeId: employee.id, employeeId: employee.id,
companyId: data.companyId, companyId: data.companyId,
startDate: dayjs(data.startDate).startOf("day").toDate(), startDate: { $lte: rangeStart },
endDate: dayjs(data.endDate).endOf("day").toDate(), endDate: { $gte: rangeEnd },
}); });
const employUser = await UsersManager.users.findOne({ const employUser = await UsersManager.users.findOne({
@@ -360,32 +373,32 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
const today = dayjs().startOf("day"); const today = dayjs().startOf("day");
const summary = await this.buildReservationPeriodsSummary(data.companyId); 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 employeeSummary = summary.employees.find(item => String(item.employeeId) === String(employee.id));
const startDate = employeeSummary?.currentOpenUntil && dayjs(employeeSummary.currentOpenUntil).isAfter(today) const startDate = employeeSummary?.currentOpenUntil && !dayjs(employeeSummary.currentOpenUntil).endOf("day").isBefore(today)
? dayjs(employeeSummary.currentOpenUntil).add(1, "day").startOf("day") ? this.getNextWeekStartAfter(dayjs(employeeSummary.currentOpenUntil))
: today; : this.getWeekStart(today);
const endDate = startDate.add(Number(duration.value), duration.unit).subtract(1, "day").endOf("day"); const weekRanges = this.buildReservationPeriodWeekRanges(startDate, duration);
return { return weekRanges.map(range => ({
updateOne: { updateOne: {
filter: { filter: {
employeeId: employee.id, employeeId: employee.id,
companyId: data.companyId, companyId: data.companyId,
startDate: startDate.toDate(), startDate: this.normalizeScheduleStartDate(range.startDate),
endDate: endDate.toDate(), endDate: this.normalizeScheduleEndDate(range.endDate),
}, },
update: { update: {
$setOnInsert: { $setOnInsert: {
employeeId: employee.id, employeeId: employee.id,
companyId: data.companyId, companyId: data.companyId,
startDate: startDate.toDate(), startDate: this.normalizeScheduleStartDate(range.startDate),
endDate: endDate.toDate(), endDate: this.normalizeScheduleEndDate(range.endDate),
}, },
}, },
upsert: true, upsert: true,
}, },
}; }));
}); });
await this.schedulesEnabled.schedulesEnabledList.bulkWrite(operations, { ordered: true }); await this.schedulesEnabled.schedulesEnabledList.bulkWrite(operations, { ordered: true });
@@ -393,6 +406,58 @@ class SchedulesEnabledManager implements ISchedulesEnabledManager {
return this.buildReservationPeriodsSummary(data.companyId); 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<ReservationPeriodsSummary> { private async buildReservationPeriodsSummary(companyId: string): Promise<ReservationPeriodsSummary> {
const today = dayjs().startOf("day"); const today = dayjs().startOf("day");
const employees = await EmployeesList.findByCompanyId({ companyId }); const employees = await EmployeesList.findByCompanyId({ companyId });
@@ -12,14 +12,12 @@ import { disableSchedule, enableSchedule, loadSchedulesEnabled } from "../Org.Se
import WeekInput from "@core/app/components/WeekInput/WeekInput"; import WeekInput from "@core/app/components/WeekInput/WeekInput";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/es"; import "dayjs/locale/es";
import weekday from "dayjs/plugin/weekday";
import "dayjs/locale/es";
import { SchedulesEnabledView } from "@models/SchedulesEnabled.type"; import { SchedulesEnabledView } from "@models/SchedulesEnabled.type";
import Avatar from "@components/Avatar/Avatar"; import Avatar from "@components/Avatar/Avatar";
import Button from "@core/app/components/Button/Button"; import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store"; import { useConfirmStore } from "@core/Store/Confirm.Store";
import { getMondayWeekStart } from "@core/helpers/week";
dayjs.extend(weekday);
dayjs.locale("es"); dayjs.locale("es");
type UrlData = { type UrlData = {
@@ -34,7 +32,7 @@ export default function Schedules() {
const SessionInfo = useSessionStore(); const SessionInfo = useSessionStore();
const alert = useAlert(); const alert = useAlert();
const confirm = useConfirmStore(); const confirm = useConfirmStore();
const [startDate, setStartDate] = useState<dayjs.Dayjs>(dayjs().weekday(0)); const [startDate, setStartDate] = useState<dayjs.Dayjs>(getMondayWeekStart());
const [collaborators, setCollaborators] = useState<SchedulesEnabledView[]>([]); const [collaborators, setCollaborators] = useState<SchedulesEnabledView[]>([]);
useEffect(() => { useEffect(() => {
@@ -12,9 +12,8 @@ import ReactAvatar from "react-avatar";
import API from "@services/Api.Service"; import API from "@services/Api.Service";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/es"; import "dayjs/locale/es";
import weekday from "dayjs/plugin/weekday"; import { getMondayWeekStart, getSundayWeekEnd } from "@core/helpers/week";
dayjs.extend(weekday);
dayjs.locale("es"); dayjs.locale("es");
import categories from "@models/Categories.type"; import categories from "@models/Categories.type";
@@ -2002,8 +2001,9 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
try { try {
// Hacemos un loop de 8 semanas, habilitando bloques exactos de lunes a domingo // Hacemos un loop de 8 semanas, habilitando bloques exactos de lunes a domingo
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
const weekStart = dayjs().weekday(i * 7).format("YYYY-MM-DD"); const weekStartDate = getMondayWeekStart().add(i, "week");
const weekEnd = dayjs().weekday(i * 7 + 6).format("YYYY-MM-DD"); const weekStart = weekStartDate.format("YYYY-MM-DD");
const weekEnd = getSundayWeekEnd(weekStartDate).format("YYYY-MM-DD");
await API.post<any>("schedules-enabled/enable", { await API.post<any>("schedules-enabled/enable", {
companyId: createdCompanyId, companyId: createdCompanyId,
@@ -5,10 +5,9 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForwardIos";
import ArrowBackwardIcon from "@mui/icons-material/ArrowBackIos"; import ArrowBackwardIcon from "@mui/icons-material/ArrowBackIos";
import HouseIcon from "@mui/icons-material/House"; import HouseIcon from "@mui/icons-material/House";
import dayjs from "dayjs"; import dayjs from "dayjs";
import weekday from "dayjs/plugin/weekday";
import "dayjs/locale/es"; import "dayjs/locale/es";
import { getMondayWeekStart, getSundayWeekEnd } from "@core/helpers/week";
dayjs.extend(weekday);
dayjs.locale("es"); dayjs.locale("es");
export interface WeekInputProps { export interface WeekInputProps {
@@ -25,15 +24,15 @@ const WeekInput = (props: WeekInputProps) => {
const { setStartDate } = props; const { setStartDate } = props;
const [fechas, setFechas] = useState<WeekInputData>({ const [fechas, setFechas] = useState<WeekInputData>({
currentDate: dayjs().clone(), currentDate: dayjs().clone(),
lunes: dayjs().clone().weekday(0), lunes: getMondayWeekStart(),
domingo: dayjs().clone().weekday(6), domingo: getSundayWeekEnd(),
}); });
const updateFechas = (fecha: dayjs.Dayjs) => { const updateFechas = (fecha: dayjs.Dayjs) => {
setFechas({ setFechas({
currentDate: dayjs().clone(), currentDate: dayjs().clone(),
lunes: fecha.clone().weekday(0), lunes: getMondayWeekStart(fecha),
domingo: fecha.clone().weekday(6), domingo: getSundayWeekEnd(fecha),
}); });
//setStartDate(fechas.lunes.clone()); //setStartDate(fechas.lunes.clone());
}; };
@@ -53,8 +52,8 @@ const WeekInput = (props: WeekInputProps) => {
const fecha = dayjs().clone(); const fecha = dayjs().clone();
setFechas({ setFechas({
currentDate: fecha, currentDate: fecha,
lunes: fecha.clone().weekday(0), lunes: getMondayWeekStart(fecha),
domingo: fecha.clone().weekday(6), domingo: getSundayWeekEnd(fecha),
}); });
}; };
+9
View File
@@ -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");
};