Fixed UI misalignments

This commit is contained in:
2026-10-26 19:57:32 +02:00
parent 80c798e5e6
commit 9f132eeb29
22 changed files with 860 additions and 214 deletions
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Search,
Trash2,
Bell,
LogOut,
Calendar,
@@ -197,6 +198,7 @@ export function DashboardPage() {
top5Unread,
markAsRead,
markAllAsRead,
deleteMessage,
sendMessage,
isSending,
} = useInAppNotifications();
@@ -1221,9 +1223,25 @@ export function DashboardPage() {
</div>
</div>
</div>
<span className="text-xs text-slate-400/70 shrink-0">
{formatFullDate(msg.createdAt)}
</span>
<div className="flex items-center gap-2.5 shrink-0">
<span className="text-xs text-slate-400/70">
{formatFullDate(msg.createdAt)}
</span>
<button
type="button"
onClick={e => {
e.stopPropagation();
deleteMessage(msg.id);
if (selectedNotification?.id === msg.id) {
setSelectedNotification(null);
}
}}
className="p-1.5 rounded-lg text-slate-400 hover:text-rose-400 hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 transition cursor-pointer"
title="Delete message (Archive)"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
})
@@ -1905,16 +1923,30 @@ export function DashboardPage() {
/>
</div>
{/* Action Buttons: Reply and Close */}
{/* Action Buttons: Reply, Delete, and Close */}
<div className="flex items-center justify-between pt-2 border-t border-white/[0.08]">
<button
type="button"
onClick={() => handleReplyToMessage(selectedNotification)}
className="px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold flex items-center gap-2 shadow-lg shadow-emerald-500/20 transition-all cursor-pointer"
>
<Reply className="w-3.5 h-3.5" />
<span>Reply</span>
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => handleReplyToMessage(selectedNotification)}
className="px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold flex items-center gap-2 shadow-lg shadow-emerald-500/20 transition-all cursor-pointer"
>
<Reply className="w-3.5 h-3.5" />
<span>Reply</span>
</button>
<button
type="button"
onClick={() => {
deleteMessage(selectedNotification.id);
setSelectedNotification(null);
}}
className="px-4 py-2.5 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/30 text-xs font-semibold flex items-center gap-2 transition-all cursor-pointer"
title="Delete message (Archive)"
>
<Trash2 className="w-3.5 h-3.5" />
<span>Delete</span>
</button>
</div>
<button
type="button"
onClick={() => setSelectedNotification(null)}
@@ -5,6 +5,8 @@ import {
getUserNotifications,
getNotifications,
createNotification,
updateNotification,
archiveNotification,
markNotificationAsRead,
getCurrentUserTenantId,
resolveUserIdByEmail,
@@ -124,7 +126,10 @@ export function useInAppNotifications() {
const inboxNotifications = useMemo(() => {
return inAppNotifications.filter(
n => !trashedIds.includes(String(n.id)) && Number(n.status) !== NotificationStatus.Draft
n =>
!trashedIds.includes(String(n.id)) &&
Number(n.status) !== NotificationStatus.Archived &&
Number(n.status) !== NotificationStatus.Draft
);
}, [inAppNotifications, trashedIds]);
@@ -151,7 +156,9 @@ export function useInAppNotifications() {
}, [inAppNotifications, starredIds, trashedIds]);
const trashedNotifications = useMemo(() => {
return inAppNotifications.filter(n => trashedIds.includes(String(n.id)));
return inAppNotifications.filter(
n => trashedIds.includes(String(n.id)) || Number(n.status) === NotificationStatus.Archived
);
}, [inAppNotifications, trashedIds]);
const markAsReadMutation = useMutation({
@@ -191,7 +198,8 @@ export function useInAppNotifications() {
});
}, [starredStorageKey]);
const deleteMessage = useCallback((id: string | number) => {
// Delete message (NotificationStatus == Archived & move to trash)
const deleteMessage = useCallback(async (id: string | number) => {
const strId = String(id);
setTrashedIds(prev => {
const next = prev.includes(strId) ? prev : [...prev, strId];
@@ -201,9 +209,22 @@ export function useInAppNotifications() {
} catch {}
return next;
});
}, [trashStorageKey]);
const restoreMessage = useCallback((id: string | number) => {
queryClient.setQueryData<Notification[]>(['inapp-notifications', userId], (old = []) =>
old.map(n => (String(n.id) === strId ? { ...n, status: NotificationStatus.Archived } : n))
);
try {
await archiveNotification(id);
} catch (err) {
console.warn('[deleteMessage] Error archiving notification on server:', err);
} finally {
queryClient.invalidateQueries({ queryKey: ['inapp-notifications'] });
}
}, [trashStorageKey, userId, queryClient]);
// Restore from trash / unarchive
const restoreMessage = useCallback(async (id: string | number) => {
const strId = String(id);
setTrashedIds(prev => {
const next = prev.filter(x => x !== strId);
@@ -213,7 +234,19 @@ export function useInAppNotifications() {
} catch {}
return next;
});
}, [trashStorageKey]);
queryClient.setQueryData<Notification[]>(['inapp-notifications', userId], (old = []) =>
old.map(n => (String(n.id) === strId ? { ...n, status: NotificationStatus.Read } : n))
);
try {
await markNotificationAsRead(id);
} catch (err) {
console.warn('[restoreMessage] Error restoring notification on server:', err);
} finally {
queryClient.invalidateQueries({ queryKey: ['inapp-notifications'] });
}
}, [trashStorageKey, userId, queryClient]);
const permanentDelete = useCallback((id: string | number) => {
const strId = String(id);
@@ -93,6 +93,7 @@ export const NOTIFICATION_STATUS_OPTIONS = [
{ value: NotificationStatus.Cancelled, label: 'Cancelled' },
{ value: NotificationStatus.Bounced, label: 'Bounced' },
{ value: NotificationStatus.Draft, label: 'Draft' },
{ value: NotificationStatus.Archived, label: 'Archived' },
];
export const NOTIFICATION_PLATFORM_OPTIONS = [
@@ -536,6 +537,8 @@ export const getEmailStatusBadge = (status?: EmailStatuses | number | null) => {
export const getNotificationStatusBadge = (status?: NotificationStatus | number | null) => {
const s = Number(status);
switch (s) {
case NotificationStatus.Archived:
return { label: 'Archived', className: 'bg-slate-800 text-slate-300 border-slate-700 font-semibold' };
case NotificationStatus.Draft:
return { label: 'Draft', className: 'bg-purple-50 text-purple-800 border-purple-300 font-semibold' };
case NotificationStatus.Delivered:
@@ -109,6 +109,7 @@ import {
UpdateRuleRequest,
Pagination,
Notification,
NotificationPlatform,
NotificationStatus,
Role,
Claim,
@@ -1898,7 +1899,9 @@ export const createNotification = async (payload: CreateNotificationRequest): Pr
subject: payload.subject || null,
destination: payload.destination || 'In-App User Inbox',
message: payload.message,
status: payload.status !== undefined ? payload.status : undefined,
status: payload.status !== undefined
? payload.status
: (Number(payload.platform) === NotificationPlatform.InApp ? NotificationStatus.Delivered : undefined),
});
};
@@ -1920,6 +1923,13 @@ export const markNotificationAsRead = async (notificationId: number | string): P
};
// ==========================================
export const archiveNotification = async (notificationId: number | string): Promise<void> => {
await updateNotification({
notificationId: toIntegerId(notificationId),
status: NotificationStatus.Archived,
});
};
// 9. Governance: Tenants APIs
// ==========================================
export const getTenants = async (): Promise<Tenant[]> => {
@@ -29,6 +29,7 @@ export enum NotificationStatus {
Cancelled = 8,
Bounced = 9,
Draft = 10,
Archived = 11,
}
export enum NotificationPlatform {