wip: notifications

This commit is contained in:
Nikolai Giman
2026-03-22 20:01:05 +01:00
parent 5c4859743e
commit c403673f4d
108 changed files with 5662 additions and 582 deletions
@@ -1,14 +1,25 @@
import TvApiBase from './base';
import type { AppResponse } from '@/api/base.types';
import type { KanbanArgAddColumn, KanbanArgDeleteColumn, KanbanArgUpdateColumn, KanbanArgUpdateTasksOrder, KanbanColumnItem, KanbanResponseDeleteColumn, KanbanResponseTasksForColumn, KanbanResponseTasksOrderForColumnAndCursor, KanbanResponseUpdateColumn, KanbanResponseUpdateTasksOrder } from './kanban.types';
import type { KanbanArgAddColumn, KanbanArgDeleteColumn, KanbanArgUpdateColumn, KanbanArgUpdateTasksOrder, KanbanColumnItem, KanbanFilters, KanbanResponseDeleteColumn, KanbanResponseTasksForColumn, KanbanResponseTasksOrderForColumnAndCursor, KanbanResponseUpdateColumn, KanbanResponseUpdateTasksOrder } from './kanban.types';
import type { Task } from './tasks.api.types';
export default class TvKanban extends TvApiBase {
protected moduleUrl = '/module/kanban';
public async fetchTasksForColumn(goalId: number, columnId: number | null, cursor: string | number | null) {
public async fetchTasksForColumn(goalId: number, columnId: number | null, cursor: string | number | null, filters?: KanbanFilters) {
const params = new URLSearchParams();
if (filters) {
if (filters.listIds && filters.listIds.length > 0) {
params.set('listIds', filters.listIds.join(','));
}
if (filters.assigneeIds && filters.assigneeIds.length > 0) {
params.set('assigneeIds', filters.assigneeIds.join(','));
}
}
const query = params.toString();
const url = `${this.moduleUrl}/tasks/${goalId}/${columnId}/${cursor}${query ? `?${query}` : ''}`;
return this.request(
this.$axios.get<AppResponse<KanbanResponseTasksForColumn>>(`${this.moduleUrl}/tasks/${goalId}/${columnId}/${cursor}`)
this.$axios.get<AppResponse<KanbanResponseTasksForColumn>>(url)
);
}
@@ -1,5 +1,10 @@
import type { Task } from "./tasks.api.types";
export type KanbanFilters = {
listIds?: number[];
assigneeIds?: number[];
};
export type KanbanColumnItem = {
id: number;
name: string;
@@ -1,7 +1,22 @@
export enum NotificationType {
DEADLINE = 'deadline',
ASSIGN = 'assign',
MENTION = 'mention',
COMMENT = 'comment',
STATUS_CHANGE = 'status_change',
}
export enum NotificationChannel {
PUSH = 'push',
WEBSOCKET = 'websocket',
EMAIL = 'email',
}
export interface Notification {
id: number;
userId: number;
taskId: number | null;
type: NotificationType;
title: string;
body: string | null;
read: boolean;
@@ -20,3 +35,20 @@ export type NotificationResponseConnectionToken = {
token: string | null;
url: string | null;
};
type Minutes = string;
type IsEnabled = boolean;
export interface NotificationPreferencesSettings {
global?: Partial<Record<NotificationType, {
channels?: Partial<Record<NotificationChannel, IsEnabled>>;
intervals?: Record<Minutes, IsEnabled>;
}>>;
projects?: Record<string, Partial<Record<NotificationType, {
channels?: Partial<Record<NotificationChannel, IsEnabled>>;
intervals?: Record<Minutes, IsEnabled>;
}>>>;
}
export type NotificationResponsePreferences = {
settings: NotificationPreferencesSettings;
};
@@ -1,6 +1,6 @@
import TvApiBase from "./base";
import type { AppResponse } from "./base.types";
import type { NotificationResponseFetch, NotificationResponseMarkRead, NotificationResponseConnectionToken } from "./notifications.api.types";
import type { NotificationResponseFetch, NotificationResponseMarkRead, NotificationResponseConnectionToken, NotificationResponsePreferences, NotificationPreferencesSettings } from "./notifications.api.types";
export default class TvNotificationsApi extends TvApiBase {
protected moduleUrl = '/module/notifications';
@@ -36,4 +36,36 @@ export default class TvNotificationsApi extends TvApiBase {
)
);
}
public async registerDevice(token: string, platform: 'android' | 'ios', timezone: string) {
return this.request(
this.$axios.post<AppResponse<boolean>>(
`${this.moduleUrl}/device/register`, { token, platform, timezone }
)
);
}
public async unregisterDevice(token: string) {
return this.request(
this.$axios.post<AppResponse<boolean>>(
`${this.moduleUrl}/device/unregister`, { token }
)
);
}
public async getPreferences() {
return this.request(
this.$axios.get<AppResponse<NotificationResponsePreferences>>(
`${this.moduleUrl}/preferences`
)
);
}
public async savePreferences(settings: NotificationPreferencesSettings) {
return this.request(
this.$axios.put<AppResponse<boolean>>(
`${this.moduleUrl}/preferences`, { settings }
)
);
}
}
@@ -0,0 +1,62 @@
import TvApiBase from './base';
import type { AppResponse } from '@/api/base.types';
import type {
WebhookArgCreate,
WebhookArgDelete,
WebhookArgUpdate,
WebhookDeliveryItem,
WebhookItem,
WebhookResponseCreate,
} from './webhooks.types';
export default class TvWebhooks extends TvApiBase {
protected moduleUrl = '/module/webhooks';
public async fetch(goalId: number) {
return this.request(
this.$axios.get<AppResponse<WebhookItem[]>>(`${this.moduleUrl}`, { params: { goalId } })
);
}
public async create(data: WebhookArgCreate) {
return this.request(
this.$axios.post<AppResponse<WebhookResponseCreate>>(`${this.moduleUrl}`, data)
);
}
public async update(data: WebhookArgUpdate) {
return this.request(
this.$axios.patch<AppResponse<WebhookItem>>(`${this.moduleUrl}`, data)
);
}
public async delete(data: WebhookArgDelete) {
return this.request(
this.$axios.delete<AppResponse<boolean>>(`${this.moduleUrl}`, { data })
);
}
public async rotateSecret(id: number) {
return this.request(
this.$axios.post<AppResponse<{ secret: string }>>(`${this.moduleUrl}/rotate-secret`, { id })
);
}
public async testDelivery(id: number) {
return this.request(
this.$axios.post<AppResponse<{ success: boolean; responseCode?: number }>>(`${this.moduleUrl}/test`, { id })
);
}
public async fetchDeliveries(webhookId: number, options?: { cursor?: number; status?: string }) {
return this.request(
this.$axios.get<AppResponse<WebhookDeliveryItem[]>>(`${this.moduleUrl}/deliveries/${webhookId}`, { params: options })
);
}
public async retryDelivery(deliveryId: number) {
return this.request(
this.$axios.post<AppResponse<{ success: boolean; responseCode?: number }>>(`${this.moduleUrl}/retry`, { id: deliveryId })
);
}
}
@@ -0,0 +1,52 @@
export type WebhookItem = {
id: number;
goalId: number;
url: string;
events: string[];
isActive: boolean;
createdAt: string | null;
updatedAt: string | null;
};
export type WebhookArgCreate = {
goalId: number;
url: string;
events: string[];
};
export type WebhookResponseCreate = {
webhook: WebhookItem;
secret: string;
};
export type WebhookArgUpdate = {
id: number;
url?: string;
events?: string[];
isActive?: boolean;
};
export type WebhookArgDelete = {
id: number;
};
export type WebhookDeliveryItem = {
id: number;
webhookId: number;
event: string;
payload: unknown;
status: string;
responseCode: number | null;
attempts: number;
lastAttemptAt: string | null;
createdAt: string | null;
};
export const WEBHOOK_EVENTS = [
'task.created',
'task.updated',
'task.deleted',
'task.assigneesChanged',
] as const;
export type WebhookEvent = typeof WEBHOOK_EVENTS[number];