recurring events

This commit is contained in:
2026-02-10 12:59:26 -05:00
parent 092bc1d0c2
commit 323add8d9a
16 changed files with 762 additions and 38 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
app/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,137 @@
const { splitSite } = require("../middlewares");
const db = require("../models");
const CalendarEventRecur = db.calendar_event_recur;
// Create a new Recurring Event Rule
exports.create = (req, res) => {
if (!req.body.rrule || !req.body.start_repeat_date) {
res.status(400).send({ message: "rrule and start_repeat_date are required!" });
return;
}
const site = splitSite.findSiteNumber(req);
const recur = new CalendarEventRecur({
title: req.body.title,
type: req.body.type,
description: req.body.description,
color: req.body.color,
status: req.body.status || 'active',
target_type: req.body.target_type,
target_uuid: req.body.target_uuid,
target_name: req.body.target_name,
event_location: req.body.event_location,
event_reminder_type: req.body.event_reminder_type,
meal_type: req.body.meal_type,
activity_category: req.body.activity_category,
ingredients: req.body.ingredients,
rrule: req.body.rrule,
start_repeat_date: req.body.start_repeat_date,
end_repeat_date: req.body.end_repeat_date,
indefinite_repeat: req.body.indefinite_repeat || false,
create_by: req.body.create_by,
create_date: req.body.create_date,
edit_by: req.body.edit_by,
edit_date: req.body.edit_date,
edit_history: req.body.edit_history,
site
});
recur
.save(recur)
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message: err.message || "Some error occurred while creating the Recurring Event Rule."
});
});
};
// Get all active recurring event rules
exports.getAll = (req, res) => {
var condition = { status: 'active' };
condition = splitSite.splitSiteGet(req, condition);
CalendarEventRecur.find(condition)
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message: err.message || "Some error occurred while retrieving Recurring Event Rules."
});
});
};
// Get one by ID
exports.getOne = (req, res) => {
const id = req.params.id;
CalendarEventRecur.findById(id)
.then(data => {
if (!data)
res.status(404).send({ message: "Not found Recurring Event Rule with id " + id });
else res.send(data);
})
.catch(err => {
res.status(500).send({ message: "Error retrieving Recurring Event Rule with id=" + id });
});
};
// Update by ID
exports.update = (req, res) => {
if (!req.body) {
return res.status(400).send({ message: "Data to update can not be empty!" });
}
const id = req.params.id;
CalendarEventRecur.findByIdAndUpdate(id, req.body, { useFindAndModify: false })
.then(data => {
if (!data) {
res.status(404).send({
message: `Cannot update Recurring Event Rule with id=${id}. Maybe it was not found!`
});
} else res.send({ success: true, message: "Recurring Event Rule was updated successfully." });
})
.catch(err => {
res.status(500).send({
success: false,
message: "Error updating Recurring Event Rule with id=" + id
});
});
};
// Disable (set status to inactive)
exports.disable = (req, res) => {
const id = req.params.id;
CalendarEventRecur.findByIdAndUpdate(id, { ...req.body, status: 'inactive' }, { useFindAndModify: false })
.then(data => {
if (!data) {
res.status(404).send({
message: `Cannot disable Recurring Event Rule with id=${id}. Maybe it was not found!`
});
} else res.send({ success: true, message: "Recurring Event Rule was disabled successfully." });
})
.catch(err => {
res.status(500).send({
success: false,
message: "Error disabling Recurring Event Rule with id=" + id
});
});
};
// Delete by ID
exports.delete = (req, res) => {
const id = req.params.id;
CalendarEventRecur.findByIdAndRemove(id)
.then(data => {
if (!data) {
res.status(404).send({
message: `Cannot delete Recurring Event Rule with id=${id}. Maybe it was not found!`
});
} else {
res.send({ message: "Recurring Event Rule was deleted successfully!" });
}
})
.catch(err => {
res.status(500).send({
message: "Could not delete Recurring Event Rule with id=" + id
});
});
};

View File

