Files
illusory-mapp/apps/main/src/lib/domains/notifications/notification.vm.svelte.ts
2026-02-28 14:50:04 +02:00

537 lines
19 KiB
TypeScript

import type {
ClientNotificationFilters,
ClientPaginationState,
Notifications,
} from "@pkg/logic/domains/notifications/data";
import { apiClient, user } from "$lib/global.stores";
import { toast } from "svelte-sonner";
import { get } from "svelte/store";
import { ResultAsync, errAsync, okAsync } from "neverthrow";
import type { Err } from "@pkg/result";
class NotificationViewModel {
notifications = $state([] as Notifications);
loading = $state(false);
selectedIds = $state(new Set<number>());
// Pagination state
pagination = $state<ClientPaginationState>({
page: 1,
pageSize: 20,
total: 0,
totalPages: 0,
sortBy: "createdAt",
sortOrder: "desc",
});
// Filter state
filters = $state<ClientNotificationFilters>({
userId: get(user)?.id!,
isArchived: false, // Default to showing non-archived
});
// Stats
unreadCount = $state(0);
async fetchNotifications() {
this.loading = true;
const params = new URLSearchParams();
// Add pagination params
params.append("page", this.pagination.page.toString());
params.append("pageSize", this.pagination.pageSize.toString());
params.append("sortBy", this.pagination.sortBy);
params.append("sortOrder", this.pagination.sortOrder);
// Add filter params
if (this.filters.isRead !== undefined) {
params.append("isRead", this.filters.isRead.toString());
}
if (this.filters.isArchived !== undefined) {
params.append("isArchived", this.filters.isArchived.toString());
}
if (this.filters.type) {
params.append("type", this.filters.type);
}
if (this.filters.category) {
params.append("category", this.filters.category);
}
if (this.filters.priority) {
params.append("priority", this.filters.priority);
}
if (this.filters.search) {
params.append("search", this.filters.search);
}
const result = await ResultAsync.fromPromise(
get(apiClient).notifications.$get({
query: Object.fromEntries(params.entries()),
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to fetch notifications",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to fetch notifications",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error || !apiResult.data) {
return errAsync(
apiResult.error || {
code: "API_ERROR",
message: "Failed to fetch notifications",
description: "Invalid response data",
detail: "Missing data in response",
},
);
}
return okAsync(apiResult.data);
});
result.match(
(data) => {
this.notifications = data.data as any;
this.pagination.total = data.total;
this.pagination.totalPages = data.totalPages;
},
(error) => {
const errorMessage = error.message || "Failed to fetch notifications";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
this.loading = false;
}
async markAsRead(notificationIds: number[]) {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications["mark-read"].$put({
json: { notificationIds },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to mark as read",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to mark as read",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
async () => {
await this.fetchNotifications();
await this.fetchUnreadCount();
},
(error) => {
const errorMessage = error.message || "Failed to mark as read";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
}
async markAsUnread(notificationIds: number[]) {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications["mark-unread"].$put({
json: { notificationIds },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to mark as unread",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to mark as unread",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
async () => {
await this.fetchNotifications();
await this.fetchUnreadCount();
},
(error) => {
const errorMessage = error.message || "Failed to mark as unread";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
}
async archive(notificationIds: number[]) {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications.archive.$put({
json: { notificationIds },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to archive",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to archive",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
async () => {
await this.fetchNotifications();
},
(error) => {
const errorMessage = error.message || "Failed to archive";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
}
async unarchive(notificationIds: number[]) {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications.unarchive.$put({
json: { notificationIds },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to unarchive",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to unarchive",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
async () => {
await this.fetchNotifications();
},
(error) => {
const errorMessage = error.message || "Failed to unarchive";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
}
async deleteNotifications(notificationIds: number[]) {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications.delete.$delete({
json: { notificationIds },
}),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to delete",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to delete",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
async () => {
await this.fetchNotifications();
await this.fetchUnreadCount();
},
(error) => {
const errorMessage = error.message || "Failed to delete";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
}
async markAllAsRead() {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications["mark-all-read"].$put(),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to mark all as read",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to mark all as read",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
async () => {
await this.fetchNotifications();
await this.fetchUnreadCount();
},
(error) => {
const errorMessage = error.message || "Failed to mark all as read";
toast.error(errorMessage, {
description: error.description || "Please try again later",
});
},
);
}
async fetchUnreadCount() {
const result = await ResultAsync.fromPromise(
get(apiClient).notifications["unread-count"].$get(),
(error): Err => ({
code: "NETWORK_ERROR",
message: "Failed to fetch unread count",
description: "Network request failed",
detail: error instanceof Error ? error.message : String(error),
}),
)
.andThen((response) => {
if (!response.ok) {
return errAsync({
code: "API_ERROR",
message: "Failed to fetch unread count",
description: `Response failed with status ${response.status}`,
detail: `HTTP ${response.status}`,
});
}
return ResultAsync.fromPromise(
response.json(),
(error): Err => ({
code: "PARSING_ERROR",
message: "Failed to parse response",
description: "Invalid response format",
detail: error instanceof Error ? error.message : String(error),
}),
);
})
.andThen((apiResult: any) => {
if (apiResult.error) {
return errAsync(apiResult.error);
}
return okAsync(apiResult.data);
});
result.match(
(data) => {
if (data !== undefined && data !== null) {
this.unreadCount = data as number;
}
},
(error) => {
// Silently fail for unread count - don't show toast as it's not critical
console.error("Failed to fetch unread count:", error);
},
);
}
// Helper methods
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; // Reset to first page
this.fetchNotifications();
}
setSorting(
sortBy: ClientPaginationState["sortBy"],
sortOrder: ClientPaginationState["sortOrder"],
) {
this.pagination.sortBy = sortBy;
this.pagination.sortOrder = sortOrder;
this.pagination.page = 1; // Reset to first page
this.fetchNotifications();
}
setFilters(newFilters: Partial<ClientNotificationFilters>) {
this.filters = { ...this.filters, ...newFilters };
this.pagination.page = 1; // Reset to first page
this.fetchNotifications();
}
clearFilters() {
this.filters = { userId: get(user)?.id!, isArchived: false };
this.pagination.page = 1;
this.fetchNotifications();
}
}
export const notificationViewModel = new NotificationViewModel();