537 lines
19 KiB
TypeScript
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();
|