Supervisor Calendar Overview basic setup

Basic structure to render and show a calendar for the supervisors. Currently renders static events.
When finished the supervisor will be able to look at an overview of all the 'first meetings' they have
scheduled so far to facilitate easier planning of future first meeting scheduling
This commit is contained in:
Nico Athanassiadis 2025-04-08 12:42:04 +02:00
parent d2e5043c95
commit 4a4aafda51
9 changed files with 403 additions and 0 deletions

@ -233,6 +233,8 @@ public class SciProApplication extends LifecycleManagedWebApplication {
mountPage("supervisor/projectideas/studentideas", SupervisorAllStudentIdeasPage.class);
mountPage("supervisor/projectideas/details", SupervisorIdeaDetailsPage.class);
mountPage("supervisor/schedule", ScheduleOverviewPage.class);
mountPage("supervisor/finalversions", SupervisorFinalThesisListingPage.class);
mountPage("supervisor/project/grading", SupervisorGradingPage.class);

@ -0,0 +1,3 @@
package se.su.dsv.scipro.components.menuhighlighting;
public interface MenuHighLightScheduleOverview extends MenuHighlight {}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<div wicket:id="myCalendarPanel"></div>
</wicket:extend>
</body>
</html>

@ -0,0 +1,21 @@
package se.su.dsv.scipro.supervisor.pages;
import static se.su.dsv.scipro.security.auth.roles.Roles.SUPERVISOR;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.security.auth.Authorization;
import se.su.dsv.scipro.supervisor.panels.MyCalendarPanel;
@Authorization(authorizedRoles = { SUPERVISOR })
public class ScheduleOverviewPage extends AbstractSupervisorPage {
public ScheduleOverviewPage() {
super();
add(new MyCalendarPanel("myCalendarPanel"));
}
public ScheduleOverviewPage(PageParameters pp) {
super(pp);
add(new MyCalendarPanel("myCalendarPanel"));
}
}

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns:wicket="http://wicket.apache.org" xmlns="http://www.w3.org/1999/xhtml">
<body>
<wicket:panel>
<div class="calendar-header-bar">
<div class="calendar-nav-wrapper">
<a wicket:id="prevMonth" class="calendar-nav-button"></a>
</div>
<div class="calendar-title-wrapper">
<span wicket:id="monthLabel" class="calendar-title"></span>
</div>
<div class="calendar-nav-wrapper">
<a wicket:id="nextMonth" class="calendar-nav-button"></a>
</div>
</div>
<div wicket:id="calendarBody">
<div class="calendar-grid">
<div class="calendar-header">Mon</div>
<div class="calendar-header">Tue</div>
<div class="calendar-header">Wed</div>
<div class="calendar-header">Thu</div>
<div class="calendar-header">Fri</div>
<div class="calendar-header">Sat</div>
<div class="calendar-header">Sun</div>
<wicket:container wicket:id="rows">
<div wicket:id="cells" class="calendar-cell">
<div wicket:id="day-number" class="day-number"></div>
<wicket:container wicket:id="calendar-events" />
</div>
</wicket:container>
</div>
</div>
</wicket:panel>
</body>
</html>

@ -0,0 +1,156 @@
package se.su.dsv.scipro.supervisor.panels;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.RepeatingView;
import se.su.dsv.scipro.io.dto.CalendarEvent;
public class MyCalendarPanel extends Panel {
private YearMonth currentMonth;
private WebMarkupContainer calendarBody;
private Label monthLabel;
public MyCalendarPanel(String id) {
this(id, YearMonth.now());
}
public MyCalendarPanel(String id, YearMonth initialMonth) {
super(id);
this.currentMonth = initialMonth;
setOutputMarkupId(true);
// Månadstitel
monthLabel = new Label("monthLabel", () -> currentMonth.getMonth().name() + " " + currentMonth.getYear());
monthLabel.setOutputMarkupId(true);
add(monthLabel);
// Navigeringsknappar
add(
new AjaxLink<Void>("prevMonth") {
@Override
public void onClick(AjaxRequestTarget target) {
currentMonth = currentMonth.minusMonths(1);
updateCalendar(target);
}
}
);
add(
new AjaxLink<Void>("nextMonth") {
@Override
public void onClick(AjaxRequestTarget target) {
currentMonth = currentMonth.plusMonths(1);
updateCalendar(target);
}
}
);
// Kalenderns innehåll
calendarBody = new WebMarkupContainer("calendarBody");
calendarBody.setOutputMarkupId(true);
add(calendarBody);
renderCalendar(calendarBody);
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(CssHeaderItem.forUrl("css/my_calendar.css"));
response.render(JavaScriptHeaderItem.forUrl("js/my_calendar.js"));
}
private void updateCalendar(AjaxRequestTarget target) {
renderCalendar(calendarBody);
target.add(calendarBody);
target.add(monthLabel);
}
private void renderCalendar(WebMarkupContainer container) {
container.removeAll();
RepeatingView rows = new RepeatingView("rows");
LocalDate firstDayOfMonth = currentMonth.atDay(1);
LocalDate lastDayOfMonth = currentMonth.atEndOfMonth();
List<LocalDate> days = new ArrayList<>();
for (LocalDate date = firstDayOfMonth; !date.isAfter(lastDayOfMonth); date = date.plusDays(1)) {
days.add(date);
}
List<MyCalendarEvent> events = generateSampleEvents(currentMonth);
DayOfWeek firstDayOfWeek = DayOfWeek.MONDAY;
int offset = (firstDayOfMonth.getDayOfWeek().getValue() - firstDayOfWeek.getValue() + 7) % 7;
for (int i = 0; i < offset; i++) {
WebMarkupContainer empty = new WebMarkupContainer(rows.newChildId());
empty.add(createEmptyCell());
rows.add(empty);
}
for (LocalDate day : days) {
WebMarkupContainer rowItem = new WebMarkupContainer(rows.newChildId());
WebMarkupContainer cell = new WebMarkupContainer("cells");
cell.add(new AttributeModifier("class", "calendar-cell"));
Label dayLabel = new Label("day-number", String.valueOf(day.getDayOfMonth()));
String dayClass = day.equals(LocalDate.now()) ? "day-number today" : "day-number";
dayLabel.add(new AttributeModifier("class", dayClass));
cell.add(dayLabel);
RepeatingView eventView = new RepeatingView("calendar-events");
for (MyCalendarEvent event : events) {
if (event.date.equals(day)) {
Label eventLabel = new Label(eventView.newChildId(), event.description());
eventLabel.add(new AttributeModifier("class", "calendar-event"));
eventView.add(eventLabel);
}
}
cell.add(eventView);
rowItem.add(cell);
rows.add(rowItem);
}
container.add(rows);
}
private WebMarkupContainer createEmptyCell() {
WebMarkupContainer cell = new WebMarkupContainer("cells");
cell.add(new Label("day-number", ""));
cell.add(new RepeatingView("calendar-events"));
cell.add(new AttributeModifier("class", "calendar-cell"));
return cell;
}
private static List<MyCalendarEvent> generateSampleEvents(YearMonth yearMonth) {
List<MyCalendarEvent> events = new ArrayList<>();
events.add(new MyCalendarEvent("event2", yearMonth.atDay(8), "Event 2"));
events.add(new MyCalendarEvent("event1", yearMonth.atDay(3), "Event 1"));
events.add(new MyCalendarEvent("event3", yearMonth.atDay(8), "Event 3"));
events.add(new MyCalendarEvent("event4", yearMonth.atDay(10), "Event 4"));
events.add(new MyCalendarEvent("event5", yearMonth.atDay(16), "Event 5"));
events.add(new MyCalendarEvent("event5", yearMonth.atDay(16), "Event 6"));
events.add(new MyCalendarEvent("event5", yearMonth.atDay(16), "Event 7"));
events.add(new MyCalendarEvent("event5", yearMonth.atDay(16), "Event 8"));
events.add(new MyCalendarEvent("event5", yearMonth.atDay(16), "Event 9"));
return events;
}
record MyCalendarEvent(String id, LocalDate date, String description) {}
}

@ -5,12 +5,14 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.Page;
import se.su.dsv.scipro.components.AbstractMenuPanel;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighLightScheduleOverview;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorAllProjects;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyGroups;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyProjects;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorProjectIdea;
import se.su.dsv.scipro.match.SupervisorIdeaStartPage;
import se.su.dsv.scipro.supervisor.pages.AbstractSupervisorPage;
import se.su.dsv.scipro.supervisor.pages.ScheduleOverviewPage;
import se.su.dsv.scipro.supervisor.pages.SupervisorAllProjectsPage;
import se.su.dsv.scipro.supervisor.pages.SupervisorMyGroupsPage;
import se.su.dsv.scipro.supervisor.pages.SupervisorStartPage;
@ -37,6 +39,7 @@ public class SupervisorTabMenuPanel extends AbstractMenuPanel {
items.add(
new MenuItem("All projects", SupervisorAllProjectsPage.class, MenuHighlightSupervisorAllProjects.class)
);
items.add(new MenuItem("Schedule Overview", ScheduleOverviewPage.class, MenuHighLightScheduleOverview.class));
return items;
}

@ -0,0 +1,143 @@
/* Optional wrapper for full border around calendar */
.calendar-grid-wrapper {
border: 1px solid #ccc;
overflow: hidden;
}
/* Core grid layout: 7 days */
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
background-color: transparent; /* removes gray gaps */
}
/* Headers */
.calendar-header {
background-color: #002F5F;
color: #FFFFFF;
font-weight: bold;
text-align: center;
padding: 6px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
box-sizing: border-box;
}
/* Each calendar day cell */
.calendar-cell {
background: white;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
padding: 4px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-start;
height: 100%;
}
/* Top and left borders added only to first row/column */
.calendar-grid > div:nth-child(7n + 1) {
border-left: 1px solid #ccc; /* First column */
}
.calendar-grid > div:nth-child(-n+7) {
border-top: 1px solid #ccc; /* First row */
}
/* Day number in the top-left */
.day-number {
font-weight: bold;
font-size: 0.85em;
color: #333;
}
/* Today indicator a la google style */
.day-number.today {
background-color: #007acc;
color: #fff;
padding: 6px;
width: 28px;
height: 28px;
line-height: 16px; /* or tweak based on font-size */
text-align: center;
border-radius: 50%;
display: inline-block;
font-size: 0.85em;
box-sizing: border-box;
}
/* Event styling */
.calendar-event {
display: block;
padding: 2px 4px;
font-size: 0.75em;
background-color: #a5c995;
border-left: 3px solid #007acc;
border-radius: 2px;
color: #1a1a1a;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
/* Spacing between events */
.calendar-event:first-child {
margin-top: 1.5em; /* space below day number */
}
.calendar-event:not(:first-child) {
margin-top: 0.2em;
}
/* Navigering */
.calendar-header-bar {
display: flex;
justify-content: space-between;
grid-template-columns: 1fr auto 1fr;
align-items: center;
background-color: #002F5F;
padding: 8px 16px;
}
/* Se till att navigationsknappar hamnar korrekt */
.calendar-nav-wrapper {
display: flex;
align-items: center;
}
.calendar-nav-wrapper:first-child {
justify-content: flex-start;
padding-left: 0.5rem;
}
.calendar-nav-wrapper:last-child {
justify-content: flex-end;
padding-right: 0.5rem;
}
.calendar-nav-button {
color: white;
border: none;
padding: 8px 12px;
font-size: 1em;
cursor: pointer;
border-radius: 4px;
margin: 0 5px;
}
/* Centrera månadstiteln i mittenkolumnen */
.calendar-title-wrapper {
display: flex;
justify-content: center;
}
.calendar-title {
font-size: 1.3em;
font-weight: bold;
color: white;
}

@ -0,0 +1,28 @@
console.log("This is my.js");
function normalizeCalendarHeights() {
const cells = document.querySelectorAll('.calendar-cell');
let maxHeight = 0;
if (cells.length === 0) return;
cells.forEach(cell => {
cell.style.height = 'auto';
const height = cell.scrollHeight;
if (height > maxHeight) maxHeight = height;
});
cells.forEach(cell => {
cell.style.height = maxHeight + 'px';
});
}
// Run once when the full page loads
window.addEventListener('load', normalizeCalendarHeights);
// ✨ Run after every Wicket AJAX update too — even if you add AJAX later
if (typeof Wicket !== 'undefined') {
Wicket.Event.subscribe('/ajax/call/complete', function() {
normalizeCalendarHeights();
});
}