dashboard
This commit is contained in:
23
index.php
23
index.php
@@ -65,6 +65,29 @@ if ($action === 'login') {
|
||||
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';
|
||||
if (file_exists($view_file)) {
|
||||
// AJAX-only POST requests must not have the HTML layout wrapped around them,
|
||||
|
||||
@@ -1,96 +1,451 @@
|
||||
|
||||
<?php
|
||||
// Calculate Task Counts
|
||||
$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");
|
||||
$stmt->execute([$user_id, $user_id]);
|
||||
$task_counts = ['pending' => 0, 'in_progress' => 0, 'completed' => 0];
|
||||
foreach($stmt->fetchAll() as $row) {
|
||||
if(isset($task_counts[$row['status']])) $task_counts[$row['status']] = $row['count'];
|
||||
}
|
||||
$total_tasks = array_sum($task_counts);
|
||||
// ── DB Migrations (safe, idempotent) ─────────────────────────────────────────
|
||||
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) {}
|
||||
|
||||
// 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]);
|
||||
$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]);
|
||||
$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>
|
||||
<h3>Welcome, <?= htmlspecialchars($current_user['username']) ?>!</h3>
|
||||
<p style="margin-top:0.5rem; color:var(--text-light);"><span id="current-datetime"></span></p>
|
||||
<h2>👋 Welcome, <?= htmlspecialchars($current_user['username']) ?>!</h2>
|
||||
<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 style="text-align:right">
|
||||
<div style="font-weight:600; color:var(--primary);">Holiday Info:</div>
|
||||
<div id="holiday-info" style="color:var(--text-light);">Loading...</div>
|
||||
<!-- Mood Picker inline in banner -->
|
||||
<div style="text-align:right;">
|
||||
<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 style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||
<!-- Tasks Card -->
|
||||
<div class="card">
|
||||
<h3>My Tasks Overview</h3>
|
||||
<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>
|
||||
<div style="display:flex; justify-content:space-between; margin-top:1rem; border-top:1px solid var(--border); padding-top:1rem;">
|
||||
<div>
|
||||
<div style="font-size:0.875rem; color:var(--text-light);">Pending</div>
|
||||
<div style="font-weight:600;"><?= $task_counts['pending'] ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:0.875rem; color:var(--text-light);">In Progress</div>
|
||||
<div style="font-weight:600; color:#F59E0B;"><?= $task_counts['in_progress'] ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:0.875rem; color:var(--text-light);">Completed</div>
|
||||
<div style="font-weight:600; color:var(--secondary);"><?= $task_counts['completed'] ?></div>
|
||||
</div>
|
||||
<!-- ── Today's Holiday Badge (injected by JS into welcome banner) ──────────── -->
|
||||
<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;">
|
||||
<span style="font-size:1.3rem;">🎉</span>
|
||||
<div>
|
||||
<strong style="color:var(--text);" id="today-holiday-name"></strong>
|
||||
<span style="color:var(--text-light); margin-left:0.4rem;" id="today-holiday-label"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Stat Cards ──────────────────────────────────────────────────────────── -->
|
||||
<div class="db-grid">
|
||||
<!-- Tasks -->
|
||||
<a href="?action=tasks" class="db-stat-card" style="border-top:3px solid var(--primary);">
|
||||
<div class="db-stat-label">
|
||||
<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>
|
||||
My Tasks
|
||||
</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 -->
|
||||
<div class="card" style="display:flex; flex-direction:column; justify-content:center;">
|
||||
<h3>My Passwords Vault</h3>
|
||||
<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>
|
||||
<a href="?action=passwords" class="btn btn-sm">Go to Vault →</a>
|
||||
</div>
|
||||
<!-- Passwords -->
|
||||
<a href="?action=passwords" class="db-stat-card" style="border-top:3px solid #10B981;">
|
||||
<div class="db-stat-label">
|
||||
<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>
|
||||
Password Vault
|
||||
</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 -->
|
||||
<div class="card" style="display:flex; flex-direction:column; justify-content:center;">
|
||||
<h3>My Projects & Files</h3>
|
||||
<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>
|
||||
<a href="?action=projects" class="btn btn-sm">Go to Explorer →</a>
|
||||
</div>
|
||||
<!-- Projects -->
|
||||
<a href="?action=projects" class="db-stat-card" style="border-top:3px solid #F59E0B;">
|
||||
<div class="db-stat-label">
|
||||
<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>
|
||||
Projects & Files
|
||||
</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>
|
||||
|
||||
<!-- ── 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>
|
||||
// Time & Holiday functions
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('current-datetime').textContent = now.toLocaleString();
|
||||
}
|
||||
setInterval(updateTime, 1000);
|
||||
updateTime();
|
||||
// ── Live clock ────────────────────────────────────────────────────────────────
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const el = document.getElementById('current-datetime');
|
||||
if (el) el.textContent = now.toLocaleString('id-ID', {
|
||||
weekday:'long', year:'numeric', month:'long', day:'numeric',
|
||||
hour:'2-digit', minute:'2-digit', second:'2-digit'
|
||||
});
|
||||
}
|
||||
setInterval(updateTime, 1000); updateTime();
|
||||
|
||||
fetch('https://date.nager.at/api/v3/NextPublicHolidaysWorldwide')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if(data && data.length > 0) {
|
||||
// Find upcoming holiday for US, or fallback to first worldwide
|
||||
let h = data.find(x => x.countryCode === 'US') || data[0];
|
||||
document.getElementById('holiday-info').textContent = h.date + ' - ' + h.name + ' (' + h.countryCode + ')';
|
||||
} else {
|
||||
document.getElementById('holiday-info').textContent = 'No upcoming holidays';
|
||||
}
|
||||
}).catch(err => {
|
||||
document.getElementById('holiday-info').textContent = 'Unable to fetch data';
|
||||
// ── Mood picker ───────────────────────────────────────────────────────────────
|
||||
function selectMood(em) {
|
||||
document.getElementById('mood-value').value = em;
|
||||
document.querySelectorAll('.mood-btn').forEach(b => b.classList.remove('active'));
|
||||
if (em) {
|
||||
// Find the button whose text equals em
|
||||
document.querySelectorAll('.mood-btn').forEach(b => {
|
||||
if (b.textContent.trim() === em) b.classList.add('active');
|
||||
});
|
||||
}
|
||||
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>
|
||||
|
||||
<?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';
|
||||
}
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user