Fixed UI misalignments
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user