refactor: centralize week calculation logic and standardize schedule boundary date normalization across client and server
This commit is contained in:
@@ -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),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user