diff --git a/_posts/student_toolkit/ProductivityFrontend/calendar/2024-12-16-calendar.md b/_posts/student_toolkit/ProductivityFrontend/calendar/2024-12-16-calendar.md
index 73e57ee9e..5f92c19da 100644
--- a/_posts/student_toolkit/ProductivityFrontend/calendar/2024-12-16-calendar.md
+++ b/_posts/student_toolkit/ProductivityFrontend/calendar/2024-12-16-calendar.md
@@ -57,7 +57,17 @@ active_tab: calendar
.fc .fc-day-today {
background-color: color-mix(in srgb, var(--accent) 20%, var(--bg-0) 80%) !important;
}
+/* Break day highlighting */
+.fc-event-break {
+ background-color: #2a2a2a !important;
+ border-color: transparent !important;
+ border: none !important;
+ box-shadow: none !important;
+}
+.fc-daygrid-day.break-day {
+ background-color: #2a2a2a !important;
+}
/* Priority-based event colors - saturated colors with white text */
.fc-event.priority-p0 {
background-color: #dc2626 !important; /* Red 600 */
@@ -130,6 +140,7 @@ active_tab: calendar
+
@@ -146,6 +157,26 @@ active_tab: calendar
let currentEvent = null;
let isAddingNewEvent = false;
let calendar;
+ function isBreakDay(dateString) {
+ // Check if the given date is a break day by looking at allEvents
+ const breakEvent = allEvents.find(event => {
+ const isBreak = (event.extendedProps && event.extendedProps.isBreak === true) || event.isBreak === true;
+ const dateMatch = formatDate(event.start) === dateString;
+ console.log(`Checking event: ${event.title}, isBreak: ${isBreak}, dateMatch: ${dateMatch}, eventDate: ${event.start}, checkDate: ${dateString}`);
+ return isBreak && dateMatch;
+ });
+ console.log(`isBreakDay(${dateString}): ${breakEvent ? 'YES' : 'NO'}`, breakEvent);
+ return !!breakEvent;
+ }
+ function getBreakName(dateString) {
+ // Get the break name for a given date (robust for either top-level or extendedProps storage)
+ const breakEvent = allEvents.find(event => {
+ const isBreak = (event.extendedProps && event.extendedProps.isBreak === true) || event.isBreak === true;
+ return isBreak && formatDate(event.start) === dateString;
+ });
+ if (!breakEvent) return null;
+ return (breakEvent.extendedProps && breakEvent.extendedProps.breakName) || breakEvent.breakName || breakEvent.title || null;
+ }
function request() {
return fetch(`${javaURI}/api/calendar/events`, fetchOptions)
.then(response => {
@@ -160,9 +191,33 @@ active_tab: calendar
return null;
});
}
+ // getAssignments removed - assignments are no longer fetched here
+ function getBreaks() {
+ // Try primary endpoint, then fallback to trailing slash if 404 (some servers require it)
+ return fetch(`${javaURI}/api/calendar/breaks`, fetchOptions)
+ .then(response => {
+ if (response.status === 404) {
+ console.warn('Breaks endpoint returned 404, retrying with trailing slash');
+ return fetch(`${javaURI}/api/calendar/breaks/`, fetchOptions);
+ }
+ return response;
+ })
+ .then(response => {
+ if (!response || response.status !== 200) {
+ console.error("HTTP status code for breaks: " + (response && response.status));
+ return [];
+ }
+ return response.json();
+ })
+ .catch(error => {
+ console.error("Fetch error for breaks: ", error);
+ return [];
+ });
+ }
function handleRequest() {
- request()
- .then((calendarEvents) => {
+ Promise.all([request(), getBreaks()])
+ .then(([calendarEvents, breaks]) => {
+ console.log("handleRequest - All data loaded. Breaks:", breaks);
allEvents = []; // Reset allEvents
if (calendarEvents !== null) {
calendarEvents.forEach(event => {
@@ -170,14 +225,12 @@ active_tab: calendar
// Extract priority from title if present (format: [P0], [P1], [P2], [P3])
let priority = event.priority || 'P2';
let displayTitle = event.title || '';
-
// Check if title starts with priority tag like [P0], [P1], etc.
const priorityMatch = displayTitle.match(/^\[(P[0-3])\]\s*/);
if (priorityMatch) {
priority = priorityMatch[1];
displayTitle = displayTitle.replace(/^\[(P[0-3])\]\s*/, ''); // Remove priority tag from display
}
-
// Priority-based colors (saturated for white text)
const priorityColors = {
'P0': '#dc2626', // Red 600
@@ -185,9 +238,7 @@ active_tab: calendar
'P2': '#ca8a04', // Yellow 600
'P3': '#16a34a' // Green 600
};
-
let color = priorityColors[priority] || "#808080";
-
// Fall back to class-based colors if no priority detected
if (!priorityMatch && !event.priority) {
if (event.class == "CSP") {
@@ -196,15 +247,16 @@ active_tab: calendar
color = "#008000";
}
}
-
allEvents.push({
id: event.id,
period: event.period || null,
priority: priority,
title: displayTitle.replace(/\(P[13]\)/gi, ""),
description: event.description,
- start: event.date,
+ // Normalize stored start to YYYY-MM-DD to avoid timezone parsing as UTC
+ start: formatDate(event.date),
color: color,
+ isBreak: false,
classNames: [`priority-${priority.toLowerCase()}`]
});
} catch (err) {
@@ -212,6 +264,40 @@ active_tab: calendar
}
});
}
+ // assignments removed from frontend; no processing here
+ if (breaks && breaks.length > 0) {
+ console.log("Breaks loaded:", breaks);
+ breaks.forEach(breakItem => {
+ try {
+ const breakEvent = {
+ id: breakItem.id,
+ // Title kept for compatibility but primary name stored in extendedProps.breakName
+ title: `Break: ${breakItem.name || 'Break'}`,
+ description: breakItem.description || breakItem.name || 'Break',
+ // Normalize break date to YYYY-MM-DD local representation
+ start: formatDate(breakItem.date),
+ backgroundColor: "#2a2a2a",
+ borderColor: "#1a1a1a",
+ textColor: "#ffffff",
+ // Mark consistently on both extendedProps and top-level for different code paths
+ isBreak: true,
+ breakName: breakItem.name || 'Break',
+ extendedProps: {
+ isBreak: true,
+ breakName: breakItem.name || 'Break',
+ description: breakItem.description || ''
+ },
+ classNames: ['fc-event-break']
+ };
+ console.log("Adding break event:", breakEvent);
+ allEvents.push(breakEvent);
+ } catch (err) {
+ console.error("Error loading break:", breakItem, err);
+ }
+ });
+ } else {
+ console.log("No breaks found");
+ }
displayCalendar(filterEventsByClass(currentFilter)); // Display filtered events
});
}
@@ -262,12 +348,30 @@ active_tab: calendar
dayGridWeek: { buttonText: 'Week' },
dayGridDay: { buttonText: 'Day' }
},
+ // Highlight break days (no text in the cell; the break event shows the name)
+ dayCellDidMount: function(arg) {
+ try {
+ const dateStr = formatDate(arg.date);
+ if (isBreakDay(dateStr)) {
+ arg.el.classList.add('break-day');
+ } else {
+ arg.el.classList.remove('break-day');
+ }
+ } catch (e) {
+ console.error('dayCellDidMount error:', e);
+ }
+ },
events: events,
eventClick: function (info) {
document.getElementById("saveButton").style.display = "none";
+ document.getElementById("makeBreakButton").style.display = "none";
currentEvent = info.event;
+ // When an existing event is clicked, this is not an 'add' flow
+ isAddingNewEvent = false;
+ const isBreak = (currentEvent.extendedProps && currentEvent.extendedProps.isBreak === true) || currentEvent.isBreak === true;
+ console.log("Event clicked:", currentEvent.title, "isBreak:", isBreak);
document.getElementById('eventTitle').textContent = currentEvent.title;
- document.getElementById('editTitle').innerHTML = currentEvent.title;
+ document.getElementById('editTitle').innerHTML = isBreak ? ((currentEvent.extendedProps && currentEvent.extendedProps.breakName) || currentEvent.breakName || currentEvent.title) : currentEvent.title;
document.getElementById('editDescription').innerHTML = slackToHtml(currentEvent.extendedProps.description || "");
document.getElementById('editDateDisplay').textContent = formatDisplayDate(currentEvent.start);
document.getElementById('editDate').value = formatDate(currentEvent.start);
@@ -276,10 +380,27 @@ active_tab: calendar
document.getElementById("editPriority").value = currentEvent.extendedProps.priority || "P2";
document.getElementById("editPriority").disabled = true;
document.getElementById("eventModal").style.display = "block";
- document.getElementById("deleteButton").style.display = "inline-block";
- document.getElementById("editButton").style.display = "inline-block";
+ // Check if this is a break event
+ if (isBreak) {
+ // For break events, show edit and delete buttons
+ document.getElementById("deleteButton").style.display = "inline-block";
+ document.getElementById("editButton").style.display = "inline-block";
+ document.getElementById("makeBreakButton").style.display = "none";
+ document.getElementById("eventModal").dataset.isBreak = "true";
+ } else {
+ // For regular events, show edit and delete buttons
+ document.getElementById("deleteButton").style.display = "inline-block";
+ document.getElementById("editButton").style.display = "inline-block";
+ document.getElementById("eventModal").dataset.isBreak = "false";
+ }
},
dateClick: function (info) {
+ const selectedDate = formatDate(info.date);
+ // Check if this date is a break day
+ if (isBreakDay(selectedDate)) {
+ alert(`There is already a break on ${formatDisplayDate(info.date)}`);
+ return;
+ }
isAddingNewEvent = true;
document.getElementById("eventTitle").textContent = "Add New Event";
document.getElementById("editTitle").innerHTML = "";
@@ -290,11 +411,12 @@ active_tab: calendar
document.getElementById("editPriority").disabled = false; // Enable priority dropdown for new events
document.getElementById("editPriority").value = "P2"; // Default to medium priority
document.getElementById('editDateDisplay').textContent = formatDisplayDate(info.date);
- document.getElementById('editDate').value = formatDate(info.date);
+ document.getElementById('editDate').value = selectedDate;
document.getElementById("eventModal").style.display = "block";
document.getElementById("deleteButton").style.display = "none";
document.getElementById("editButton").style.display = "none";
document.getElementById("saveButton").style.display = "inline-block";
+ document.getElementById("makeBreakButton").style.display = "inline-block";
document.getElementById("saveButton").onclick = function () {
const updatedTitle = document.getElementById("editTitle").innerHTML.trim();
const updatedDescription = document.getElementById("editDescription").innerHTML;
@@ -367,19 +489,36 @@ active_tab: calendar
function filterEventsByClass(className) {
let filtered = allEvents;
if (className) {
- filtered = allEvents.filter(event => event.period === className);
+ // Include break events regardless of filter, plus filtered regular events
+ filtered = allEvents.filter(event => {
+ const isBreak = event.extendedProps && event.extendedProps.isBreak === true;
+ return isBreak || event.period === className;
+ });
}
- // Sort by priority (P0 first, then P1, P2, P3)
+ // Sort by priority (P0 first, then P1, P2, P3), breaks are not prioritized
return filtered.sort((a, b) => {
+ // Breaks always come first
+ const aIsBreak = a.extendedProps && a.extendedProps.isBreak === true;
+ const bIsBreak = b.extendedProps && b.extendedProps.isBreak === true;
+ if (aIsBreak && !bIsBreak) return -1;
+ if (!aIsBreak && bIsBreak) return 1;
+ if (aIsBreak && bIsBreak) return 0;
+ // For non-break events, sort by priority
const priorityOrder = { 'P0': 0, 'P1': 1, 'P2': 2, 'P3': 3 };
const aPriority = priorityOrder[a.priority] ?? 2;
const bPriority = priorityOrder[b.priority] ?? 2;
return aPriority - bPriority;
});
}
- function formatDate(dateString) {
- const date = new Date(dateString);
- return date.toISOString().split("T")[0];
+ function formatDate(dateInput) {
+ // If already a YYYY-MM-DD string, return as-is to avoid UTC parsing issues
+ if (!dateInput && dateInput !== 0) return '';
+ if (typeof dateInput === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateInput)) return dateInput;
+ const d = (dateInput instanceof Date) ? dateInput : new Date(dateInput);
+ const year = d.getFullYear();
+ const month = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
}
document.getElementById("closeModal").onclick = function () {
document.getElementById('editDateDisplay').style.display = 'block';
@@ -392,146 +531,207 @@ active_tab: calendar
document.getElementById("eventModal").style.display = "none";
};
document.getElementById("saveButton").onclick = function () {
- const updatedPeriod = document.getElementById("editPeriod").value;
- const updatedPriority = document.getElementById("editPriority").value;
+ const isBreak = document.getElementById("eventModal").dataset.isBreak === "true";
const updatedTitle = document.getElementById("editTitle").innerHTML.trim();
- const updatedDescription = document.getElementById("editDescription").innerHTML;
- const updatedDate = document.getElementById("editDate").value;
-
- // Priority-based colors
- const priorityColors = {
- 'P0': '#fecaca', 'P1': '#fed7aa', 'P2': '#fef08a', 'P3': '#bbf7d0'
- };
-
+ const updatedDescription = document.getElementById("editDescription").innerHTML;
+ // Reset UI state
document.getElementById("saveButton").style.display = "none";
document.getElementById('editDateDisplay').style.display = 'block';
document.getElementById('editDate').style.display = 'none';
- document.getElementById('editDateDisplay').textContent = formatDisplayDate(new Date(updatedDate));
document.getElementById("editDescription").contentEditable = false;
+ document.getElementById("editTitle").contentEditable = false;
document.getElementById("editPeriod").disabled = true;
- document.getElementById("editPriority").disabled = true;
- if (!updatedTitle || !updatedDescription || !updatedDate) {
- alert("Title, Description, and Date cannot be empty!");
+ document.getElementById("editPriority").disabled = true;
+ if (!updatedTitle || !updatedDescription) {
+ alert("Title and Description cannot be empty!");
return;
- }
- if (isAddingNewEvent) {
- const newEventPayload = {
- title: updatedTitle,
- description: updatedDescription,
- date: updatedDate,
- period: updatedPeriod, // Event class (CSA, CSP, CSSE)
- priority: updatedPriority
- };
- console.log(updatedPeriod);
- fetch(`${javaURI}/api/calendar/add_event`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(newEventPayload),
- })
- .then(response => {
- if (!response.ok) {
- throw new Error(`Failed to add new event: ${response.status} ${response.statusText}`);
- }
- return response.json(); // Parse the response JSON if needed
- })
- .then(newEvent => {
- calendar.addEvent({
- id: newEvent.id,
- title: newEvent.title,
- start: newEvent.date,
- description: newEvent.description,
- priority: updatedPriority,
- color: priorityColors[updatedPriority] || "#808080",
- classNames: [`priority-${updatedPriority.toLowerCase()}`]
- });
- document.getElementById("eventModal").style.display = "none";
- })
- .catch(error => {
- console.warn("Error adding event to Slack:", error);
- alert("This event has been added to the calendar but could not be updated in Slack.");
- calendar.addEvent({
- title: updatedTitle,
- start: updatedDate,
- description: updatedDescription,
- priority: updatedPriority,
- color: priorityColors[updatedPriority] || "#808080",
- classNames: [`priority-${updatedPriority.toLowerCase()}`]
- });
- document.getElementById("eventModal").style.display = "none";
- });
- } else {
- const payload = { newTitle: updatedTitle, description: updatedDescription, date: updatedDate, period: updatedPeriod, priority: updatedPriority };
+ }
+ if (isBreak) {
+ // Handle break editing
const id = currentEvent.id;
- fetch(`${javaURI}/api/calendar/edit/${id}`, {
+ const breakPayload = {
+ name: updatedTitle,
+ description: updatedDescription
+ };
+ fetch(`${javaURI}/api/calendar/breaks/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload),
+ body: JSON.stringify(breakPayload),
})
.then(response => {
if (!response.ok) {
- throw new Error(`Failed to update event: ${response.status} ${response.statusText}`);
+ throw new Error(`Failed to update break: ${response.status} ${response.statusText}`);
}
- return response.text();
+ return response.json();
})
.then(() => {
- currentEvent.setProp("title", updatedTitle);
- currentEvent.setExtendedProp("description", updatedDescription);
- currentEvent.setStart(updatedDate);
- currentEvent.setExtendedProp("period", updatedPeriod);
- currentEvent.setExtendedProp("priority", updatedPriority);
- currentEvent.setProp("color", priorityColors[updatedPriority] || "#808080");
document.getElementById("eventModal").style.display = "none";
- // Refresh calendar to apply new priority styling
handleRequest();
})
.catch(error => {
- console.warn("Error updating event in Slack:", error);
- alert("This event has been updated in the calendar but could not be updated in Slack.");
- currentEvent.setProp("title", updatedTitle);
- currentEvent.setExtendedProp("description", updatedDescription);
- currentEvent.setStart(updatedDate);
- currentEvent.setExtendedProp("priority", updatedPriority);
- currentEvent.setProp("color", priorityColors[updatedPriority] || "#808080");
- document.getElementById("eventModal").style.display = "none";
- handleRequest();
+ console.error("Error updating break:", error);
+ alert("Failed to update break. Please try again.\n\nError: " + error.message);
});
+ } else {
+ // Handle regular event editing
+ const updatedPeriod = document.getElementById("editPeriod").value;
+ const updatedPriority = document.getElementById("editPriority").value;
+ const updatedDate = document.getElementById("editDate").value;
+ const priorityColors = {
+ 'P0': '#fecaca', 'P1': '#fed7aa', 'P2': '#fef08a', 'P3': '#bbf7d0'
+ };
+ document.getElementById('editDateDisplay').textContent = formatDisplayDate(new Date(updatedDate));
+ if (!updatedDate) {
+ alert("Date cannot be empty!");
+ return;
+ }
+ if (isAddingNewEvent) {
+ const newEventPayload = {
+ title: updatedTitle,
+ description: updatedDescription,
+ date: updatedDate,
+ period: updatedPeriod,
+ priority: updatedPriority
+ };
+ fetch(`${javaURI}/api/calendar/add_event`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(newEventPayload),
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Failed to add new event: ${response.status} ${response.statusText}`);
+ }
+ return response.json();
+ })
+ .then(() => {
+ document.getElementById("eventModal").style.display = "none";
+ handleRequest();
+ })
+ .catch(error => {
+ console.error("Error adding event:", error);
+ alert("Failed to add event. Please try again.\n\nError: " + error.message);
+ });
+ } else {
+ const payload = { newTitle: updatedTitle, description: updatedDescription, date: updatedDate, period: updatedPeriod, priority: updatedPriority };
+ const id = currentEvent.id;
+ fetch(`${javaURI}/api/calendar/edit/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Failed to update event: ${response.status} ${response.statusText}`);
+ }
+ return response.text();
+ })
+ .then(() => {
+ document.getElementById("eventModal").style.display = "none";
+ handleRequest();
+ })
+ .catch(error => {
+ console.error("Error updating event:", error);
+ alert("Failed to update event. Please try again.\n\nError: " + error.message);
+ });
+ }
}
};
document.getElementById("editButton").onclick = function () {
+ const isBreak = document.getElementById("eventModal").dataset.isBreak === "true";
document.getElementById('editDateDisplay').style.display = 'none';
- document.getElementById('editDate').style.display = 'block';
+ document.getElementById('editDate').style.display = isBreak ? 'none' : 'block';
document.getElementById("deleteButton").style.display = 'none';
document.getElementById("saveButton").style.display = 'inline-block';
document.getElementById("editDescription").contentEditable = true;
document.getElementById("editTitle").contentEditable = true;
- document.getElementById("editPeriod").disabled = false; // Enable only on edit
- document.getElementById("editPriority").disabled = false; // Enable priority on edit
+ // Editing an existing event should not create a new one
+ isAddingNewEvent = false;
+ if (!isBreak) {
+ document.getElementById("editPeriod").disabled = false;
+ document.getElementById("editPriority").disabled = false;
+ }
document.getElementById("editDescription").innerHTML = currentEvent.extendedProps.description || "";
};
document.getElementById("deleteButton").onclick = function () {
if (!currentEvent) return;
+ const isBreak = document.getElementById("eventModal").dataset.isBreak === "true";
const id = currentEvent.id;
const confirmation = confirm(`Are you sure you want to delete "${currentEvent.title}"?`);
if (!confirmation) return;
- fetch(`${javaURI}/api/calendar/delete/${id}`, {
+ const endpoint = isBreak ? `${javaURI}/api/calendar/breaks/${id}` : `${javaURI}/api/calendar/delete/${id}`;
+ fetch(endpoint, {
method: "DELETE",
headers: { "Content-Type": "application/json" }
})
.then(response => {
if (!response.ok) {
- throw new Error(`Failed to delete event: ${response.status} ${response.statusText}`);
+ throw new Error(`Failed to delete: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then(() => {
currentEvent.remove();
document.getElementById("eventModal").style.display = "none";
+ handleRequest();
})
.catch(error => {
- console.error("Error deleting event:", error);
- alert("This event has been removed from the calendar but could not be deleted from Slack.");
- currentEvent.remove();
+ console.error("Error deleting:", error);
+ alert("Failed to delete. Please try again.\n\nError: " + error.message);
+ });
+ };
+ document.getElementById("makeBreakButton").onclick = function () {
+ const breakDate = document.getElementById("editDate").value;
+ const breakTitle = document.getElementById("editTitle").innerHTML.trim();
+ const breakDescription = document.getElementById("editDescription").innerHTML;
+ console.log("Break creation - Title:", breakTitle, "Date:", breakDate, "Description:", breakDescription);
+ if (!breakDate) {
+ alert("Please select a date for the break!");
+ return;
+ }
+ if (!breakTitle) {
+ alert("Please enter a name for the break!");
+ return;
+ }
+ // Check if a break already exists on this date
+ if (isBreakDay(breakDate)) {
+ alert(`There is already a break on ${formatDisplayDate(new Date(breakDate.split('-').map(Number)[0], breakDate.split('-').map(Number)[1] - 1, breakDate.split('-').map(Number)[2]))}`);
+ return;
+ }
+ // Parse date string safely to avoid timezone issues
+ const [year, month, day] = breakDate.split('-').map(Number);
+ const localDate = new Date(year, month - 1, day);
+ const confirmation = confirm(`Are you sure you want to make ${formatDisplayDate(localDate)} a break day with the name "${breakTitle}"? Events on this day will be moved to the next non-break day.`);
+ if (!confirmation) return;
+ const breakPayload = {
+ date: breakDate,
+ name: breakTitle,
+ description: breakDescription,
+ moveToNextNonBreakDay: true
+ };
+ console.log("Sending break payload:", breakPayload);
+ fetch(`${javaURI}/api/calendar/breaks/create`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(breakPayload),
+ })
+ .then(response => {
+ if (!response.ok) {
+ return response.text().then(text => {
+ throw new Error(`Failed to create break: ${response.status} ${response.statusText} - ${text}`);
+ });
+ }
+ return response.json();
+ })
+ .then((result) => {
+ console.log("Break creation response:", result);
+ alert("Break day created successfully. Events on this day have been moved to the next non-break day.");
document.getElementById("eventModal").style.display = "none";
+ handleRequest(); // Refresh the calendar
+ })
+ .catch(error => {
+ console.error("Error creating break:", error);
+ alert("Failed to create break day. Please try again.\n\nError: " + error.message);
});
};
handleRequest();
@@ -621,4 +821,4 @@ active_tab: calendar
day: 'numeric'
});
}
-
+
\ No newline at end of file