Files

192 lines
8.7 KiB
HTML
Raw Permalink Normal View History

2026-09-13 20:09:20 +01:00
<!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>