Files
server-manager/src/Models/Notification.php
Agent 12c3e23d9c feat: track notification read receipts per user
- New notification_reads table (migration 008) with unique (notification_id, user_id)
- Notification model: markAsRead inserts into notification_reads; getForUser JOINs
  to show per-user read status; getUnreadCount checks NOT EXISTS in reads table
- getAll() includes read_count subquery for admin view
- Admin view shows 'X / Y' read count for broadcast notifications,
  'Read'/'Unread' badge for user-targeted ones
- Migrates existing read notifications into notification_reads
- AGENTS.md updated with new conventions
2026-06-13 11:48:05 -04:00

161 lines
4.9 KiB
PHP

<?php
declare(strict_types=1);
namespace ServerManager\Models;
use ServerManager\Core\Database;
class Notification
{
private Database $db;
public function __construct()
{
$this->db = Database::getInstance();
}
public function create(array $data): int
{
$data['created_at'] = date('Y-m-d H:i:s');
return $this->db->insert('notifications', $data);
}
public function findById(int $id): ?array
{
return $this->db->fetch('SELECT * FROM notifications WHERE id = ?', [$id]);
}
public function getForUser(int $userId, int $page = 1, int $perPage = 20): array
{
$offset = ($page - 1) * $perPage;
$total = $this->db->fetch(
"SELECT COUNT(*) as total FROM notifications n
WHERE (n.user_id IS NULL OR n.user_id = ?)",
[$userId]
);
$items = $this->db->fetchAll(
"SELECT n.*,
CASE
WHEN nr.id IS NOT NULL THEN 1
ELSE 0
END as is_read,
nr.read_at as read_at,
(SELECT COUNT(*) FROM notification_reads WHERE notification_id = n.id) as total_reads,
(SELECT COUNT(*) FROM users WHERE status = 'active') as total_users
FROM notifications n
LEFT JOIN notification_reads nr ON nr.notification_id = n.id AND nr.user_id = ?
WHERE n.user_id IS NULL OR n.user_id = ?
ORDER BY n.created_at DESC LIMIT ? OFFSET ?",
[$userId, $userId, $perPage, $offset]
);
return [
'data' => $items,
'total' => (int) ($total['total'] ?? 0),
'page' => $page,
'per_page' => $perPage,
'total_pages' => (int) ceil((int) ($total['total'] ?? 0) / $perPage),
];
}
public function getUnreadCount(int $userId): int
{
$result = $this->db->fetch(
"SELECT COUNT(*) as total FROM notifications n
WHERE (n.user_id IS NULL OR n.user_id = ?)
AND NOT EXISTS (
SELECT 1 FROM notification_reads nr
WHERE nr.notification_id = n.id AND nr.user_id = ?
)",
[$userId, $userId]
);
return (int) ($result['total'] ?? 0);
}
public function markAsRead(int $id, int $userId): void
{
try {
$this->db->insert('notification_reads', [
'notification_id' => $id,
'user_id' => $userId,
'read_at' => date('Y-m-d H:i:s'),
]);
} catch (\Throwable $e) {
// Already read, ignore
}
$this->db->update(
'notifications',
['is_read' => 1, 'read_at' => date('Y-m-d H:i:s')],
'id = ? AND (user_id IS NULL OR user_id = ?)',
[$id, $userId]
);
}
public function markAllAsRead(int $userId): void
{
$unread = $this->db->fetchAll(
"SELECT n.id FROM notifications n
WHERE (n.user_id IS NULL OR n.user_id = ?)
AND NOT EXISTS (
SELECT 1 FROM notification_reads nr
WHERE nr.notification_id = n.id AND nr.user_id = ?
)",
[$userId, $userId]
);
$now = date('Y-m-d H:i:s');
foreach ($unread as $note) {
try {
$this->db->insert('notification_reads', [
'notification_id' => (int) $note['id'],
'user_id' => $userId,
'read_at' => $now,
]);
} catch (\Throwable $e) {
// Duplicate entry — already read, skip
}
}
$this->db->update(
'notifications',
['is_read' => 1, 'read_at' => $now],
'(user_id IS NULL OR user_id = ?) AND is_read = 0',
[$userId]
);
}
public function getAll(int $page = 1, int $perPage = 20): array
{
$offset = ($page - 1) * $perPage;
$total = $this->db->fetch("SELECT COUNT(*) as total FROM notifications");
$items = $this->db->fetchAll(
"SELECT n.*, u.username as target_username,
(SELECT COUNT(*) FROM notification_reads nr WHERE nr.notification_id = n.id) as read_count
FROM notifications n
LEFT JOIN users u ON n.user_id = u.id
ORDER BY n.created_at DESC LIMIT ? OFFSET ?",
[$perPage, $offset]
);
return [
'data' => $items,
'total' => (int) ($total['total'] ?? 0),
'page' => $page,
'per_page' => $perPage,
'total_pages' => (int) ceil((int) ($total['total'] ?? 0) / $perPage),
];
}
public function getTotalActiveUsers(): int
{
$result = $this->db->fetch("SELECT COUNT(*) as total FROM users WHERE status = 'active'");
return (int) ($result['total'] ?? 0);
}
}