/* 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