fix: missing avatar URLs in notifications, admin panel, and budget

- Notifications: map raw avatar filename to /uploads/avatars/ URL in
  getNotifications, createNotification broadcasts, and respond handler
- Admin listUsers: include avatar field in SELECT and map to avatar_url
- Admin page: render actual avatar image instead of initial letter only
- Budget loadItemMembers: map avatar to avatar_url (fixed in prior commit)

Fixes #507
This commit is contained in:
Maurice
2026-04-08 18:17:08 +02:00
parent 9dc91b08a9
commit 2d17ec60db
3 changed files with 26 additions and 10 deletions
+8 -3
View File
@@ -30,6 +30,7 @@ interface AdminUser {
last_login?: string | null
online?: boolean
oidc_issuer?: string | null
avatar_url?: string | null
}
interface AdminStats {
@@ -605,9 +606,13 @@ export default function AdminPage(): React.ReactElement {
<td className="px-5 py-3">
<div className="flex items-center gap-2">
<div className="relative">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-sm font-medium text-slate-700">
{u.username.charAt(0).toUpperCase()}
</div>
{u.avatar_url ? (
<img src={u.avatar_url} alt={u.username} className="w-8 h-8 rounded-full object-cover" />
) : (
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-sm font-medium text-slate-700">
{u.username.charAt(0).toUpperCase()}
</div>
)}
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2" style={{ borderColor: 'var(--bg-card)', background: u.online ? '#22c55e' : '#94a3b8' }} />
</div>
<div>
+3 -2
View File
@@ -40,8 +40,8 @@ export const isDocker = (() => {
export function listUsers() {
const users = db.prepare(
'SELECT id, username, email, role, created_at, updated_at, last_login FROM users ORDER BY created_at DESC'
).all() as Pick<User, 'id' | 'username' | 'email' | 'role' | 'created_at' | 'updated_at' | 'last_login'>[];
'SELECT id, username, email, role, avatar, created_at, updated_at, last_login FROM users ORDER BY created_at DESC'
).all() as (Pick<User, 'id' | 'username' | 'email' | 'role' | 'created_at' | 'updated_at' | 'last_login'> & { avatar?: string | null })[];
let onlineUserIds = new Set<number>();
try {
const { getOnlineUserIds } = require('../websocket');
@@ -49,6 +49,7 @@ export function listUsers() {
} catch { /* */ }
return users.map(u => ({
...u,
avatar_url: u.avatar ? `/uploads/avatars/${u.avatar}` : null,
created_at: utcSuffix(u.created_at),
updated_at: utcSuffix(u.updated_at as string),
last_login: utcSuffix(u.last_login),
+15 -5
View File
@@ -159,7 +159,7 @@ function createNotification(input: NotificationInput): number[] {
notification: {
...row,
sender_username: sender?.username ?? null,
sender_avatar: sender?.avatar ?? null,
sender_avatar: sender?.avatar ? `/uploads/avatars/${sender.avatar}` : null,
},
});
}
@@ -219,7 +219,7 @@ export function createNotificationForRecipient(
notification: {
...row,
sender_username: sender?.username ?? null,
sender_avatar: sender?.avatar ?? null,
sender_avatar: sender?.avatar ? `/uploads/avatars/${sender.avatar}` : null,
},
});
@@ -249,7 +249,12 @@ function getNotifications(
const { total } = db.prepare(`SELECT COUNT(*) as total FROM notifications ${wherePlain}`).get(userId) as { total: number };
const { unread_count } = db.prepare('SELECT COUNT(*) as unread_count FROM notifications WHERE recipient_id = ? AND is_read = 0').get(userId) as { unread_count: number };
return { notifications: rows, total, unread_count };
const mapped = rows.map(r => ({
...r,
sender_avatar: r.sender_avatar ? `/uploads/avatars/${r.sender_avatar}` : null,
}));
return { notifications: mapped, total, unread_count };
}
function getUnreadCount(userId: number): number {
@@ -326,9 +331,14 @@ async function respondToBoolean(
WHERE n.id = ?
`).get(notificationId) as NotificationRow;
broadcastToUser(userId, { type: 'notification:updated', notification: updated });
const mappedUpdated = {
...updated,
sender_avatar: updated.sender_avatar ? `/uploads/avatars/${updated.sender_avatar}` : null,
};
return { success: true, notification: updated };
broadcastToUser(userId, { type: 'notification:updated', notification: mappedUpdated });
return { success: true, notification: mappedUpdated };
}
export {