& so it begins
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user