← back to NationalPaperHangers
public/js/calendar-installer.js
110 lines
// Installer admin calendar JS — manage availability, time-off, view upcoming.
(function () {
function csrf() {
const m = document.querySelector('meta[name="csrf-token"]');
return m ? m.getAttribute('content') : '';
}
// ---- Availability
const tbody = document.getElementById('availability-tbody');
const addAvail = document.getElementById('add-availability');
if (addAvail) {
addAvail.addEventListener('submit', async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(addAvail).entries());
const r = await fetch('/api/admin/availability', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-csrf-token': csrf() },
body: JSON.stringify(data)
});
if (!r.ok) { alert('Could not add window'); return; }
const row = await r.json();
const tr = document.createElement('tr');
tr.dataset.id = row.id;
tr.innerHTML = `<td>${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][row.day_of_week]}</td><td>${row.start_time}</td><td>${row.end_time}</td><td><button type="button" class="btn btn-ghost btn-sm" data-remove-availability="${row.id}">Remove</button></td>`;
tbody.appendChild(tr);
addAvail.reset();
});
}
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-remove-availability]');
if (!btn) return;
const id = btn.dataset.removeAvailability;
if (!confirm('Remove this availability window?')) return;
const r = await fetch(`/api/admin/availability/${id}`, {
method: 'DELETE',
headers: { 'x-csrf-token': csrf() }
});
if (r.ok) btn.closest('tr').remove();
});
// ---- Time-off
const offList = document.getElementById('time-off-list');
const addOff = document.getElementById('add-time-off');
async function loadTimeOff() {
if (!offList) return;
offList.innerHTML = '';
const r = await fetch('/api/admin/time-off');
const j = await r.json();
if (!j.timeOff || !j.timeOff.length) {
offList.innerHTML = '<li class="muted">No upcoming blocks.</li>';
return;
}
for (const t of j.timeOff) {
const li = document.createElement('li');
li.innerHTML = `<span>${new Date(t.start_at).toLocaleString()} → ${new Date(t.end_at).toLocaleString()}</span>${t.reason ? '<span class="muted"> · ' + escapeHtml(t.reason) + '</span>' : ''}<button type="button" class="btn btn-ghost btn-sm" data-remove-off="${t.id}" style="margin-left:auto">Remove</button>`;
offList.appendChild(li);
}
}
if (addOff) {
addOff.addEventListener('submit', async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(addOff).entries());
data.start_at = new Date(data.start_at).toISOString();
data.end_at = new Date(data.end_at).toISOString();
const r = await fetch('/api/admin/time-off', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-csrf-token': csrf() },
body: JSON.stringify(data)
});
if (!r.ok) { alert('Could not add block'); return; }
addOff.reset();
loadTimeOff();
});
}
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-remove-off]');
if (!btn) return;
if (!confirm('Remove this block?')) return;
const r = await fetch(`/api/admin/time-off/${btn.dataset.removeOff}`, {
method: 'DELETE',
headers: { 'x-csrf-token': csrf() }
});
if (r.ok) loadTimeOff();
});
// ---- Upcoming bookings
async function loadUpcoming() {
const el = document.getElementById('upcoming-cal-list');
if (!el) return;
const r = await fetch('/api/admin/bookings.json');
const j = await r.json();
if (!j.bookings || !j.bookings.length) { el.textContent = 'No upcoming bookings.'; return; }
el.innerHTML = '<ul class="time-off-list">' + j.bookings.map(b =>
`<li><span><strong>${escapeHtml(b.customer_name)}</strong> · ${new Date(b.scheduled_start).toLocaleString()} · ${escapeHtml((b.project_type || 'consultation').replace(/_/g, ' '))}</span><span class="status-badge status-${b.status}" style="margin-left:auto">${b.status}</span></li>`
).join('') + '</ul>';
}
function escapeHtml(s) {
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
loadTimeOff();
loadUpcoming();
})();