@@ -0,0 +1,48 @@
module.exports = mongoose => {
var editHistorySchema = mongoose.Schema({
employee: String,
date: Date
});
var schema = mongoose.Schema(
{
// Event template data
title: String,
type: String,
description: String,
color: String,
status: String,
// value could be ['active', 'inactive']
target_type: String,
target_uuid: String,
target_name: String,
event_location: String,
event_reminder_type: String,
meal_type: String,
activity_category: String,
ingredients: String,
// Recurrence fields
rrule: String,
// rrule could be 'FREQ=DAILY', 'FREQ=WEEKLY', 'FREQ=MONTHLY', 'FREQ=YEARLY'
start_repeat_date: Date,
end_repeat_date: Date,
indefinite_repeat: Boolean,
// Metadata
create_by: String,
create_date: Date,
edit_by: String,
edit_date: Date,
edit_history: [{
type: editHistorySchema
}],
site: Number
},
{ collection: 'calendar_event_recur', timestamps: true }
);
schema.method("toJSON", function() {
const { __v, _id, ...object } = this.toObject();
object.id = _id;
return object;
});
const CalendarEventRecur = mongoose.model("calendar_event_recur", schema);
return CalendarEventRecur;
};

View File

@@ -19,6 +19,7 @@ db.center_phone = require("./center-phone.model")(mongoose);
db.message_token = require("./message-token.model")(mongoose);
db.sent_message = require("./sent-message.model")(mongoose);
db.calendar_event = require("./calendar-event.model")(mongoose);
db.calendar_event_recur = require("./calendar-event-recur.model")(mongoose);
db.doctemplate = require("./doctemplate.model")(mongoose);
db.exceltemplate = require("./xlsxtemplate.model")(mongoose);
db.timedata = require("./timedata.model")(mongoose);

View File

