192 lines
8.7 KiB
HTML
192 lines
8.7 KiB
HTML
<!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>
|