dashboard

This commit is contained in:
root
2026-03-26 22:58:26 +07:00
parent 5984c18459
commit 9b1d5753cb
2 changed files with 449 additions and 71 deletions

View File

@@ -65,6 +65,29 @@ if ($action === 'login') {
exit; exit;
} }
// Handle POST actions that need a redirect BEFORE layout outputs any HTML.
// Adding new redirect-based POST keys here keeps header() calls safe.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user_id) {
// Mood setter
if (isset($_POST['set_mood'])) {
try { $pdo->exec("ALTER TABLE users ADD COLUMN mood TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN mood_updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action TEXT NOT NULL, detail TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE)"); } catch(Exception $e) {}
$mood = trim((string)($_POST['mood'] ?? ''));
$allowed = ['😊','😢','😠','😴','🤩','😟','😎','🤔','😐','🥳'];
if (in_array($mood, $allowed) || $mood === '') {
$pdo->prepare("UPDATE users SET mood=?, mood_updated_at=CURRENT_TIMESTAMP WHERE id=?")
->execute([$mood ?: null, $user_id]);
try {
$pdo->prepare("INSERT INTO activity_log (user_id, action, detail) VALUES (?, 'mood_set', ?)")
->execute([$user_id, $mood ?: 'cleared']);
} catch(Exception $e) {}
}
header('Location: ?action=dashboard');
exit;
}
}
$view_file = 'views/' . basename($action) . '.php'; $view_file = 'views/' . basename($action) . '.php';
if (file_exists($view_file)) { if (file_exists($view_file)) {
// AJAX-only POST requests must not have the HTML layout wrapped around them, // AJAX-only POST requests must not have the HTML layout wrapped around them,

View File

@@ -1,96 +1,451 @@
<?php <?php
// Calculate Task Counts // ── DB Migrations (safe, idempotent) ─────────────────────────────────────────
$stmt = $pdo->prepare("SELECT status, COUNT(*) as count FROM tasks WHERE (assignee_type='user' AND assignee_id=?) OR (assignee_type='custom' AND assignor_id=?) GROUP BY status"); try { $pdo->exec("ALTER TABLE users ADD COLUMN mood TEXT DEFAULT NULL"); } catch(Exception $e) {}
$stmt->execute([$user_id, $user_id]); try { $pdo->exec("ALTER TABLE users ADD COLUMN mood_updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {}
$task_counts = ['pending' => 0, 'in_progress' => 0, 'completed' => 0]; try {
foreach($stmt->fetchAll() as $row) { $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log (
if(isset($task_counts[$row['status']])) $task_counts[$row['status']] = $row['count']; id INTEGER PRIMARY KEY AUTOINCREMENT,
} user_id INTEGER,
$total_tasks = array_sum($task_counts); action TEXT NOT NULL,
detail TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
// Calculate Passwords count
$stmt = $pdo->prepare("SELECT COUNT(*) FROM passwords p LEFT JOIN password_shares ps ON p.id = ps.password_id WHERE p.owner_id = ? OR (ps.share_type='user' AND ps.share_id=?)"); // ── Task Counts ───────────────────────────────────────────────────────────────
$user_groups = get_user_groups($user_id);
$my_group_ids = array_column($user_groups, 'id');
$group_in = implode(',', array_fill(0, max(count($my_group_ids), 1), '?')) ?: '0';
$params_tasks = array_merge([$user_id, $user_id], $my_group_ids, $my_group_ids, [$user_id]);
$sql_tasks = "SELECT status, COUNT(*) as cnt FROM tasks WHERE
(assignee_type='user' AND assignee_id=?)
OR (assignor_type='user' AND assignor_id=?)
OR (assignee_type='group' AND assignee_id IN ($group_in))
OR (assignor_type='group' AND assignor_group_id IN ($group_in))
OR (assignor_id=?)
GROUP BY status";
$stmt = $pdo->prepare($sql_tasks);
$stmt->execute($params_tasks);
$task_counts = ['pending'=>0,'in_progress'=>0,'completed'=>0,'archived'=>0];
foreach($stmt->fetchAll() as $row) {
if (isset($task_counts[$row['status']])) $task_counts[$row['status']] = $row['cnt'];
}
$total_tasks = $task_counts['pending'] + $task_counts['in_progress'] + $task_counts['completed'];
// ── Password & Project counts ─────────────────────────────────────────────────
$stmt = $pdo->prepare("SELECT COUNT(*) FROM passwords p LEFT JOIN password_shares ps ON p.id=ps.password_id WHERE p.owner_id=? OR (ps.share_type='user' AND ps.share_id=?)");
$stmt->execute([$user_id, $user_id]); $stmt->execute([$user_id, $user_id]);
$passwords_count = $stmt->fetchColumn(); $passwords_count = $stmt->fetchColumn();
// Calculate Projects count $stmt = $pdo->prepare("SELECT COUNT(*) FROM projects p LEFT JOIN project_shares ps ON p.id=ps.project_id WHERE p.owner_id=? OR (ps.share_type='user' AND ps.share_id=?)");
$stmt = $pdo->prepare("SELECT COUNT(*) FROM projects p LEFT JOIN project_shares ps ON p.id = ps.project_id WHERE p.owner_id = ? OR (ps.share_type='user' AND ps.share_id=?)");
$stmt->execute([$user_id, $user_id]); $stmt->execute([$user_id, $user_id]);
$projects_count = $stmt->fetchColumn(); $projects_count = $stmt->fetchColumn();
// ── Notes count ───────────────────────────────────────────────────────────────
$notes_count = 0;
try {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM notes WHERE owner_id=?");
$stmt->execute([$user_id]);
$notes_count = $stmt->fetchColumn();
} catch(Exception $e) {}
// ── Current user mood ─────────────────────────────────────────────────────────
$my_mood = $current_user['mood'] ?? null;
// ── Team moods (all users + their mood) ──────────────────────────────────────
$team_moods = query("SELECT id, username, mood, mood_updated_at FROM users WHERE mood IS NOT NULL AND id != ? ORDER BY mood_updated_at DESC", [$user_id])->fetchAll();
// ── Activity log (user + their groups, last 20) ───────────────────────────────
$recent_logs = [];
try {
// Collect relevant user IDs: current user + members of same groups
$peer_ids = [$user_id];
if ($my_group_ids) {
$gin = implode(',', array_fill(0, count($my_group_ids), '?'));
$peers = $pdo->prepare("SELECT DISTINCT user_id FROM user_groups WHERE group_id IN ($gin)");
$peers->execute($my_group_ids);
$peer_ids = array_unique(array_merge($peer_ids, array_column($peers->fetchAll(), 'user_id')));
}
$uid_in = implode(',', array_fill(0, count($peer_ids), '?'));
$stmt = $pdo->prepare("SELECT al.*, u.username FROM activity_log al LEFT JOIN users u ON al.user_id=u.id WHERE al.user_id IN ($uid_in) ORDER BY al.created_at DESC LIMIT 20");
$stmt->execute($peer_ids);
$recent_logs = $stmt->fetchAll();
} catch(Exception $e) {}
// ── Auto-log task changes for history (read recent task status changes) ───────
$task_logs = [];
try {
// Include group-assigned tasks too
$group_in_tl = $my_group_ids ? implode(',', array_fill(0, count($my_group_ids), '?')) : '0';
$tl_params = array_merge([$user_id, $user_id], $my_group_ids, $my_group_ids);
$stmt = $pdo->prepare("SELECT t.title, t.status, t.status_updated_at, u.username as assignor
FROM tasks t LEFT JOIN users u ON t.assignor_id=u.id
WHERE t.status_updated_at IS NOT NULL
AND (
t.assignee_id=? OR t.assignor_id=?
OR (t.assignee_type='group' AND t.assignee_id IN ($group_in_tl))
OR (t.assignor_type='group' AND t.assignor_group_id IN ($group_in_tl))
)
ORDER BY t.status_updated_at DESC LIMIT 30");
$stmt->execute($tl_params);
$task_logs = $stmt->fetchAll();
} catch(Exception $e) {}
?> ?>
<div class="header">
<h1>Dashboard</h1>
</div>
<div class="card" style="display:flex; justify-content:space-between; align-items:center;"> <style>
.db-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px,1fr)); gap:1.25rem; margin-bottom:1.5rem; }
.db-stat-card {
background:var(--surface); border-radius:1rem; padding:1.25rem 1.5rem;
box-shadow:0 4px 6px -1px rgba(0,0,0,0.07); border:1px solid var(--border);
display:flex; flex-direction:column; gap:0.5rem; transition:transform 0.15s, box-shadow 0.15s;
text-decoration:none !important; color:var(--text) !important;
}
.db-stat-card:hover { transform:translateY(-3px); box-shadow:0 8px 15px -3px rgba(0,0,0,0.1); }
.db-stat-label { font-size:0.8rem; color:var(--text-light); font-weight:500; text-transform:uppercase; letter-spacing:0.04em; }
.db-stat-value { font-size:2.2rem; font-weight:700; line-height:1; }
.db-stat-sub { display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.35rem; }
.db-stat-sub span { font-size:0.75rem; font-weight:600; padding:2px 8px; border-radius:20px; }
.db-stat-link { font-size:0.78rem; color:var(--primary); margin-top:auto; font-weight:500; }
/* Welcome banner */
.db-welcome {
background: linear-gradient(135deg, var(--primary) 0%, #10B981 100%);
border-radius:1rem; padding:1.5rem; color:#fff; margin-bottom:1.5rem;
display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:1rem;
}
.db-welcome h2 { font-size:1.4rem; font-weight:700; }
.db-welcome p { font-size:0.875rem; opacity:0.85; margin-top:0.25rem; }
/* Holiday strip */
.db-holiday {
background:var(--surface); border:1px solid var(--border); border-radius:0.75rem;
padding:0.75rem 1.25rem; display:flex; align-items:center; gap:0.75rem;
font-size:0.875rem; margin-bottom:1.5rem; overflow:hidden;
}
.db-holiday-list {
display:flex; gap:1.5rem; overflow-x:auto; flex:1; padding-bottom:2px;
scrollbar-width:thin;
}
.db-holiday-item { flex-shrink:0; display:flex; align-items:center; gap:0.4rem; white-space:nowrap; }
.db-holiday-date { font-size:0.7rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:2px 6px; border-radius:6px; font-weight:600; }
.db-holiday-name { color:var(--text); font-weight:500; }
.db-holiday-country { font-size:0.7rem; color:var(--text-light); }
/* Mood picker */
.mood-row { display:flex; gap:0.4rem; flex-wrap:wrap; align-items:center; }
.mood-btn {
font-size:1.5rem; background:none; border:2px solid transparent;
border-radius:50%; width:40px; height:40px; cursor:pointer;
display:flex; align-items:center; justify-content:center;
transition:transform 0.15s, border-color 0.15s, background 0.15s;
line-height:1;
}
.mood-btn:hover { transform:scale(1.15); background:rgba(var(--primary-rgb),0.08); }
.mood-btn.active { border-color:var(--primary); background:rgba(var(--primary-rgb),0.12); transform:scale(1.1); }
/* Team moods */
.team-mood-row { display:flex; flex-wrap:wrap; gap:0.6rem; }
.team-mood-chip {
display:flex; align-items:center; gap:0.4rem; font-size:0.8rem;
background:var(--bg); border:1px solid var(--border); border-radius:20px;
padding:4px 10px; color:var(--text);
}
.team-mood-chip .em { font-size:1.2rem; }
/* Activity log */
.log-list { display:flex; flex-direction:column; gap:0.5rem; overflow-y:auto; flex:1; }
.log-item { display:flex; gap:0.75rem; align-items:flex-start; font-size:0.8rem; flex-shrink:0; }
.log-avatar { width:26px; height:26px; border-radius:50%; background:var(--primary); color:#fff; display:flex; align-items:center; justify-content:center; font-size:0.65rem; font-weight:700; flex-shrink:0; }
.log-body { flex:1; min-width:0; }
.log-body strong { color:var(--text); }
.log-body .log-detail { color:var(--text-light); }
.log-time { font-size:0.7rem; color:var(--text-light); white-space:nowrap; flex-shrink:0; }
/* Full-height bottom section */
.db-wrapper { display:flex; flex-direction:column; min-height:calc(100vh - 4rem); }
.two-col {
display:grid; grid-template-columns:1fr 1fr; gap:1.25rem;
flex:1; min-height:0;
}
.two-col > .card {
margin-bottom:0; display:flex; flex-direction:column;
min-height:0; /* allow shrink inside grid */
}
@media(max-width:720px){ .two-col { grid-template-columns:1fr; } }
</style>
<?php
// ── Log a "viewed dashboard" entry (once per session key) ───────────────────
if (empty($_SESSION['dash_logged_today'])) {
try {
$pdo->prepare("INSERT INTO activity_log (user_id, action, detail) VALUES (?, 'visit', 'Opened dashboard')")->execute([$user_id]);
} catch(Exception $e) {}
$_SESSION['dash_logged_today'] = date('Y-m-d');
}
?>
<div class="db-wrapper">
<!-- ── Welcome Banner ──────────────────────────────────────────────────────── -->
<div class="db-welcome">
<div> <div>
<h3>Welcome, <?= htmlspecialchars($current_user['username']) ?>!</h3> <h2>👋 Welcome, <?= htmlspecialchars($current_user['username']) ?>!</h2>
<p style="margin-top:0.5rem; color:var(--text-light);"><span id="current-datetime"></span></p> <p id="current-datetime"></p>
<?php if($user_groups): ?>
<p style="margin-top:0.25rem; opacity:0.8; font-size:0.8rem;">
Groups: <?= implode(', ', array_map(fn($g) => htmlspecialchars($g['name']), $user_groups)) ?>
</p>
<?php endif; ?>
</div> </div>
<div style="text-align:right"> <!-- Mood Picker inline in banner -->
<div style="font-weight:600; color:var(--primary);">Holiday Info:</div> <div style="text-align:right;">
<div id="holiday-info" style="color:var(--text-light);">Loading...</div> <div style="font-size:0.75rem; font-weight:600; opacity:0.8; margin-bottom:0.4rem; text-transform:uppercase; letter-spacing:0.05em;">Your Mood</div>
<form method="POST" id="mood-form" style="display:inline;">
<input type="hidden" name="set_mood" value="1">
<input type="hidden" name="mood" id="mood-value" value="">
<div class="mood-row" style="justify-content:flex-end;">
<?php
$moods = ['😊','🤩','😎','🥳','😐','🤔','😴','😟','😢','😠'];
foreach($moods as $em):
?>
<button type="button" class="mood-btn <?= $my_mood === $em ? 'active' : '' ?>"
onclick="selectMood('<?= $em ?>')" title="<?= $em ?>">
<?= $em ?>
</button>
<?php endforeach; ?>
<?php if($my_mood): ?>
<button type="button" class="mood-btn" onclick="selectMood('')" title="Clear mood" style="font-size:0.7rem; width:auto; border-radius:12px; padding:0 8px; color:#fff; border-color:rgba(255,255,255,0.4);">✕ Clear</button>
<?php endif; ?>
</div>
</form>
<?php if($my_mood): ?>
<div style="margin-top:0.4rem; font-size:0.8rem; opacity:0.8;">Currently: <?= $my_mood ?></div>
<?php endif; ?>
</div> </div>
</div> </div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;"> <!-- ── Today's Holiday Badge (injected by JS into welcome banner) ──────────── -->
<!-- Tasks Card --> <div id="today-holiday-banner" style="display:none; margin-bottom:1rem; padding:0.6rem 1.25rem; background:linear-gradient(90deg,rgba(245,158,11,0.12),rgba(16,185,129,0.08)); border:1px solid rgba(245,158,11,0.3); border-radius:0.75rem; align-items:center; gap:0.75rem; font-size:0.875rem;">
<div class="card"> <span style="font-size:1.3rem;">🎉</span>
<h3>My Tasks Overview</h3> <div>
<h1 style="margin: 1rem 0; font-size: 2.5rem; color: var(--primary);"><?= $total_tasks ?> <span style="font-size:1rem; color:var(--text-light);">Total</span></h1> <strong style="color:var(--text);" id="today-holiday-name"></strong>
<div style="display:flex; justify-content:space-between; margin-top:1rem; border-top:1px solid var(--border); padding-top:1rem;"> <span style="color:var(--text-light); margin-left:0.4rem;" id="today-holiday-label"></span>
<div> </div>
<div style="font-size:0.875rem; color:var(--text-light);">Pending</div> </div>
<div style="font-weight:600;"><?= $task_counts['pending'] ?></div>
</div> <!-- ── Stat Cards ──────────────────────────────────────────────────────────── -->
<div> <div class="db-grid">
<div style="font-size:0.875rem; color:var(--text-light);">In Progress</div> <!-- Tasks -->
<div style="font-weight:600; color:#F59E0B;"><?= $task_counts['in_progress'] ?></div> <a href="?action=tasks" class="db-stat-card" style="border-top:3px solid var(--primary);">
</div> <div class="db-stat-label">
<div> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>
<div style="font-size:0.875rem; color:var(--text-light);">Completed</div> My Tasks
<div style="font-weight:600; color:var(--secondary);"><?= $task_counts['completed'] ?></div>
</div>
</div> </div>
</div> <div class="db-stat-value" style="color:var(--primary);"><?= $total_tasks ?></div>
<div class="db-stat-sub">
<span style="background:rgba(245,158,11,0.12); color:#F59E0B;">⏳ <?= $task_counts['pending'] ?> Pending</span>
<span style="background:rgba(59,130,246,0.12); color:#3B82F6;">🔵 <?= $task_counts['in_progress'] ?> In Progress</span>
<span style="background:rgba(16,185,129,0.12); color:#10B981;">✅ <?= $task_counts['completed'] ?> Done</span>
</div>
<div class="db-stat-link">Open Kanban →</div>
</a>
<!-- Passwords Card --> <!-- Passwords -->
<div class="card" style="display:flex; flex-direction:column; justify-content:center;"> <a href="?action=passwords" class="db-stat-card" style="border-top:3px solid #10B981;">
<h3>My Passwords Vault</h3> <div class="db-stat-label">
<h1 style="margin: 1rem 0; font-size: 2.5rem; color: var(--primary);"><?= $passwords_count ?> <span style="font-size:1rem; color:var(--text-light);">Items</span></h1> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
<a href="?action=passwords" class="btn btn-sm">Go to Vault &rarr;</a> Password Vault
</div> </div>
<div class="db-stat-value" style="color:#10B981;"><?= $passwords_count ?></div>
<div class="db-stat-sub">
<span style="background:rgba(16,185,129,0.12); color:#10B981;">🔐 Stored items</span>
</div>
<div class="db-stat-link">Open Vault →</div>
</a>
<!-- Projects Card --> <!-- Projects -->
<div class="card" style="display:flex; flex-direction:column; justify-content:center;"> <a href="?action=projects" class="db-stat-card" style="border-top:3px solid #F59E0B;">
<h3>My Projects & Files</h3> <div class="db-stat-label">
<h1 style="margin: 1rem 0; font-size: 2.5rem; color: var(--primary);"><?= $projects_count ?> <span style="font-size:1rem; color:var(--text-light);">Files/Folders</span></h1> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
<a href="?action=projects" class="btn btn-sm">Go to Explorer &rarr;</a> Projects & Files
</div> </div>
<div class="db-stat-value" style="color:#F59E0B;"><?= $projects_count ?></div>
<div class="db-stat-sub">
<span style="background:rgba(245,158,11,0.12); color:#F59E0B;">📁 Items</span>
</div>
<div class="db-stat-link">Open Explorer →</div>
</a>
<!-- Notes -->
<a href="?action=notes" class="db-stat-card" style="border-top:3px solid #8B5CF6;">
<div class="db-stat-label">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
Notes
</div>
<div class="db-stat-value" style="color:#8B5CF6;"><?= $notes_count ?></div>
<div class="db-stat-sub">
<span style="background:rgba(139,92,246,0.12); color:#8B5CF6;">📝 Notes saved</span>
</div>
<div class="db-stat-link">Open Notes →</div>
</a>
</div> </div>
<!-- ── Team Moods + Activity Log ───────────────────────────────────────────── -->
<div class="two-col">
<!-- Team Moods -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:700;">👥 Team Mood</h3>
<span style="font-size:0.75rem; color:var(--text-light); font-style:italic;">Live from colleagues</span>
</div>
<?php if(empty($team_moods)): ?>
<p style="color:var(--text-light); font-size:0.85rem;">No one else has set a mood yet.</p>
<?php else: ?>
<div class="team-mood-row">
<?php foreach($team_moods as $tm): ?>
<div class="team-mood-chip" title="Updated: <?= date('M j, H:i', strtotime($tm['mood_updated_at'])) ?>">
<span class="em"><?= $tm['mood'] ?></span>
<span><?= htmlspecialchars($tm['username']) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if($my_mood): ?>
<div style="margin-top:1rem; padding-top:0.75rem; border-top:1px dashed var(--border); font-size:0.8rem; color:var(--text-light);">
Your mood is visible to your team: <strong style="color:var(--text);"><?= $my_mood ?></strong>
</div>
<?php endif; ?>
</div>
<!-- Activity Log -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h3 style="font-size:1rem; font-weight:700;">🕓 Recent Activity</h3>
<span style="font-size:0.75rem; color:var(--text-light);">You & your groups</span>
</div>
<?php
// Merge activity_log + task_logs into one unified feed sorted by time
$feed = [];
foreach($recent_logs as $l) {
$feed[] = [
'time' => $l['created_at'],
'user' => $l['username'] ?? 'System',
'action' => $l['action'],
'detail' => $l['detail'] ?? '',
];
}
foreach($task_logs as $l) {
$status_label = ['pending'=>'moved to Pending','in_progress'=>'started In Progress','completed'=>'marked Completed','archived'=>'archived'][$l['status']] ?? $l['status'];
$feed[] = [
'time' => $l['status_updated_at'],
'user' => $l['assignor'] ?? 'Someone',
'action' => 'task_status',
'detail' => '"'.htmlspecialchars(mb_substr($l['title'],0,30)).'" '.$status_label,
];
}
usort($feed, fn($a,$b) => strtotime($b['time']) - strtotime($a['time']));
$feed = array_slice($feed, 0, 30);
$action_icons = [
'visit' => '🌐',
'mood_set' => '😊',
'task_status' => '✅',
];
?>
<?php if(empty($feed)): ?>
<p style="color:var(--text-light); font-size:0.85rem;">No activity yet.</p>
<?php else: ?>
<div class="log-list">
<?php foreach($feed as $entry):
$initial = strtoupper(mb_substr($entry['user'], 0, 1));
$icon = $action_icons[$entry['action']] ?? '🔔';
$ago = human_time_diff(strtotime($entry['time']));
?>
<div class="log-item">
<div class="log-avatar"><?= $initial ?></div>
<div class="log-body">
<strong><?= htmlspecialchars($entry['user']) ?></strong>
<span class="log-detail"> <?= $icon ?> <?= htmlspecialchars($entry['detail']) ?></span>
</div>
<div class="log-time"><?= $ago ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div><!-- .db-wrapper -->
<script> <script>
// Time & Holiday functions // ── Live clock ────────────────────────────────────────────────────────────────
function updateTime() { function updateTime() {
const now = new Date(); const now = new Date();
document.getElementById('current-datetime').textContent = now.toLocaleString(); const el = document.getElementById('current-datetime');
} if (el) el.textContent = now.toLocaleString('id-ID', {
setInterval(updateTime, 1000); weekday:'long', year:'numeric', month:'long', day:'numeric',
updateTime(); hour:'2-digit', minute:'2-digit', second:'2-digit'
});
}
setInterval(updateTime, 1000); updateTime();
fetch('https://date.nager.at/api/v3/NextPublicHolidaysWorldwide') // ── Mood picker ───────────────────────────────────────────────────────────────
.then(r => r.json()) function selectMood(em) {
.then(data => { document.getElementById('mood-value').value = em;
if(data && data.length > 0) { document.querySelectorAll('.mood-btn').forEach(b => b.classList.remove('active'));
// Find upcoming holiday for US, or fallback to first worldwide if (em) {
let h = data.find(x => x.countryCode === 'US') || data[0]; // Find the button whose text equals em
document.getElementById('holiday-info').textContent = h.date + ' - ' + h.name + ' (' + h.countryCode + ')'; document.querySelectorAll('.mood-btn').forEach(b => {
} else { if (b.textContent.trim() === em) b.classList.add('active');
document.getElementById('holiday-info').textContent = 'No upcoming holidays';
}
}).catch(err => {
document.getElementById('holiday-info').textContent = 'Unable to fetch data';
}); });
}
document.getElementById('mood-form').submit();
}
// ── Holiday check: is TODAY a holiday in Indonesia (ID) or Internationally (UN)?
(function checkTodayHoliday() {
const year = new Date().getFullYear();
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
const urls = [
{ url: `https://date.nager.at/api/v3/PublicHolidays/${year}/ID`, flag: '🇮🇩', label: 'Indonesia' },
{ url: `https://date.nager.at/api/v3/PublicHolidays/${year}/UN`, flag: '🌍', label: 'International Day' }
];
Promise.allSettled(urls.map(e => fetch(e.url).then(r => r.json()).then(d => ({...e, data: d}))))
.then(results => {
const banner = document.getElementById('today-holiday-banner');
const nameEl = document.getElementById('today-holiday-name');
const labelEl = document.getElementById('today-holiday-label');
const matches = [];
results.forEach(r => {
if (r.status === 'fulfilled' && Array.isArray(r.value.data)) {
r.value.data.forEach(h => {
if (h.date === today) matches.push(`${r.value.flag} ${h.localName || h.name}`);
});
}
});
if (matches.length > 0) {
nameEl.textContent = matches.join(' · ');
labelEl.textContent = '— Today is a holiday! 🎊';
banner.style.display = 'flex';
}
// If no holiday today, banner stays hidden (display:none)
});
})();
</script> </script>
<?php
// ── Helper: human-readable time difference ───────────────────────────────────
function human_time_diff($ts) {
$diff = time() - $ts;
if ($diff < 60) return 'just now';
if ($diff < 3600) return floor($diff/60).'m ago';
if ($diff < 86400) return floor($diff/3600).'h ago';
return floor($diff/86400).'d ago';
}
?>