/* time_admin.js - Admin Time-Tracking Console * Renders an "All Team Clock-Ins (Admin)" section in the Time Tracking tab. * Visible only when the logged-in user is Dee or Alyssa. * Provides: date range filter, employee filter dropdown, per-row Edit / Approve / * Unapprove / Delete actions, bulk-approve, per-user totals, and pending * clock-in + time-edit request queues. */ (function () { 'use strict'; const ADMIN_USERNAMES = ['dee', 'alyssa']; function getUsername() { const el = document.getElementById('who'); if (!el) return null; const m = (el.textContent || '').match(/\(([^)]+)\)\s*$/); return m ? m[1].trim().toLowerCase() : null; } function isAdmin(u) { return !!u && ADMIN_USERNAMES.includes(u.toLowerCase()); } async function rpc(fn, params) { try { const r = await window.rpc(fn, params || {}); return { ok: true, data: r }; } catch (e) { console.error('[TimeAdmin] rpc', fn, e); return { ok: false, error: e && e.message ? e.message : String(e) }; } } function el(tag, attrs, kids) { const n = document.createElement(tag); if (attrs) for (const k in attrs) { if (k === 'style') n.style.cssText = attrs[k]; else if (k === 'cls') n.className = attrs[k]; else if (k.startsWith('on')) n.addEventListener(k.slice(2), attrs[k]); else if (k === 'html') n.innerHTML = attrs[k]; else n.setAttribute(k, attrs[k]); } if (kids) [].concat(kids).forEach(c => { if (c == null) return; n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); }); return n; } function fmt(ts) { if (!ts) return '--'; try { return new Date(ts).toLocaleString(); } catch (e) { return String(ts); } } function fmtDate(d) { if (!d) return ''; try { return new Date(d + 'T00:00:00').toLocaleDateString(); } catch (e) { return d; } } function todayISO() { return new Date().toISOString().slice(0,10); } function daysAgoISO(n) { const d = new Date(); d.setDate(d.getDate() - n); return d.toISOString().slice(0,10); } function toLocalInput(ts) { if (!ts) return ''; const d = new Date(ts); const pad = x => String(x).padStart(2,'0'); return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'T'+pad(d.getHours())+':'+pad(d.getMinutes()); } function buildPanel() { const username = getUsername(); if (!username || !isAdmin(username)) return null; const root = el('div', { id: 'time-admin-panel', cls: 'card', style: 'margin-top:16px;padding:16px;border:1px solid #444;' }, [ el('h3', { style: 'margin:0 0 4px 0;color:#f5a623;' }, 'All Team Clock-Ins (Admin)'), el('div', { style: 'color:#888;font-size:12px;margin-bottom:10px;' }, 'View, edit, and approve every team member\'s clock-ins for payroll.'), // Filters el('div', { style: 'display:flex;flex-wrap:wrap;gap:8px;align-items:end;margin-bottom:10px;' }, [ el('label', null, [ el('div', { style:'font-size:11px;color:#888;' }, 'FROM'), el('input', { id:'ta-from', type:'date', value: daysAgoISO(13), style:'width:140px;' }) ]), el('label', null, [ el('div', { style:'font-size:11px;color:#888;' }, 'TO'), el('input', { id:'ta-to', type:'date', value: todayISO(), style:'width:140px;' }) ]), el('label', null, [ el('div', { style:'font-size:11px;color:#888;' }, 'EMPLOYEE'), el('select', { id:'ta-filter', style:'width:180px;' }, [ el('option', { value:'' }, 'All employees'), ]) ]), el('button', { cls:'btn', onclick: refresh }, 'Refresh'), el('button', { cls:'btn', style:'background:#1f7a4d;color:#fff;', onclick: bulkApprove }, 'Bulk-approve range'), el('button', { cls:'btn', style:'background:#0a6cb6;color:#fff;', onclick: openAddDialog }, 'Add entry for employee'), ]), // Pending counts banner el('div', { id:'ta-pending-banner', style:'margin-bottom:10px;color:#bbb;' }), // Per-user totals el('h4', { style:'margin:14px 0 4px 0;' }, 'Per-Employee Totals'), el('div', { id:'ta-summary', style:'font-size:13px;color:#bbb;' }, 'Loading...'), // Entries table el('h4', { style:'margin:14px 0 4px 0;' }, 'Time Entries'), el('div', { id:'ta-entries', style:'font-size:13px;color:#bbb;overflow-x:auto;' }, 'Loading...'), // Pending clock-in requests el('h4', { style:'margin:14px 0 4px 0;color:#f5a623;' }, 'Pending Clock-In Requests'), el('div', { id:'ta-clock-reqs', style:'font-size:13px;color:#bbb;' }, 'Loading...'), // Pending time-edit requests el('h4', { style:'margin:14px 0 4px 0;color:#f5a623;' }, 'Pending Time-Edit Requests'), el('div', { id:'ta-edit-reqs', style:'font-size:13px;color:#bbb;' }, 'Loading...'), ]); return root; } function mount(panel) { if (!panel) return; const prior = document.getElementById('time-admin-panel'); if (prior) prior.remove(); const host = document.getElementById('panel-time-tracking') || document.getElementById('panel-manage') || document.getElementById('app') || document.body; host.appendChild(panel); } async function loadEmployeeOptions() { const sel = document.getElementById('ta-filter'); if (!sel) return; const dash = await rpc('app_get_dashboard_data', { p_username: getUsername() }); if (!dash.ok) return; const users = (dash.data && dash.data.all_app_users) || []; users.filter(u => u.is_active).sort((a,b)=> (a.full_name||'').localeCompare(b.full_name||'')) .forEach(u => sel.appendChild(el('option', { value: u.username }, u.full_name + ' ('+u.username+')'))); } async function loadPendingCounts() { const banner = document.getElementById('ta-pending-banner'); if (!banner) return; const r = await rpc('app_admin_pending_counts', { p_username: getUsername() }); if (!r.ok) { banner.textContent = ''; return; } const row = (r.data || [])[0] || r.data || {}; const parts = []; if (row.clock_request_count) parts.push(''+row.clock_request_count+' pending clock-in request(s)'); if (row.time_edit_count) parts.push(''+row.time_edit_count+' pending time-edit request(s)'); if (row.unapproved_entries_count) parts.push(''+row.unapproved_entries_count+' unapproved entry(s)'); banner.innerHTML = parts.length ? parts.join('   |   ') : 'All caught up.'; } async function loadSummary() { const wrap = document.getElementById('ta-summary'); if (!wrap) return; const from = document.getElementById('ta-from').value; const to = document.getElementById('ta-to').value; const r = await rpc('app_admin_get_team_time_summary', { p_username: getUsername(), p_from_date: from, p_to_date: to }); if (!r.ok) { wrap.textContent = 'Could not load summary. ('+r.error+')'; return; } const rows = r.data || []; wrap.innerHTML = ''; if (rows.length === 0) { wrap.textContent = 'No entries in this date range.'; return; } const head = el('div', { style: 'display:grid;grid-template-columns:1.8fr 0.9fr 0.7fr 0.9fr 0.9fr;gap:8px;padding:6px 0;border-bottom:1px solid #333;font-weight:600;color:#bbb;font-size:12px;' }, [ el('div', null, 'Employee'), el('div', null, 'Office'), el('div', null, 'Entries'), el('div', null, 'Approved hrs'), el('div', null, 'Pending hrs'), ]); wrap.appendChild(head); rows.forEach(s => { wrap.appendChild(el('div', { style: 'display:grid;grid-template-columns:1.8fr 0.9fr 0.7fr 0.9fr 0.9fr;gap:8px;padding:6px 0;border-bottom:1px solid #222;font-size:13px;' }, [ el('div', { style:'color:#ddd;' }, s.full_name + ' ('+s.username+')'), el('div', null, s.office || ''), el('div', null, String(s.entries_count || 0)), el('div', { style:'color:#5dd5a8;' }, String(s.approved_hours || 0)), el('div', { style:'color:#f5a623;' }, String(s.pending_hours || 0)), ])); }); } async function loadEntries() { const wrap = document.getElementById('ta-entries'); if (!wrap) return; const from = document.getElementById('ta-from').value; const to = document.getElementById('ta-to').value; const filt = document.getElementById('ta-filter').value || null; const r = await rpc('app_admin_get_team_time_entries', { p_username: getUsername(), p_from_date: from, p_to_date: to, p_filter_username: filt }); if (!r.ok) { wrap.textContent = 'Could not load entries. ('+r.error+')'; return; } const rows = r.data || []; wrap.innerHTML = ''; if (rows.length === 0) { wrap.textContent = 'No entries.'; return; } const head = el('div', { style:'display:grid;grid-template-columns:1.4fr 1.5fr 1.5fr 0.7fr 0.7fr 0.9fr 1.4fr;gap:8px;padding:6px 0;border-bottom:1px solid #333;font-weight:600;color:#bbb;font-size:12px;' }, [ el('div', null, 'Employee'), el('div', null, 'Clock In'), el('div', null, 'Clock Out'), el('div', null, 'Break'), el('div', null, 'Hours'), el('div', null, 'Status'), el('div', null, 'Actions'), ]); wrap.appendChild(head); rows.forEach(e => wrap.appendChild(renderEntryRow(e))); } function renderEntryRow(e) { const open = !e.clock_out; const status = open ? 'OPEN' : (e.payroll_approved ? 'APPROVED' : 'PENDING'); const statusColor = open ? '#f5a623' : (e.payroll_approved ? '#5dd5a8' : '#e26565'); const actions = []; actions.push(el('button', { cls:'btn-sm', style:'background:#3b6db2;color:#fff;', onclick: () => openEditDialog(e) }, 'Edit')); if (!open) { if (e.payroll_approved) actions.push(el('button', { cls:'btn-sm', style:'background:#8a4d1b;color:#fff;', onclick: () => approve(e.id, false) }, 'Unapprove')); else actions.push(el('button', { cls:'btn-sm', style:'background:#1f7a4d;color:#fff;', onclick: () => approve(e.id, true) }, 'Approve')); } actions.push(el('button', { cls:'btn-sm', style:'background:#a13b3b;color:#fff;', onclick: () => deleteEntry(e.id) }, 'Delete')); return el('div', { style:'display:grid;grid-template-columns:1.4fr 1.5fr 1.5fr 0.7fr 0.7fr 0.9fr 1.4fr;gap:8px;padding:6px 0;border-bottom:1px solid #222;font-size:13px;align-items:center;' }, [ el('div', { style:'color:#ddd;' }, (e.full_name || '') + ' ('+e.username+')'), el('div', null, fmt(e.clock_in)), el('div', null, fmt(e.clock_out)), el('div', null, String(e.break_minutes || 0) + ' min'), el('div', { style:'color:#5dd5a8;' }, e.hours_worked != null ? String(e.hours_worked) : '--'), el('div', { style:'color:'+statusColor+';font-weight:600;font-size:11px;' }, status), el('div', { style:'display:flex;gap:4px;flex-wrap:wrap;' }, actions), ]); } function openAddDialog() { const employees = window.__teamMembersCache || []; /* Build option list from current filter