@@ -0,0 +1,19 @@
const { authJwt } = require("../middlewares");
module.exports = app => {
const controller = require("../controllers/calendar-event-recur.controller.js");
app.use((req, res, next) => {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
var router = require("express").Router();
router.get("/", [authJwt.verifyToken], controller.getAll);
router.post("/", [authJwt.verifyToken], controller.create);
router.get("/:id", [authJwt.verifyToken], controller.getOne);
router.put("/:id", [authJwt.verifyToken], controller.update);
router.put("/:id/disable", [authJwt.verifyToken], controller.disable);
router.delete("/:id", [authJwt.verifyToken], controller.delete);
app.use('/api/event-recurrences', router);
};

View File

@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.57cff37a.css",
"main.js": "/static/js/main.e64707d9.js",
"main.js": "/static/js/main.1a821513.js",
"static/js/787.c4e7f8f9.chunk.js": "/static/js/787.c4e7f8f9.chunk.js",
"static/media/landing.png": "/static/media/landing.d4c6072db7a67dff6a78.png",
"index.html": "/index.html",
"main.57cff37a.css.map": "/static/css/main.57cff37a.css.map",
"main.e64707d9.js.map": "/static/js/main.e64707d9.js.map",
"main.1a821513.js.map": "/static/js/main.1a821513.js.map",
"787.c4e7f8f9.chunk.js.map": "/static/js/787.c4e7f8f9.chunk.js.map"
},
"entrypoints": [
"static/css/main.57cff37a.css",
"static/js/main.e64707d9.js"
"static/js/main.1a821513.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.e64707d9.js"></script><link href="/static/css/main.57cff37a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><link rel="manifest" href="/manifest.json"/><title>Worldshine Transportation</title><script defer="defer" src="/static/js/main.1a821513.js"></script><link href="/static/css/main.57cff37a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
client/.DS_Store vendored

Binary file not shown.

View File

@@ -90,6 +90,14 @@ const EventsCalendar = () => {
const [newReminderTitleCategory, setNewReminderTitleCategory] = useState('');
const [newReminderAssociatedEntity, setNewReminderAssociatedEntity] = useState(null);
// Repeat date fields (new modal)
const [newRepeatStartDate, setNewRepeatStartDate] = useState(null);
const [newRepeatEndDate, setNewRepeatEndDate] = useState(null);
const [newIndefiniteRepeat, setNewIndefiniteRepeat] = useState(false);
// Event recurrences data
const [allEventRecurrences, setAllEventRecurrences] = useState([]);
// Edit modal state (for non-medical tabs)
const [showEditModal, setShowEditModal] = useState(false);
const [editingEventId, setEditingEventId] = useState(null);
@@ -108,6 +116,11 @@ const EventsCalendar = () => {
// Important Dates edit fields
const [editReminderTitleCategory, setEditReminderTitleCategory] = useState('');
const [editReminderAssociatedEntity, setEditReminderAssociatedEntity] = useState(null);
// Repeat date fields (edit modal)
const [editRepeatStartDate, setEditRepeatStartDate] = useState(null);
const [editRepeatEndDate, setEditRepeatEndDate] = useState(null);
const [editIndefiniteRepeat, setEditIndefiniteRepeat] = useState(false);
const [editingRecurId, setEditingRecurId] = useState(null); // tracks if editing a recurrence rule
// Helper function to format name from "lastname, firstname" to "firstname lastname"
const formatFullName = (name) => {
@@ -143,6 +156,56 @@ const EventsCalendar = () => {
return doctorName ? `${fullName} - ${doctorName}` : fullName;
};
// Helper: expand a recurring rule into individual event instances for a date range
const expandRecurrence = (rule, rangeFrom, rangeTo) => {
const instances = [];
const startDate = new Date(rule.start_repeat_date);
const endDate = new Date(rule.end_repeat_date);
const from = new Date(rangeFrom);
const to = new Date(rangeTo);
// Determine the effective range (overlap of rule range and visible range)
const effectiveStart = from > startDate ? from : startDate;
const effectiveEnd = to < endDate ? to : endDate;
if (effectiveStart > effectiveEnd) return instances;
const freq = rule.rrule; // e.g. 'FREQ=DAILY', 'FREQ=WEEKLY', 'FREQ=MONTHLY', 'FREQ=YEARLY'
let current = new Date(startDate);
// Limit to 1000 instances max to avoid infinite loops
let count = 0;
while (current <= effectiveEnd && count < 1000) {
if (current >= effectiveStart) {
const dateStr = moment(current).format('YYYY-MM-DD');
instances.push({
...rule,
id: `recur-${rule.id}-${dateStr}`,
_recur_id: rule.id,
start_time: new Date(current),
stop_time: new Date(current),
});
}
// Advance to next occurrence
if (freq === 'FREQ=DAILY') {
current = new Date(current.getTime() + 24 * 60 * 60 * 1000);
} else if (freq === 'FREQ=WEEKLY') {
current = new Date(current.getTime() + 7 * 24 * 60 * 60 * 1000);
} else if (freq === 'FREQ=MONTHLY') {
const next = new Date(current);
next.setMonth(next.getMonth() + 1);
current = next;
} else if (freq === 'FREQ=YEARLY') {
const next = new Date(current);
next.setFullYear(next.getFullYear() + 1);
current = next;
} else {
break;
}
count++;
}
return instances;
};
const eventTypeMap = {
medicalCalendar: 'medical',
activitiesCalendar: 'activity',
@@ -261,8 +324,21 @@ const EventsCalendar = () => {
useEffect(() => {
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then(data => setAllEvents(data?.data));
EventsService.getAllEventRecurrences().then(data => setAllEventRecurrences(data?.data || []));
}, [fromDate, toDate]);
// Auto-fill repeat start date from event date when repeat option is selected (new modal)
useEffect(() => {
if (newEventRecurring && newEventStartDateTime) {
setNewRepeatStartDate(new Date(newEventStartDateTime));
}
if (!newEventRecurring) {
setNewRepeatStartDate(null);
setNewRepeatEndDate(null);
setNewIndefiniteRepeat(false);
}
}, [newEventRecurring, newEventStartDateTime]);
useEffect(() => {
setNewEventType(eventTypeMap[currentTab]);
if (currentTab === 'medicalCalendar') {
@@ -313,9 +389,13 @@ const EventsCalendar = () => {
return false;
}));
}
} else {
const originalEvents = [...allEvents];
let filteredEvents = originalEvents?.filter(item => item.type === eventTypeMap[currentTab])?.map(item => {
} else {
// Expand recurring event rules into individual instances and merge with regular events
const recurInstances = allEventRecurrences
.filter(rule => rule.type === eventTypeMap[currentTab] && rule.status === 'active')
.flatMap(rule => expandRecurrence(rule, fromDate, toDate));
const originalEvents = [...allEvents, ...recurInstances];
let filteredEvents = originalEvents?.filter(item => item.type === eventTypeMap[currentTab])?.map(item => {
// For Important Dates, remap old blue/orange colors to new member_related/vehicle_maintenance
let eventColor = item?.color;
if (currentTab === 'reminderDatesCalendar') {
@@ -369,7 +449,7 @@ const EventsCalendar = () => {
setEvents(filteredEvents);
}
}, [customers, resources, timeData, currentTab, allEvents, showDeletedItems, selectedColorFilters])
}, [customers, resources, timeData, currentTab, allEvents, allEventRecurrences, showDeletedItems, selectedColorFilters])
@@ -429,6 +509,22 @@ const EventsCalendar = () => {
}
const disableEvent = (id) => {
// Handle recurring event instances
const isRecurInstance = typeof id === 'string' && id.startsWith('recur-');
if (isRecurInstance) {
const currentEvent = events.find(item => item.id === id);
const recurId = currentEvent?._recur_id || id.split('-')[1];
EventsService.disableEventRecurrence(recurId, { status: 'inactive' }).then(() => {
Promise.all([
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }),
EventsService.getAllEventRecurrences()
]).then(([eventsRes, recurRes]) => {
setAllEvents(eventsRes.data);
setAllEventRecurrences(recurRes.data || []);
});
});
return;
}
const currentEvent = events.find(item => item.id === id);
EventsService.disableEvent(id, { status: 'inactive', edit_by: localStorage.getItem('user') && JSON.parse(localStorage.getItem('user'))?.name,
edit_date: new Date(),
@@ -479,9 +575,34 @@ const EventsCalendar = () => {
// Edit modal functions (for non-medical tabs)
const openEditModal = (calendarEvent) => {
const eventData = allEvents.find(e => e.id === calendarEvent.id) || calendarEvent;
setEditingEventId(eventData.id);
setEditEventStartDateTime(eventData.start_time ? new Date(eventData.start_time) : new Date());
// Check if this is a recurring event instance (ID starts with 'recur-')
const isRecurInstance = typeof calendarEvent.id === 'string' && calendarEvent.id.startsWith('recur-');
let eventData;
if (isRecurInstance) {
// Find the recurrence rule using the _recur_id stored on expanded instances
const recurId = calendarEvent._recur_id || calendarEvent.id.split('-')[1];
const recurRule = allEventRecurrences.find(r => r.id === recurId);
if (recurRule) {
eventData = { ...recurRule };
setEditingRecurId(recurId);
setEditingEventId(null);
setEditRepeatStartDate(recurRule.start_repeat_date ? new Date(recurRule.start_repeat_date) : null);
setEditRepeatEndDate(recurRule.end_repeat_date ? new Date(recurRule.end_repeat_date) : null);
setEditIndefiniteRepeat(recurRule.indefinite_repeat || false);
} else {
return; // Can't find rule, bail
}
} else {
eventData = allEvents.find(e => e.id === calendarEvent.id) || calendarEvent;
setEditingEventId(eventData.id);
setEditingRecurId(null);
setEditRepeatStartDate(null);
setEditRepeatEndDate(null);
setEditIndefiniteRepeat(false);
}
setEditEventStartDateTime(eventData.start_time ? new Date(eventData.start_time) : (eventData.start_repeat_date ? new Date(eventData.start_repeat_date) : new Date()));
if (currentTab === 'activitiesCalendar') {
setEditEventTitle(eventData.title || '');
@@ -491,6 +612,7 @@ const EventsCalendar = () => {
} else if (currentTab === 'incidentsCalendar') {
setEditAttendanceCustomer(eventData.target_uuid ? { value: eventData.target_uuid, label: eventData.target_name } : null);
setEditAttendanceReason(eventData.description || '');
setEditEventRecurring(eventData.rrule || '');
} else if (currentTab === 'mealPlanCalendar') {
setEditEventTitle(eventData.title || '');
setEditMealType(eventData.meal_type || '');
@@ -501,6 +623,13 @@ const EventsCalendar = () => {
setEditReminderAssociatedEntity(eventData.target_uuid ? { value: eventData.target_uuid, label: eventData.target_name } : null);
setEditEventRecurring(eventData.rrule || '');
}
// If editing a regular event that has an rrule, populate repeat date fields
if (!isRecurInstance && eventData.rrule) {
setEditRepeatStartDate(eventData.start_time ? new Date(eventData.start_time) : null);
setEditRepeatEndDate(null);
setEditIndefiniteRepeat(false);
}
setShowEditModal(true);
};
@@ -508,6 +637,7 @@ const EventsCalendar = () => {
const closeEditModal = () => {
setShowEditModal(false);
setEditingEventId(null);
setEditingRecurId(null);
setEditEventTitle('');
setEditEventStartDateTime(null);
setEditEventLocation('');
@@ -519,6 +649,9 @@ const EventsCalendar = () => {
setEditMealIngredients('');
setEditReminderTitleCategory('');
setEditReminderAssociatedEntity(null);
setEditRepeatStartDate(null);
setEditRepeatEndDate(null);
setEditIndefiniteRepeat(false);
};
const handleEditSave = () => {
@@ -538,7 +671,6 @@ const EventsCalendar = () => {
activity_category: editActivityCategory,
color: editActivityCategory,
event_location: editEventLocation,
rrule: editEventRecurring || undefined,
};
} else if (currentTab === 'incidentsCalendar') {
data = {
@@ -560,7 +692,6 @@ const EventsCalendar = () => {
meal_type: editMealType,
color: editMealType === 'breakfast' ? 'brown' : (editMealType === 'lunch' ? 'green' : 'red'),
ingredients: editMealIngredients,
rrule: editEventRecurring || undefined,
};
} else if (currentTab === 'reminderDatesCalendar') {
const isMemberCategory = ['birthday', 'adcaps_completion', 'center_qualification_expiration'].includes(editReminderTitleCategory);
@@ -573,17 +704,73 @@ const EventsCalendar = () => {
target_type: isMemberCategory ? 'customer' : 'vehicle',
target_uuid: editReminderAssociatedEntity?.value,
target_name: editReminderAssociatedEntity?.label,
rrule: editEventRecurring || undefined,
color: isMemberCategory ? 'member_related' : 'vehicle_maintenance',
};
}
EventsService.updateEvent(editingEventId, data).then(() => {
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then((data) => {
setAllEvents(data.data);
const refreshAll = () => {
Promise.all([
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }),
EventsService.getAllEventRecurrences()
]).then(([eventsRes, recurRes]) => {
setAllEvents(eventsRes.data);
setAllEventRecurrences(recurRes.data || []);
closeEditModal();
});
});
};
// If editing a recurrence rule
if (editingRecurId) {
const recurData = {
...data,
rrule: editEventRecurring,
start_repeat_date: editRepeatStartDate,
end_repeat_date: editIndefiniteRepeat ? new Date('2099-12-31') : editRepeatEndDate,
indefinite_repeat: editIndefiniteRepeat,
};
delete recurData.start_time;
delete recurData.stop_time;
if (!editEventRecurring) {
// Changed from recurring to one-time: delete recurrence rule, create regular event
EventsService.deleteEventRecurrence(editingRecurId).then(() => {
const eventData = { ...data };
delete eventData.rrule;
eventData.start_time = editEventStartDateTime;
eventData.stop_time = editEventStartDateTime;
eventData.type = eventTypeMap[currentTab];
eventData.status = 'active';
EventsService.createNewEvent(eventData).then(() => refreshAll());
});
} else {
EventsService.updateEventRecurrence(editingRecurId, recurData).then(() => refreshAll());
}
} else if (editingEventId) {
// Editing a regular event
if (editEventRecurring) {
// Changed from one-time to recurring: create recurrence rule, delete regular event
const recurData = {
...data,
type: eventTypeMap[currentTab],
status: 'active',
rrule: editEventRecurring,
start_repeat_date: editRepeatStartDate || editEventStartDateTime,
end_repeat_date: editIndefiniteRepeat ? new Date('2099-12-31') : editRepeatEndDate,
indefinite_repeat: editIndefiniteRepeat,
create_by: data.edit_by,
create_date: new Date(),
};
delete recurData.start_time;
delete recurData.stop_time;
EventsService.createEventRecurrence(recurData).then(() => {
EventsService.disableEvent(editingEventId, {}).then(() => refreshAll());
});
} else {
// Still one-time, just update normally
delete data.rrule;
EventsService.updateEvent(editingEventId, data).then(() => refreshAll());
}
}
};
const toggleColorFilter = (colorValue) => {
@@ -902,6 +1089,9 @@ const handleClose = () => {
setNewEventEndDateTime(undefined);
setNewEventColor('');
setNewEventRecurring(undefined);
setNewRepeatStartDate(null);
setNewRepeatEndDate(null);
setNewIndefiniteRepeat(false);
setNewEventReminderType('');
// Reset medical fields
setNewEventCustomer('');
@@ -975,7 +1165,6 @@ const handleSave = () => {
activity_category: newActivityCategory,
color: newActivityCategory, // category maps to color for display
event_location: newEventLocation,
rrule: newEventRecurring,
};
}
@@ -1004,7 +1193,6 @@ const handleSave = () => {
meal_type: newMealType,
color: newMealType === 'breakfast' ? 'brown' : (newMealType === 'lunch' ? 'green' : 'red'),
ingredients: newMealIngredients,
rrule: newEventRecurring,
};
}
@@ -1020,17 +1208,42 @@ const handleSave = () => {
target_type: isMemberCategory ? 'customer' : 'vehicle',
target_uuid: newReminderAssociatedEntity?.value,
target_name: newReminderAssociatedEntity?.label,
rrule: newEventRecurring,
color: isMemberCategory ? 'member_related' : 'vehicle_maintenance',
};
}
EventsService.createNewEvent(data).then(() => {
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then((data) => {
setAllEvents(data.data);
handleClose();
})
});
// If a repeat option is selected, save to calendar_event_recur collection
if (newEventRecurring && currentTab !== 'medicalCalendar') {
const recurData = {
...data,
rrule: newEventRecurring,
start_repeat_date: newRepeatStartDate || newEventStartDateTime,
end_repeat_date: newIndefiniteRepeat ? new Date('2099-12-31') : newRepeatEndDate,
indefinite_repeat: newIndefiniteRepeat,
};
// Remove rrule from main event data — recurring logic goes in calendar_event_recur only
delete recurData.start_time;
delete recurData.stop_time;
EventsService.createEventRecurrence(recurData).then(() => {
Promise.all([
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }),
EventsService.getAllEventRecurrences()
]).then(([eventsRes, recurRes]) => {
setAllEvents(eventsRes.data);
setAllEventRecurrences(recurRes.data || []);
handleClose();
});
});
} else {
// One-time event — save to calendar_event as before (strip rrule)
delete data.rrule;
EventsService.createNewEvent(data).then(() => {
EventsService.getAllEvents({ from: EventsService.formatDate(fromDate), to: EventsService.formatDate(toDate) }).then((data) => {
setAllEvents(data.data);
handleClose();
})
});
}
}
// Helper function to get reminder title label
@@ -1219,6 +1432,39 @@ const getReminderTitleLabel = (value) => {
<option value="FREQ=YEARLY">Yearly</option>
</select>
</div>
{newEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatStartDate}
onChange={setNewRepeatStartDate}
dateFormat="MM/dd/yyyy"
disabled
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatEndDate}
onChange={setNewRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={newIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={newIndefiniteRepeat} onChange={(e) => {
setNewIndefiniteRepeat(e.target.checked);
if (e.target.checked) setNewRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
</>
)}
@@ -1244,14 +1490,56 @@ const getReminderTitleLabel = (value) => {
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">Reason</div>
<input type="text" className="form-control" placeholder="Enter reason" value={newAttendanceReason || ''} onChange={e => setNewAttendanceReason(e.target.value)}/>
</div>
<div className="mb-3">
<div className="field-label">Repeat</div>
<select className="form-control" value={newEventRecurring} onChange={(e) => setNewEventRecurring(e.target.value)}>
<option value="">No (One-time)</option>
<option value="FREQ=DAILY">Daily</option>
<option value="FREQ=WEEKLY">Weekly</option>
<option value="FREQ=MONTHLY">Monthly</option>
</select>
</div>
{newEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Reason</div>
<input type="text" className="form-control" placeholder="Enter reason" value={newAttendanceReason || ''} onChange={e => setNewAttendanceReason(e.target.value)}/>
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatStartDate}
onChange={setNewRepeatStartDate}
dateFormat="MM/dd/yyyy"
disabled
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatEndDate}
onChange={setNewRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={newIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={newIndefiniteRepeat} onChange={(e) => {
setNewIndefiniteRepeat(e.target.checked);
if (e.target.checked) setNewRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
{/* Meal Item Fields */}
</>
)}
{/* Meal Item Fields */}
{currentTab === 'mealPlanCalendar' && (
<>
<div className="mb-3">
@@ -1285,6 +1573,39 @@ const getReminderTitleLabel = (value) => {
<option value="FREQ=MONTHLY">Monthly</option>
</select>
</div>
{newEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatStartDate}
onChange={setNewRepeatStartDate}
dateFormat="MM/dd/yyyy"
disabled
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatEndDate}
onChange={setNewRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={newIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={newIndefiniteRepeat} onChange={(e) => {
setNewIndefiniteRepeat(e.target.checked);
if (e.target.checked) setNewRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
<div className="mb-3">
<div className="field-label">Ingredients</div>
<input type="text" className="form-control" placeholder="Enter ingredients" value={newMealIngredients || ''} onChange={e => setNewMealIngredients(e.target.value)}/>
@@ -1362,6 +1683,39 @@ const getReminderTitleLabel = (value) => {
<option value="FREQ=YEARLY">Yearly</option>
</select>
</div>
{newEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatStartDate}
onChange={setNewRepeatStartDate}
dateFormat="MM/dd/yyyy"
disabled
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={newRepeatEndDate}
onChange={setNewRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={newIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={newIndefiniteRepeat} onChange={(e) => {
setNewIndefiniteRepeat(e.target.checked);
if (e.target.checked) setNewRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
</>
)}
</Modal.Body>
@@ -1437,6 +1791,38 @@ const getReminderTitleLabel = (value) => {
<option value="FREQ=YEARLY">Yearly</option>
</select>
</div>
{editEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatStartDate}
onChange={setEditRepeatStartDate}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatEndDate}
onChange={setEditRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={editIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={editIndefiniteRepeat} onChange={(e) => {
setEditIndefiniteRepeat(e.target.checked);
if (e.target.checked) setEditRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
</>
)}
@@ -1462,14 +1848,55 @@ const getReminderTitleLabel = (value) => {
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">Reason</div>
<input type="text" className="form-control" placeholder="Enter reason" value={editAttendanceReason || ''} onChange={e => setEditAttendanceReason(e.target.value)}/>
</div>
<div className="mb-3">
<div className="field-label">Repeat</div>
<select className="form-control" value={editEventRecurring} onChange={(e) => setEditEventRecurring(e.target.value)}>
<option value="">No (One-time)</option>
<option value="FREQ=DAILY">Daily</option>
<option value="FREQ=WEEKLY">Weekly</option>
<option value="FREQ=MONTHLY">Monthly</option>
</select>
</div>
{editEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Reason</div>
<input type="text" className="form-control" placeholder="Enter reason" value={editAttendanceReason || ''} onChange={e => setEditAttendanceReason(e.target.value)}/>
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatStartDate}
onChange={setEditRepeatStartDate}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatEndDate}
onChange={setEditRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={editIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={editIndefiniteRepeat} onChange={(e) => {
setEditIndefiniteRepeat(e.target.checked);
if (e.target.checked) setEditRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
</>
)}
{/* Meal Item Edit Fields */}
{/* Meal Item Edit Fields */}
{currentTab === 'mealPlanCalendar' && (
<>
<div className="mb-3">
@@ -1503,6 +1930,38 @@ const getReminderTitleLabel = (value) => {
<option value="FREQ=MONTHLY">Monthly</option>
</select>
</div>
{editEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatStartDate}
onChange={setEditRepeatStartDate}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatEndDate}
onChange={setEditRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={editIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={editIndefiniteRepeat} onChange={(e) => {
setEditIndefiniteRepeat(e.target.checked);
if (e.target.checked) setEditRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
<div className="mb-3">
<div className="field-label">Ingredients</div>
<input type="text" className="form-control" placeholder="Enter ingredients" value={editMealIngredients || ''} onChange={e => setEditMealIngredients(e.target.value)}/>
@@ -1580,6 +2039,38 @@ const getReminderTitleLabel = (value) => {
<option value="FREQ=YEARLY">Yearly</option>
</select>
</div>
{editEventRecurring && (
<>
<div className="mb-3">
<div className="field-label">Start Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatStartDate}
onChange={setEditRepeatStartDate}
dateFormat="MM/dd/yyyy"
/>
</div>
<div className="mb-3">
<div className="field-label">End Repeat Date</div>
<DatePicker
className="form-control"
selected={editRepeatEndDate}
onChange={setEditRepeatEndDate}
dateFormat="MM/dd/yyyy"
disabled={editIndefiniteRepeat}
/>
</div>
<div className="mb-3">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={editIndefiniteRepeat} onChange={(e) => {
setEditIndefiniteRepeat(e.target.checked);
if (e.target.checked) setEditRepeatEndDate(new Date('2099-12-31'));
}} />
Indefinite Repeat
</label>
</div>
</>
)}
</>
)}
</Modal.Body>

View File

@@ -36,6 +36,27 @@ const assignTransportationToEvents = (data) => {
return http.post(`/events/assign`, data);
}
// Event Recurrence API
const getAllEventRecurrences = () => {
return http.get('/event-recurrences');
};
const createEventRecurrence = (data) => {
return http.post('/event-recurrences', data);
};
const updateEventRecurrence = (id, data) => {
return http.put(`/event-recurrences/${id}`, data);
};
const disableEventRecurrence = (id, data) => {
return http.put(`/event-recurrences/${id}/disable`, data);
};
const deleteEventRecurrence = (id) => {
return http.delete(`/event-recurrences/${id}`);
};
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
@@ -333,6 +354,12 @@ export const EventsService = {
getTimeData,
getByCustomer,
site,
// Event Recurrence
getAllEventRecurrences,
createEventRecurrence,
updateEventRecurrence,
disableEventRecurrence,
deleteEventRecurrence,
// New option names
languageSupportOptions,
labelOptions,

View File

@@ -235,6 +235,7 @@ require("./app/routes/resource.routes")(app);
require("./app/routes/center-phone.routes")(app);
require("./app/routes/message-token.routes")(app);
require("./app/routes/calendar-event.route")(app);
require("./app/routes/calendar-event-recur.route")(app);
require("./app/routes/doctemplate.route")(app);
require("./app/routes/xlsxtemplate.route")(app);
require("./app/routes/timedata.routes")(app);