feat: admin calendar settings preview

This commit is contained in:
chris 2026-02-11 20:15:43 -05:00
parent 3784ec7ca7
commit 61401d8dc7
3 changed files with 141 additions and 3 deletions

View File

@ -233,6 +233,52 @@ async function handleAddNote(e) {
}
}
async function handleCalendarSettingsSubmit(e) {
e.preventDefault();
const input = document.getElementById('calendar-embed-url');
if (!input) return;
const url = input.value.trim();
const res = await apiCall('/admin/calendar-url', 'POST', { url });
if (res.success) {
showMessage(res.data.message || 'Calendar settings updated.', 'success');
await loadCalendarSettings();
}
}
async function handleCalendarSettingsClear() {
const res = await apiCall('/admin/calendar-url', 'POST', { url: '' });
if (res.success) {
const input = document.getElementById('calendar-embed-url');
if (input) input.value = '';
updateCalendarPreview('');
showMessage(res.data.message || 'Calendar settings cleared.', 'success');
}
}
function updateCalendarPreview(url) {
const iframe = document.getElementById('calendar-preview-iframe');
const empty = document.getElementById('calendar-preview-empty');
if (!iframe || !empty) return;
if (!url) {
iframe.removeAttribute('src');
empty.classList.remove('hidden');
return;
}
iframe.src = url;
empty.classList.add('hidden');
}
async function loadCalendarSettings() {
const input = document.getElementById('calendar-embed-url');
if (!input) return;
const res = await apiCall('/admin/calendar-url');
if (res.success) {
const url = res.data?.url || '';
input.value = url;
updateCalendarPreview(url);
}
}
async function handleResetPassword(e) {
e.preventDefault();
const username = e.target.elements['reset-username'].value;
@ -411,7 +457,12 @@ export function attachAdminDashboardListeners() {
document.getElementById('create-user-form')?.addEventListener('submit', handleCreateUser);
document.getElementById('add-punch-form')?.addEventListener('submit', handleAddPunch);
document.getElementById('add-note-form')?.addEventListener('submit', handleAddNote);
document.getElementById('calendar-settings-form')?.addEventListener('submit', handleCalendarSettingsSubmit);
document.getElementById('calendar-clear-btn')?.addEventListener('click', handleCalendarSettingsClear);
document.getElementById('calendar-embed-url')?.addEventListener('input', (e) => updateCalendarPreview(e.target.value.trim()));
setupTabbedInterface();
if (lastAdminTab === 'calendar') renderCalendarView();
if (lastAdminTab === 'settings') loadCalendarSettings();
}
export function attachTimeOffHistoryListeners() {
@ -470,6 +521,9 @@ function setupTabbedInterface() {
if (tabTarget === 'calendar') {
renderCalendarView();
}
if (tabTarget === 'settings') {
loadCalendarSettings();
}
});
}

View File

