Files
Infosec/public/calendar-view.html
2026-09-13 20:10:17 +01:00

192 lines
8.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calendar View · Infosec</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.cal-head { display:flex; align-items:center; gap:12px; flex-wrap:wrap; }
.cal-head .cal-title { font-weight:800; font-size:18px; letter-spacing:-.01em; margin:0; }
.cal-head .spacer { flex:1; }
.cal-nav { display:flex; align-items:center; gap:6px; }
.cal-legend { display:flex; align-items:center; gap:8px; color:var(--muted); font-size:13px; }
.cal-legend .dot { width:10px; height:10px; border-radius:50%; background:var(--accent); display:inline-block; }
.cal-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:6px; margin-top:14px; }
.cal-dow { text-align:center; font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
color:var(--muted); padding:4px 0; }
.cal-cell { min-height:82px; border:1px solid var(--line); border-radius:10px; padding:8px; background:var(--surface);
display:flex; flex-direction:column; gap:6px; text-align:left; cursor:default; position:relative; }
.cal-cell.blank { border:none; background:transparent; }
.cal-cell .daynum { font-size:13px; font-weight:600; color:var(--text); }
.cal-cell.today .daynum { background:var(--accent); color:#fff; border-radius:50%; width:24px; height:24px;
display:grid; place-items:center; }
.cal-cell.has-events { cursor:pointer; border-color:var(--accent); background:var(--accent-soft); }
.cal-cell.has-events:hover { box-shadow:var(--shadow); }
.cal-cell.selected { outline:2px solid var(--accent); outline-offset:0; }
.cal-count { margin-top:auto; align-self:flex-start; background:var(--accent); color:#fff; border-radius:999px;
font-size:11px; font-weight:700; padding:2px 8px; }
.cal-count .lbl { font-weight:600; opacity:.9; }
.day-panel { margin-top:18px; }
.day-panel h3 { margin:0 0 10px; font-size:15px; }
.ev-row { display:flex; align-items:center; gap:12px; flex-wrap:wrap; border:1px solid var(--line);
border-radius:10px; padding:12px 14px; margin-bottom:10px; background:var(--surface); }
.ev-row .ev-name { font-weight:700; }
.ev-row .ev-dates { color:var(--muted); font-size:13px; }
.ev-row .ev-dates .expired { color:var(--danger); font-weight:600; }
.ev-row .spacer { flex:1; }
.cal-empty { color:var(--muted); font-size:14px; padding:8px 2px; }
@media (max-width:640px){ .cal-cell { min-height:64px; padding:6px; } .cal-cell .daynum { font-size:12px; } }
</style>
</head>
<body>
<script src="/js/app.js"></script>
<script>
(async () => {
const shell = await buildShell('calendar'); if (!shell) return;
const { content } = shell;
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
const DOW = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
let events = []; // { id, name, date(ISO), legal_validity, accessible, access }
let byDate = {}; // iso -> [events]
let view = {}; // { y, m } (m is 0-11)
let selected = null; // iso string or null
let todayIso = null;
const pad = (n) => String(n).padStart(2, '0');
const iso = (y, m, d) => `${y}-${pad(m + 1)}-${pad(d)}`;
const daysInMonth = (y, m) => new Date(y, m + 1, 0).getDate();
// Monday-based index of the 1st of the month (0 = Monday … 6 = Sunday)
const firstIndex = (y, m) => (new Date(y, m, 1).getDay() + 6) % 7;
function accessBadge(ev) {
const a = ev.access || { state: ev.accessible ? 'permanent' : 'denied' };
if (a.state === 'admin') return '<span class="badge badge-approved">Full access</span>';
if (a.state === 'permanent') return '<span class="badge badge-approved">Access granted</span>';
if (a.state === 'timed') return '<span class="badge badge-approved">Access granted (timed)</span>';
return '<span class="badge badge-denied">Access required</span>';
}
async function load() {
const state = await api('/api/data/tree');
// "Events" are folders that carry an Incident Date — each folder is a dated
// item (an incident/recording) shown on its incident day.
events = (state.folders || [])
.filter((f) => f.recorded_date)
.map((f) => ({
id: f.id, name: f.name, date: f.recorded_date, legal_validity: f.legal_validity,
accessible: f.accessible, access: f.access,
}));
byDate = {};
for (const ev of events) (byDate[ev.date] = byDate[ev.date] || []).push(ev);
const t = state.server_time ? new Date(String(state.server_time).replace(' ', 'T') + 'Z') : new Date();
todayIso = iso(t.getFullYear(), t.getMonth(), t.getDate());
view = { y: t.getFullYear(), m: t.getMonth() };
selected = byDate[todayIso] ? todayIso : null;
render();
}
function calGrid() {
const { y, m } = view;
const cells = [];
const lead = firstIndex(y, m);
for (let i = 0; i < lead; i++) cells.push('<div class="cal-cell blank"></div>');
const total = daysInMonth(y, m);
for (let d = 1; d <= total; d++) {
const id = iso(y, m, d);
const list = byDate[id] || [];
const cls = ['cal-cell'];
if (list.length) cls.push('has-events');
if (id === todayIso) cls.push('today');
if (id === selected) cls.push('selected');
const count = list.length
? `<span class="cal-count">${list.length} <span class="lbl">event${list.length > 1 ? 's' : ''}</span></span>`
: '';
const clickable = list.length ? ` data-iso="${id}"` : '';
cells.push(`<div class="${cls.join(' ')}"${clickable}><span class="daynum">${d}</span>${count}</div>`);
}
return `<div class="cal-grid">
${DOW.map((w) => `<div class="cal-dow">${w}</div>`).join('')}
${cells.join('')}
</div>`;
}
function dayPanel() {
if (!selected) {
return '<div class="day-panel"><div class="cal-empty">Select a highlighted day to see the events recorded on it.</div></div>';
}
const list = (byDate[selected] || []).slice().sort((a, b) => a.name.localeCompare(b.name));
const rows = list.map((ev) => {
const expired = ev.legal_validity && ev.legal_validity < todayIso;
const valid = ev.legal_validity
? `<span class="${expired ? 'expired' : ''}">valid to ${esc(fmtDate(ev.legal_validity))}${expired ? ' (expired)' : ''}</span>`
: 'no validity';
const open = ev.accessible
? '<a class="btn btn-sm" href="/view-data.html">Open in View Data</a>'
: '';
return `<div class="ev-row">
<span class="ic">${ICON.folder}</span>
<span class="ev-name">${esc(ev.name)}</span>
<span class="ev-dates">incident ${esc(fmtDate(ev.date))} · ${valid}</span>
<span class="spacer"></span>
${accessBadge(ev)}
${open}
</div>`;
}).join('');
return `<div class="day-panel">
<h3>${esc(fmtDate(selected))}${list.length} event${list.length > 1 ? 's' : ''}</h3>
${rows}
</div>`;
}
function render() {
const monthHasNone = !events.length;
content.innerHTML = `
<div class="page-head">
<h1>Calendar View</h1>
<p>A month-by-month overview of the days that have events. Each event is a folder's Incident Date; highlighted days can be opened to list them.</p>
</div>
<div class="card">
<div class="card-body">
<div class="cal-head">
<div class="cal-nav">
<button class="btn btn-sm" id="calPrev"> Prev</button>
<button class="btn btn-sm" id="calToday">Today</button>
<button class="btn btn-sm" id="calNext">Next </button>
</div>
<h2 class="cal-title">${MONTHS[view.m]} ${view.y}</h2>
<span class="spacer"></span>
<span class="cal-legend"><span class="dot"></span> day with events</span>
</div>
${calGrid()}
${monthHasNone ? '<div class="cal-empty" style="margin-top:14px">No events yet. Folders you can see with an Incident Date will appear here.</div>' : dayPanel()}
</div>
</div>`;
const prev = document.getElementById('calPrev');
const next = document.getElementById('calNext');
const today = document.getElementById('calToday');
if (prev) prev.onclick = () => { view.m -= 1; if (view.m < 0) { view.m = 11; view.y -= 1; } render(); };
if (next) next.onclick = () => { view.m += 1; if (view.m > 11) { view.m = 0; view.y += 1; } render(); };
if (today) today.onclick = () => {
const [ty, tm] = todayIso.split('-').map(Number);
view = { y: ty, m: tm - 1 };
selected = byDate[todayIso] ? todayIso : selected;
render();
};
content.querySelectorAll('[data-iso]').forEach((cell) => {
cell.onclick = () => { selected = cell.dataset.iso; render(); };
});
}
load();
})();
</script>
</body>
</html>