Files
illusory-mapp/apps/main/src/lib/domains/notifications/notification.vm.svelte.ts
2026-03-01 04:06:54 +02:00

236 lines
6.9 KiB
TypeScript

import type {
ClientNotificationFilters,
ClientPaginationState,
Notifications,
} from "@pkg/logic/domains/notifications/data";
import {
archiveSC,
deleteNotificationsSC,
getNotificationsSQ,
getUnreadCountSQ,
markAllReadSC,
markReadSC,
markUnreadSC,
unarchiveSC,
} from "./notifications.remote";
import { user } from "$lib/global.stores";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
class NotificationViewModel {
notifications = $state([] as Notifications);
loading = $state(false);
selectedIds = $state(new Set<number>());
pagination = $state<ClientPaginationState>({
page: 1,
pageSize: 20,
total: 0,
totalPages: 0,
sortBy: "createdAt",
sortOrder: "desc",
});
filters = $state<ClientNotificationFilters>({
userId: get(user)?.id!,
isArchived: false,
});
unreadCount = $state(0);
private getFetchQueryInput() {
return {
filters: {
userId: this.filters.userId,
isRead: this.filters.isRead,
isArchived: this.filters.isArchived,
type: this.filters.type,
category: this.filters.category,
priority: this.filters.priority,
search: this.filters.search,
},
pagination: {
page: this.pagination.page,
pageSize: this.pagination.pageSize,
sortBy: this.pagination.sortBy,
sortOrder: this.pagination.sortOrder,
},
};
}
private async runCommand(
fn: (payload: { notificationIds: number[] }) => Promise<any>,
notificationIds: number[],
errorMessage: string,
after: Array<() => Promise<void>>,
) {
try {
const result = await fn({ notificationIds });
if (result?.error) {
toast.error(result.error.message || errorMessage, {
description: result.error.description || "Please try again later",
});
return;
}
for (const action of after) {
await action();
}
} catch (error) {
toast.error(errorMessage, {
description:
error instanceof Error ? error.message : "Please try again later",
});
}
}
async fetchNotifications() {
this.loading = true;
try {
const result = await getNotificationsSQ(this.getFetchQueryInput());
if (result?.error || !result?.data) {
toast.error(
result?.error?.message || "Failed to fetch notifications",
{
description:
result?.error?.description || "Please try again later",
},
);
return;
}
this.notifications = result.data.data as Notifications;
this.pagination.total = result.data.total;
this.pagination.totalPages = result.data.totalPages;
} catch (error) {
toast.error("Failed to fetch notifications", {
description:
error instanceof Error ? error.message : "Please try again later",
});
} finally {
this.loading = false;
}
}
async markAsRead(notificationIds: number[]) {
await this.runCommand(markReadSC, notificationIds, "Failed to mark as read", [
() => this.fetchNotifications(),
() => this.fetchUnreadCount(),
]);
}
async markAsUnread(notificationIds: number[]) {
await this.runCommand(
markUnreadSC,
notificationIds,
"Failed to mark as unread",
[() => this.fetchNotifications(), () => this.fetchUnreadCount()],
);
}
async archive(notificationIds: number[]) {
await this.runCommand(archiveSC, notificationIds, "Failed to archive", [
() => this.fetchNotifications(),
]);
}
async unarchive(notificationIds: number[]) {
await this.runCommand(unarchiveSC, notificationIds, "Failed to unarchive", [
() => this.fetchNotifications(),
]);
}
async deleteNotifications(notificationIds: number[]) {
await this.runCommand(
deleteNotificationsSC,
notificationIds,
"Failed to delete",
[() => this.fetchNotifications(), () => this.fetchUnreadCount()],
);
}
async markAllAsRead() {
try {
const result = await markAllReadSC({});
if (result?.error) {
toast.error(result.error.message || "Failed to mark all as read", {
description: result.error.description || "Please try again later",
});
return;
}
await this.fetchNotifications();
await this.fetchUnreadCount();
} catch (error) {
toast.error("Failed to mark all as read", {
description:
error instanceof Error ? error.message : "Please try again later",
});
}
}
async fetchUnreadCount() {
try {
const result = await getUnreadCountSQ();
if (result?.error) {
return;
}
if (result?.data !== undefined && result?.data !== null) {
this.unreadCount = result.data as number;
}
} catch {
// Intentionally silent - unread count is non-critical UI data.
}
}
toggleSelection(id: number) {
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
} else {
this.selectedIds.add(id);
}
}
selectAll() {
this.notifications.forEach((n) => this.selectedIds.add(n.id));
}
clearSelection() {
this.selectedIds.clear();
}
goToPage(page: number) {
this.pagination.page = page;
this.fetchNotifications();
}
setPageSize(pageSize: number) {
this.pagination.pageSize = pageSize;
this.pagination.page = 1;
this.fetchNotifications();
}
setSorting(
sortBy: ClientPaginationState["sortBy"],
sortOrder: ClientPaginationState["sortOrder"],
) {
this.pagination.sortBy = sortBy;
this.pagination.sortOrder = sortOrder;
this.pagination.page = 1;
this.fetchNotifications();
}
setFilters(newFilters: Partial<ClientNotificationFilters>) {
this.filters = { ...this.filters, ...newFilters };
this.pagination.page = 1;
this.fetchNotifications();
}
clearFilters() {
this.filters = { userId: get(user)?.id!, isArchived: false };
this.pagination.page = 1;
this.fetchNotifications();
}
}
export const notificationViewModel = new NotificationViewModel();