feat: admin calendar settings preview
This commit is contained in:
parent
3784ec7ca7
commit
61401d8dc7
@ -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) {
|
async function handleResetPassword(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const username = e.target.elements['reset-username'].value;
|
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('create-user-form')?.addEventListener('submit', handleCreateUser);
|
||||||
document.getElementById('add-punch-form')?.addEventListener('submit', handleAddPunch);
|
document.getElementById('add-punch-form')?.addEventListener('submit', handleAddPunch);
|
||||||
document.getElementById('add-note-form')?.addEventListener('submit', handleAddNote);
|
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();
|
setupTabbedInterface();
|
||||||
|
if (lastAdminTab === 'calendar') renderCalendarView();
|
||||||
|
if (lastAdminTab === 'settings') loadCalendarSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachTimeOffHistoryListeners() {
|
export function attachTimeOffHistoryListeners() {
|
||||||
@ -470,6 +521,9 @@ function setupTabbedInterface() {
|
|||||||
if (tabTarget === 'calendar') {
|
if (tabTarget === 'calendar') {
|
||||||
renderCalendarView();
|
renderCalendarView();
|
||||||
}
|
}
|
||||||
|
if (tabTarget === 'settings') {
|
||||||
|
loadCalendarSettings();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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="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="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="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>
|
||||||
<div id="admin-tabs-content" class="bg-white rounded-b-lg shadow-md p-6">
|
<div id="admin-tabs-content" class="bg-white rounded-b-lg shadow-md p-6">
|
||||||
<div id="tab-content-overview" class="space-y-8">
|
<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>
|
||||||
<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><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>
|
||||||
|
<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>
|
||||||
</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() {
|
export function renderArchiveView() {
|
||||||
apiCall('/admin/archives').then(res => {
|
apiCall('/admin/archives').then(res => {
|
||||||
if (!res.success) return;
|
if (!res.success) return;
|
||||||
|
|||||||
41
server.js
41
server.js
@ -1,7 +1,7 @@
|
|||||||
//server.js
|
//server.js
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
console.log('--- DIAGNOSTIC TEST ---');
|
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 ---');
|
console.log('--- END DIAGNOSTIC TEST ---');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const sqlite3 = require('sqlite3');
|
const sqlite3 = require('sqlite3');
|
||||||
@ -97,6 +97,10 @@ async function initializeDatabase() {
|
|||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (employee_user_id) REFERENCES users (id) ON DELETE CASCADE
|
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]);
|
const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]);
|
||||||
if (!adminUser) {
|
if (!adminUser) {
|
||||||
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||||||
@ -106,6 +110,18 @@ async function initializeDatabase() {
|
|||||||
console.log("Database initialization complete.");
|
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
|
// In server.js
|
||||||
|
|
||||||
async function addEventToNextcloud(request) {
|
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) => {
|
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const entriesToArchive = await db.all("SELECT * FROM time_entries WHERE status = 'out'");
|
const entriesToArchive = await db.all("SELECT * FROM time_entries WHERE status = 'out'");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user