Files
alrahma_sunday_school_api/resources/views/teacher/calendar.blade.php
T
2026-04-25 00:00:23 -04:00

664 lines
21 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Teacher Calendar</title>
<style>
:root {
--bg: #f4efe6;
--panel: #fffaf2;
--panel-strong: #f0e3ce;
--ink: #1d2a39;
--muted: #6b7280;
--accent: #0f766e;
--accent-soft: #d4f4ef;
--line: #d8ccb8;
--shadow: 0 18px 45px rgba(38, 32, 24, 0.12);
--today: #f59e0b;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Georgia, "Times New Roman", serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(15, 118, 110, 0.15), transparent 35%),
radial-gradient(circle at top right, rgba(245, 158, 11, 0.18), transparent 28%),
linear-gradient(180deg, #f7f1e7 0%, #efe7da 100%);
}
.shell {
max-width: 1400px;
margin: 0 auto;
padding: 32px 20px 48px;
}
.hero {
display: grid;
gap: 18px;
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr);
align-items: end;
margin-bottom: 24px;
}
.hero-card,
.summary-card,
.calendar-card,
.agenda-card {
background: rgba(255, 250, 242, 0.92);
border: 1px solid rgba(216, 204, 184, 0.9);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero-card {
border-radius: 28px;
padding: 28px;
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 12px;
}
h1 {
margin: 0 0 8px;
font-size: clamp(2rem, 4vw, 3.6rem);
line-height: 0.94;
font-weight: 700;
}
.hero p,
.summary-meta,
.agenda-empty,
.legend span,
.calendar-note,
.month-caption {
color: var(--muted);
}
.summary-card {
border-radius: 24px;
padding: 22px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.stat {
background: var(--panel-strong);
border-radius: 18px;
padding: 14px;
}
.stat strong {
display: block;
font-size: 1.8rem;
line-height: 1;
margin-bottom: 6px;
}
.layout {
display: grid;
gap: 22px;
grid-template-columns: minmax(0, 1.65fr) minmax(300px, 0.95fr);
}
.calendar-card,
.agenda-card {
border-radius: 28px;
overflow: hidden;
}
.calendar-header,
.agenda-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
padding: 22px 24px;
border-bottom: 1px solid var(--line);
background: rgba(240, 227, 206, 0.45);
}
.calendar-header h2,
.agenda-header h2 {
margin: 0;
font-size: 1.35rem;
}
.calendar-toolbar {
display: flex;
align-items: center;
gap: 10px;
}
button {
appearance: none;
border: 0;
border-radius: 999px;
cursor: pointer;
font: inherit;
}
.nav-button,
.today-button {
padding: 10px 14px;
background: #e8ddcb;
color: var(--ink);
}
.today-button {
background: var(--accent);
color: #fff;
}
.calendar-body {
padding: 18px 18px 22px;
}
.weekdays,
.grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 10px;
}
.weekdays {
padding: 0 6px 12px;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.day {
min-height: 132px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border-radius: 20px;
border: 1px solid rgba(216, 204, 184, 0.85);
background: rgba(255, 255, 255, 0.74);
transition: transform 140ms ease, box-shadow 140ms ease, border-color 140ms ease;
}
.day:hover {
transform: translateY(-2px);
box-shadow: 0 14px 28px rgba(54, 46, 33, 0.08);
}
.day.muted {
opacity: 0.45;
}
.day.today {
border-color: rgba(245, 158, 11, 0.9);
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.2);
}
.day.selected {
border-color: var(--accent);
box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.22);
background: #fffdfa;
}
.day-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 50%;
font-weight: 700;
}
.today .day-number {
background: rgba(245, 158, 11, 0.18);
color: #9a5600;
}
.event-stack {
display: flex;
flex-direction: column;
gap: 6px;
}
.event-pill {
display: block;
padding: 7px 9px;
border-radius: 12px;
font-size: 0.8rem;
line-height: 1.2;
color: #12202f;
background: #dbe7f5;
border-left: 4px solid #8aa6cf;
overflow: hidden;
text-overflow: ellipsis;
}
.event-pill.more {
background: var(--panel-strong);
border-left-color: #9c8360;
color: #5c4730;
}
.legend {
display: flex;
gap: 14px;
flex-wrap: wrap;
margin-top: 16px;
padding: 0 8px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.legend-swatch {
width: 14px;
height: 14px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.agenda-body {
padding: 20px;
}
.agenda-date {
margin: 0 0 14px;
font-size: 1.05rem;
}
.agenda-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.agenda-item {
padding: 16px;
border-radius: 18px;
border: 1px solid rgba(216, 204, 184, 0.85);
background: rgba(255, 255, 255, 0.7);
}
.agenda-item h3 {
margin: 0 0 8px;
font-size: 1rem;
}
.agenda-item p {
margin: 0;
color: var(--muted);
white-space: pre-line;
}
.agenda-tag {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 10px;
color: #5e5c52;
}
.agenda-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: #8aa6cf;
}
.calendar-note {
margin-top: 10px;
font-size: 0.9rem;
}
@media (max-width: 1100px) {
.hero,
.layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.shell {
padding: 16px 12px 28px;
}
.weekdays,
.grid {
gap: 6px;
}
.day {
min-height: 108px;
padding: 10px 8px;
border-radius: 16px;
}
.event-pill {
padding: 6px 7px;
font-size: 0.72rem;
}
.summary-grid {
grid-template-columns: 1fr;
}
.calendar-header,
.agenda-header {
flex-direction: column;
align-items: stretch;
}
}
</style>
</head>
<body>
<div class="shell">
<section class="hero">
<div class="hero-card hero">
<div>
<div class="eyebrow">Teacher Portal</div>
<h1>School Calendar</h1>
<p>Monthly view for teacher-visible events, no-school days, and staff-only items.</p>
</div>
</div>
<aside class="summary-card">
<div class="eyebrow">Current Scope</div>
<div class="summary-meta">School Year {{ $schoolYear }} · Default Semester {{ $defaultSemester }}</div>
<div class="summary-grid">
<div class="stat">
<strong id="stat-total">0</strong>
<span>Total events</span>
</div>
<div class="stat">
<strong id="stat-no-school">0</strong>
<span>No-school days</span>
</div>
<div class="stat">
<strong id="stat-staff">0</strong>
<span>Staff-only events</span>
</div>
</div>
</aside>
</section>
<section class="layout">
<article class="calendar-card">
<div class="calendar-header">
<div>
<h2 id="month-label">Month</h2>
<div class="month-caption" id="month-caption">Teacher schedule overview</div>
</div>
<div class="calendar-toolbar">
<button class="nav-button" id="prev-month" type="button">Prev</button>
<button class="today-button" id="jump-today" type="button">Today</button>
<button class="nav-button" id="next-month" type="button">Next</button>
</div>
</div>
<div class="calendar-body">
<div class="weekdays">
<div>Sun</div>
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
</div>
<div class="grid" id="calendar-grid"></div>
<div class="legend">
<div class="legend-item"><span class="legend-swatch" style="background:#dbe7f5"></span><span>General event</span></div>
<div class="legend-item"><span class="legend-swatch" style="background:#ffc107"></span><span>No school</span></div>
<div class="legend-item"><span class="legend-swatch" style="background:#28a745"></span><span>Staff only</span></div>
<div class="legend-item"><span class="legend-swatch" style="background:#6f42c1"></span><span>Meeting</span></div>
</div>
<div class="calendar-note">Select any day to inspect the event list for that date.</div>
</div>
</article>
<aside class="agenda-card">
<div class="agenda-header">
<h2>Day Agenda</h2>
<div class="month-caption" id="agenda-count">0 events</div>
</div>
<div class="agenda-body">
<h3 class="agenda-date" id="agenda-date"></h3>
<div class="agenda-list" id="agenda-list"></div>
<p class="agenda-empty" id="agenda-empty">No events scheduled for this day.</p>
</div>
</aside>
</section>
</div>
<script>
const rawEvents = @json($events);
const events = rawEvents.map((event) => {
const start = String(event.start || '').slice(0, 10);
return {
...event,
start,
backgroundColor: event.backgroundColor || '#8aa6cf',
};
});
const eventsByDate = events.reduce((map, event) => {
if (!event.start) {
return map;
}
if (!map[event.start]) {
map[event.start] = [];
}
map[event.start].push(event);
return map;
}, {});
Object.values(eventsByDate).forEach((list) => {
list.sort((left, right) => String(left.title || '').localeCompare(String(right.title || '')));
});
const monthLabel = document.getElementById('month-label');
const monthCaption = document.getElementById('month-caption');
const calendarGrid = document.getElementById('calendar-grid');
const agendaDate = document.getElementById('agenda-date');
const agendaList = document.getElementById('agenda-list');
const agendaEmpty = document.getElementById('agenda-empty');
const agendaCount = document.getElementById('agenda-count');
const today = new Date();
const todayKey = toDateKey(today);
const availableDates = Object.keys(eventsByDate).sort();
const initialDate = availableDates.includes(todayKey) ? todayKey : (availableDates[0] || todayKey);
let selectedDate = initialDate;
let visibleMonth = new Date(initialDate + 'T12:00:00');
visibleMonth.setDate(1);
document.getElementById('prev-month').addEventListener('click', () => {
visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() - 1, 1);
renderCalendar();
});
document.getElementById('next-month').addEventListener('click', () => {
visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 1);
renderCalendar();
});
document.getElementById('jump-today').addEventListener('click', () => {
selectedDate = todayKey;
visibleMonth = new Date(today.getFullYear(), today.getMonth(), 1);
renderCalendar();
renderAgenda();
});
function renderCalendar() {
calendarGrid.innerHTML = '';
const year = visibleMonth.getFullYear();
const month = visibleMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const leading = firstDay.getDay();
const totalCells = Math.ceil((leading + lastDay.getDate()) / 7) * 7;
const startDate = new Date(year, month, 1 - leading);
monthLabel.textContent = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(firstDay);
monthCaption.textContent = `${countMonthEvents(year, month)} event${countMonthEvents(year, month) === 1 ? '' : 's'} in view`;
for (let i = 0; i < totalCells; i += 1) {
const cellDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + i);
const dateKey = toDateKey(cellDate);
const dayEvents = eventsByDate[dateKey] || [];
const isCurrentMonth = cellDate.getMonth() === month;
const isToday = dateKey === todayKey;
const isSelected = dateKey === selectedDate;
const cell = document.createElement('button');
cell.type = 'button';
cell.className = `day${isCurrentMonth ? '' : ' muted'}${isToday ? ' today' : ''}${isSelected ? ' selected' : ''}`;
cell.addEventListener('click', () => {
selectedDate = dateKey;
if (!isCurrentMonth) {
visibleMonth = new Date(cellDate.getFullYear(), cellDate.getMonth(), 1);
}
renderCalendar();
renderAgenda();
});
const number = document.createElement('span');
number.className = 'day-number';
number.textContent = String(cellDate.getDate());
cell.appendChild(number);
const stack = document.createElement('div');
stack.className = 'event-stack';
dayEvents.slice(0, 3).forEach((event) => {
const pill = document.createElement('span');
pill.className = 'event-pill';
pill.textContent = event.title || 'Untitled';
pill.style.borderLeftColor = event.backgroundColor || '#8aa6cf';
pill.style.background = withAlpha(event.backgroundColor || '#dbe7f5', 0.18);
stack.appendChild(pill);
});
if (dayEvents.length > 3) {
const more = document.createElement('span');
more.className = 'event-pill more';
more.textContent = `+${dayEvents.length - 3} more`;
stack.appendChild(more);
}
cell.appendChild(stack);
calendarGrid.appendChild(cell);
}
}
function renderAgenda() {
const dayEvents = eventsByDate[selectedDate] || [];
agendaDate.textContent = new Intl.DateTimeFormat('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
}).format(new Date(selectedDate + 'T12:00:00'));
agendaList.innerHTML = '';
agendaCount.textContent = `${dayEvents.length} event${dayEvents.length === 1 ? '' : 's'}`;
agendaEmpty.style.display = dayEvents.length === 0 ? 'block' : 'none';
dayEvents.forEach((event) => {
const item = document.createElement('article');
item.className = 'agenda-item';
const tag = document.createElement('div');
tag.className = 'agenda-tag';
const dot = document.createElement('span');
dot.className = 'agenda-dot';
dot.style.background = event.backgroundColor || '#8aa6cf';
tag.appendChild(dot);
tag.append(document.createTextNode(buildTagLabel(event)));
const title = document.createElement('h3');
title.textContent = event.title || 'Untitled';
const description = document.createElement('p');
description.textContent = event.description || 'No extra details provided.';
item.appendChild(tag);
item.appendChild(title);
item.appendChild(description);
agendaList.appendChild(item);
});
}
function buildTagLabel(event) {
const props = event.extendedProps || {};
if (String(event.id || '').startsWith('pm-')) {
return 'Meeting';
}
if (props.no_school) {
return 'No School';
}
if (props.notify_teacher && props.notify_admin && !props.notify_parent) {
return 'Staff Only';
}
return 'School Event';
}
function countMonthEvents(year, month) {
return events.filter((event) => {
const date = new Date(event.start + 'T12:00:00');
return date.getFullYear() === year && date.getMonth() === month;
}).length;
}
function toDateKey(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function withAlpha(hex, alpha) {
const normalized = hex.replace('#', '');
if (normalized.length !== 6) {
return 'rgba(219, 231, 245, 0.18)';
}
const r = parseInt(normalized.slice(0, 2), 16);
const g = parseInt(normalized.slice(2, 4), 16);
const b = parseInt(normalized.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
document.getElementById('stat-total').textContent = String(events.length);
document.getElementById('stat-no-school').textContent = String(events.filter((event) => event.extendedProps && event.extendedProps.no_school).length);
document.getElementById('stat-staff').textContent = String(events.filter((event) => {
const props = event.extendedProps || {};
return props.notify_teacher && props.notify_admin && !props.notify_parent;
}).length);
renderCalendar();
renderAgenda();
</script>
</body>
</html>