@ -198,6 +198,8 @@ export async function renderAdminDashboard() {
<button data-tab="overview" class="tab-btn active-tab py-3 px-4">Overview</button>
<button data-tab="logs" class="tab-btn py-3 px-4">Time Logs</button>
<button data-tab="users" class="tab-btn py-3 px-4">User Management</button>
<button data-tab="calendar" class="tab-btn py-3 px-4">Calendar</button>
<button data-tab="settings" class="tab-btn py-3 px-4">Settings</button>
</div>
<div id="admin-tabs-content" class="bg-white rounded-b-lg shadow-md p-6">
<div id="tab-content-overview" class="space-y-8">
@ -252,6 +254,29 @@ ${e.status === 'out' ? `<button class="archive-log-btn font-medium text-amber-60
</div>
<div><h4 class="font-semibold mb-2">Manage Users</h4><div class="overflow-x-auto border rounded-lg"><table class="min-w-full text-sm text-left"><thead class="bg-gray-50"><tr><th class="p-2">Username</th><th class="p-2">Role</th><th class="p-2">Actions</th></tr></thead><tbody>${allUsers.map(u => `<tr class="border-t"><td class="p-2 font-medium">${u.username}</td><td class="p-2 capitalize">${u.role}</td><td class="p-2"><div class="flex flex-col sm:flex-row items-start sm:items-center gap-2">${u.isPrimary ? `<span class="text-sm text-gray-500">Primary Admin</span>` : `<button class="reset-pw-btn font-medium text-blue-600 hover:underline" data-username="${u.username}">Reset PW</button><button class="change-role-btn font-medium text-purple-600 hover:underline" data-username="${u.username}" data-role="${u.role}">${u.role === 'admin' ? 'Demote' : 'Promote'}</button>${u.username !== user.username ? `<button class="delete-user-btn font-medium text-red-600 hover:underline" data-username="${u.username}">Delete</button>` : ''}`}</div></td></tr>`).join('')}</tbody></table></div></div>
</div>
<div id="tab-content-calendar" class="space-y-8 hidden">
</div>
<div id="tab-content-settings" class="space-y-6 hidden">
<div class="max-w-2xl">
<h3 class="text-xl font-bold text-gray-700 mb-2">Calendar Settings</h3>
<p class="text-sm text-gray-500 mb-4">Set the Nextcloud calendar embed URL to display the calendar in the admin dashboard.</p>
<form id="calendar-settings-form" class="space-y-3 bg-gray-50 p-4 rounded-lg">
<label for="calendar-embed-url" class="block text-sm font-medium text-gray-700">Embed URL</label>
<input id="calendar-embed-url" name="calendar-embed-url" type="url" placeholder="https://your-nextcloud.example.com/apps/calendar/p/..." class="w-full p-2 border rounded" />
<div class="flex gap-2">
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
<button type="button" id="calendar-clear-btn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">Clear</button>
</div>
</form>
<div class="mt-6">
<h4 class="text-lg font-semibold text-gray-700 mb-2">Live Preview</h4>
<div class="overflow-hidden rounded-lg border bg-white">
<iframe id="calendar-preview-iframe" title="Calendar Preview" style="width: 100%; height: 60vh; border: none;"></iframe>
</div>
<p id="calendar-preview-empty" class="text-sm text-gray-500 mt-2 hidden">Paste a valid embed URL to preview.</p>
</div>
</div>
</div>
</div>
</div>
`;
@ -276,6 +301,26 @@ ${e.status === 'out' ? `<button class="archive-log-btn font-medium text-amber-60
});
}
export async function renderCalendarView() {
const calendarContainer = document.getElementById('tab-content-calendar');
calendarContainer.innerHTML = '<p>Loading calendar...</p>';
const res = await apiCall('/admin/calendar-url');
if (res.success && res.data.url) {
calendarContainer.innerHTML = `
<iframe src="${res.data.url}" style="width: 100%; height: 80vh; border: none;"></iframe>
`;
} else {
calendarContainer.innerHTML = `
<div class="text-center p-8">
<h3 class="text-xl font-bold text-gray-700">Calendar Not Configured</h3>
<p class="text-gray-500 mt-2">Please configure the Nextcloud calendar embed URL in the Admin Settings tab.</p>
</div>
`;
}
}
export function renderArchiveView() {
apiCall('/admin/archives').then(res => {
if (!res.success) return;
@ -438,4 +483,4 @@ export function updatePendingRequestsList(requests) {
</td>
</tr>
`).join('');
}
}

View File

@ -1,7 +1,7 @@
//server.js
require('dotenv').config();
console.log('--- DIAGNOSTIC TEST ---');
console.log('PUBLIC_VAPID_KEY loaded as:', process.env.PUBLIC_VAPID_KEY);
console.log('publicVapidKey loaded as:', process.env.publicVapidKey);
console.log('--- END DIAGNOSTIC TEST ---');
const express = require('express');
const sqlite3 = require('sqlite3');
@ -97,6 +97,10 @@ async function initializeDatabase() {
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_user_id) REFERENCES users (id) ON DELETE CASCADE
)`);
await db.exec(`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
)`);
const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]);
if (!adminUser) {
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
@ -106,6 +110,18 @@ async function initializeDatabase() {
console.log("Database initialization complete.");
}
async function getSetting(key) {
const row = await db.get('SELECT value FROM settings WHERE key = ?', [key]);
return row ? row.value : null;
}
async function setSetting(key, value) {
await db.run(
'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
[key, value]
);
}
// In server.js
async function addEventToNextcloud(request) {
@ -552,6 +568,29 @@ app.post('/api/admin/notify', authenticateToken, requireRole('admin'), async (re
}
});
app.get('/api/admin/calendar-url', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const storedUrl = await getSetting('nextcloud_calendar_embed_url');
res.json({ url: storedUrl || process.env.NEXTCLOUD_CALENDAR_EMBED_URL || null });
} catch (err) {
res.status(500).json({ message: 'Failed to fetch calendar settings.' });
}
});
app.post('/api/admin/calendar-url', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { url } = req.body;
if (!url) {
await db.run('DELETE FROM settings WHERE key = ?', ['nextcloud_calendar_embed_url']);
return res.json({ message: 'Calendar embed URL cleared.' });
}
await setSetting('nextcloud_calendar_embed_url', url);
res.json({ message: 'Calendar embed URL updated.' });
} catch (err) {
res.status(500).json({ message: 'Failed to update calendar settings.' });
}
});
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const entriesToArchive = await db.all("SELECT * FROM time_entries WHERE status = 'out'");
@ -726,4 +765,4 @@ app.delete('/api/user/time-off-requests/:id', authenticateToken, async (req, res
});
}
startServer();
startServer();