wip
This commit is contained in:
@@ -113,6 +113,7 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
rootPath: join(__dirname, '../../..', 'public'),
|
||||
serveRoot: '/public',
|
||||
}),
|
||||
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: '.env',
|
||||
load: config,
|
||||
|
||||
@@ -11,24 +11,56 @@ import {
|
||||
HttpStatus,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import { IsNumber, IsString, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { GetNotificationsService } from './queries/GetNotifications.service';
|
||||
import { CreateNotificationService } from './commands/CreateNotification.service';
|
||||
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { IsOptional } from '@/common/decorators/Validators';
|
||||
|
||||
class MarkAsReadDto {
|
||||
notificationId: number;
|
||||
}
|
||||
|
||||
class GetNotificationsQueryDto {
|
||||
limit?: string;
|
||||
offset?: string;
|
||||
@ApiPropertyOptional({ description: 'Number of notifications per page', example: 20 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
limit?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Offset for pagination', example: 0 })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
offset?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter to only unread notifications' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['true', 'false'])
|
||||
unreadOnly?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by notification category' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
category?: string;
|
||||
}
|
||||
|
||||
@Controller('notifications')
|
||||
@ApiTags('Notifications')
|
||||
@ApiCommonHeaders()
|
||||
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||
export class NotificationsController {
|
||||
constructor(
|
||||
@@ -43,11 +75,43 @@ export class NotificationsController {
|
||||
* @returns {Promise<{ notifications: Notification[]; total: number; unreadCount: number }>}
|
||||
*/
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Retrieves the list of notifications for the current user.',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'limit',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Number of notifications per page',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'offset',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Offset for pagination',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'unreadOnly',
|
||||
required: false,
|
||||
type: Boolean,
|
||||
description: 'Filter to only unread notifications',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'category',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'Filter by notification category',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The notifications list has been successfully retrieved.',
|
||||
})
|
||||
async getNotifications(@Query() query: GetNotificationsQueryDto) {
|
||||
const user = await this.tenancyContext.getSystemUser();
|
||||
const options = {
|
||||
limit: query.limit ? parseInt(query.limit, 10) : 20,
|
||||
offset: query.offset ? parseInt(query.offset, 10) : 0,
|
||||
limit: query.limit ?? 20,
|
||||
offset: query.offset ?? 0,
|
||||
unreadOnly: query.unreadOnly === 'true',
|
||||
category: query.category,
|
||||
};
|
||||
@@ -60,6 +124,15 @@ export class NotificationsController {
|
||||
* @returns {Promise<{ count: number }>}
|
||||
*/
|
||||
@Get('unread-count')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Gets the count of unread notifications for the current user.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The unread notifications count has been successfully retrieved.',
|
||||
})
|
||||
async getUnreadCount() {
|
||||
const user = await this.tenancyContext.getSystemUser();
|
||||
const count = await this.getNotificationsService.getUnreadCount(user.id);
|
||||
@@ -73,6 +146,16 @@ export class NotificationsController {
|
||||
*/
|
||||
@Post(':id/read')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Marks a notification as read.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
type: Number,
|
||||
description: 'The notification id',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The notification has been successfully marked as read.',
|
||||
})
|
||||
async markAsRead(@Param('id', ParseIntPipe) notificationId: number) {
|
||||
const user = await this.tenancyContext.getSystemUser();
|
||||
const notification = await this.createNotificationService.markAsRead(
|
||||
@@ -88,6 +171,15 @@ export class NotificationsController {
|
||||
*/
|
||||
@Post('read-all')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Marks all notifications as read for the current user.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'All notifications have been successfully marked as read.',
|
||||
})
|
||||
async markAllAsRead() {
|
||||
const user = await this.tenancyContext.getSystemUser();
|
||||
const markedAsRead = await this.createNotificationService.markAllAsRead(
|
||||
@@ -103,6 +195,16 @@ export class NotificationsController {
|
||||
*/
|
||||
@Post(':id/delete')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Deletes a notification.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
type: Number,
|
||||
description: 'The notification id',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The notification has been successfully deleted.',
|
||||
})
|
||||
async deleteNotification(@Param('id', ParseIntPipe) notificationId: number) {
|
||||
const user = await this.tenancyContext.getSystemUser();
|
||||
const deleted = await this.createNotificationService.deleteNotification(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Notification } from '../models/Notification.model';
|
||||
|
||||
export interface CreateNotificationPayload {
|
||||
@@ -12,13 +13,18 @@ export interface CreateNotificationPayload {
|
||||
|
||||
@Injectable()
|
||||
export class CreateNotificationService {
|
||||
constructor(
|
||||
@Inject(Notification.name)
|
||||
private readonly notificationModel: TenantModelProxy<typeof Notification>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new notification.
|
||||
* @param {CreateNotificationPayload} payload - Notification data
|
||||
* @returns {Promise<Notification>}
|
||||
*/
|
||||
async createNotification(payload: CreateNotificationPayload): Promise<Notification> {
|
||||
const notification = await Notification.query().insert({
|
||||
const notification = await this.notificationModel().query().insert({
|
||||
userId: payload.userId ?? null,
|
||||
title: payload.title,
|
||||
message: payload.message,
|
||||
@@ -38,7 +44,8 @@ export class CreateNotificationService {
|
||||
* @returns {Promise<Notification | null>}
|
||||
*/
|
||||
async markAsRead(notificationId: number, userId: number): Promise<Notification | null> {
|
||||
const notification = await Notification.query()
|
||||
const notification = await this.notificationModel()
|
||||
.query()
|
||||
.findById(notificationId)
|
||||
.modify('forUser', userId);
|
||||
|
||||
@@ -46,11 +53,12 @@ export class CreateNotificationService {
|
||||
return null;
|
||||
}
|
||||
|
||||
await Notification.query()
|
||||
await this.notificationModel()
|
||||
.query()
|
||||
.findById(notificationId)
|
||||
.patch({ readAt: new Date() });
|
||||
|
||||
return Notification.query().findById(notificationId);
|
||||
return this.notificationModel().query().findById(notificationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,7 +67,8 @@ export class CreateNotificationService {
|
||||
* @returns {Promise<number>} - Number of notifications marked as read
|
||||
*/
|
||||
async markAllAsRead(userId: number): Promise<number> {
|
||||
const result = await Notification.query()
|
||||
const result = await this.notificationModel()
|
||||
.query()
|
||||
.modify('forUser', userId)
|
||||
.modify('unread')
|
||||
.patch({ readAt: new Date() });
|
||||
@@ -74,7 +83,8 @@ export class CreateNotificationService {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteNotification(notificationId: number, userId: number): Promise<boolean> {
|
||||
const notification = await Notification.query()
|
||||
const notification = await this.notificationModel()
|
||||
.query()
|
||||
.findById(notificationId)
|
||||
.modify('forUser', userId);
|
||||
|
||||
@@ -82,7 +92,7 @@ export class CreateNotificationService {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Notification.query().deleteById(notificationId);
|
||||
await this.notificationModel().query().deleteById(notificationId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Model } from 'objection';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export class Notification extends TenantBaseModel {
|
||||
@@ -92,27 +91,6 @@ export class Notification extends TenantBaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const { User } = require('@/modules/UsersModule/models/User.model');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Notification belongs to a user.
|
||||
*/
|
||||
user: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: User,
|
||||
join: {
|
||||
from: 'notifications.userId',
|
||||
to: 'users.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON schema for validation.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Notification } from '../models/Notification.model';
|
||||
|
||||
interface GetNotificationsOptions {
|
||||
@@ -10,6 +11,11 @@ interface GetNotificationsOptions {
|
||||
|
||||
@Injectable()
|
||||
export class GetNotificationsService {
|
||||
constructor(
|
||||
@Inject(Notification.name)
|
||||
private readonly notificationModel: TenantModelProxy<typeof Notification>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves notifications for a user with pagination and filtering.
|
||||
* @param {number} userId - The user ID
|
||||
@@ -23,7 +29,8 @@ export class GetNotificationsService {
|
||||
const { limit = 20, offset = 0, unreadOnly = false, category } = options;
|
||||
|
||||
// Build base query for user's notifications
|
||||
let query = Notification.query()
|
||||
let query = this.notificationModel()
|
||||
.query()
|
||||
.modify('forUser', userId)
|
||||
.modify('newestFirst');
|
||||
|
||||
@@ -38,7 +45,8 @@ export class GetNotificationsService {
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
const countQuery = Notification.query()
|
||||
const countQuery = this.notificationModel()
|
||||
.query()
|
||||
.modify('forUser', userId)
|
||||
.modify(unreadOnly ? 'unread' : 'newestFirst');
|
||||
|
||||
@@ -65,7 +73,8 @@ export class GetNotificationsService {
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async getUnreadCount(userId: number): Promise<number> {
|
||||
return Notification.query()
|
||||
return this.notificationModel()
|
||||
.query()
|
||||
.modify('forUser', userId)
|
||||
.modify('unread')
|
||||
.resultSize();
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"camelcase": "^5.3.1",
|
||||
"classnames": "^2.3.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepdash": "^5.3.9",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
|
||||
@@ -51,6 +51,7 @@ export * from './generic-resource';
|
||||
export * from './cashflow-accounts';
|
||||
export * from './bank-rules';
|
||||
export * from './misc';
|
||||
export * from './notifications';
|
||||
export * from './reports';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ApiFetcher } from './fetch-utils';
|
||||
import { rawRequest } from './fetch-utils';
|
||||
|
||||
export const NOTIFICATIONS_ROUTES = {
|
||||
LIST: '/api/notifications',
|
||||
UNREAD_COUNT: '/api/notifications/unread-count',
|
||||
MARK_AS_READ: '/api/notifications/{id}/read',
|
||||
MARK_ALL_AS_READ: '/api/notifications/read-all',
|
||||
DELETE: '/api/notifications/{id}/delete',
|
||||
} as const;
|
||||
|
||||
export interface GetNotificationsQuery {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
unreadOnly?: boolean;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export async function fetchNotifications(
|
||||
fetcher: ApiFetcher,
|
||||
query?: GetNotificationsQuery
|
||||
): Promise<unknown> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.limit !== undefined) params.set('limit', String(query.limit));
|
||||
if (query?.offset !== undefined) params.set('offset', String(query.offset));
|
||||
if (query?.unreadOnly !== undefined) params.set('unreadOnly', String(query.unreadOnly));
|
||||
if (query?.category) params.set('category', query.category);
|
||||
|
||||
const qs = params.toString();
|
||||
const path = qs ? `${NOTIFICATIONS_ROUTES.LIST}?${qs}` : NOTIFICATIONS_ROUTES.LIST;
|
||||
return rawRequest(fetcher, 'GET', path);
|
||||
}
|
||||
|
||||
export async function fetchUnreadNotificationsCount(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'GET', NOTIFICATIONS_ROUTES.UNREAD_COUNT);
|
||||
}
|
||||
|
||||
export async function markNotificationAsRead(
|
||||
fetcher: ApiFetcher,
|
||||
id: number
|
||||
): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'POST', `/api/notifications/${id}/read`);
|
||||
}
|
||||
|
||||
export async function markAllNotificationsAsRead(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'POST', NOTIFICATIONS_ROUTES.MARK_ALL_AS_READ);
|
||||
}
|
||||
|
||||
export async function deleteNotification(
|
||||
fetcher: ApiFetcher,
|
||||
id: number
|
||||
): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'POST', `/api/notifications/${id}/delete`);
|
||||
}
|
||||
Reference in New Issue
Block a user