feat(notifications): overdue sale invoice job and notification fixes
- Add daily cron to notify invoice owners when delivered invoices become overdue - Fix TenantNotification jsonSchema/jsonAttributes for readAt patches (MySQL + Objection) - Register TenantNotification model; adjust notifications API and drawer item UI Made-with: Cursor
This commit is contained in:
@@ -9,8 +9,9 @@ import {
|
|||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Inject,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { IsIn, IsInt, IsString, Min } from 'class-validator';
|
||||||
|
import { ToNumber, IsOptional } from '@/common/decorators/Validators';
|
||||||
import { GetNotificationsService } from './queries/GetNotifications.service';
|
import { GetNotificationsService } from './queries/GetNotifications.service';
|
||||||
import { CreateNotificationService } from './commands/CreateNotification.service';
|
import { CreateNotificationService } from './commands/CreateNotification.service';
|
||||||
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
@@ -22,9 +23,24 @@ class MarkAsReadDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GetNotificationsQueryDto {
|
class GetNotificationsQueryDto {
|
||||||
limit?: string;
|
@IsOptional()
|
||||||
offset?: string;
|
@ToNumber()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
limit?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ToNumber()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
offset?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['true', 'false'])
|
||||||
unreadOnly?: string;
|
unreadOnly?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
category?: string;
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +62,8 @@ export class NotificationsController {
|
|||||||
async getNotifications(@Query() query: GetNotificationsQueryDto) {
|
async getNotifications(@Query() query: GetNotificationsQueryDto) {
|
||||||
const user = await this.tenancyContext.getSystemUser();
|
const user = await this.tenancyContext.getSystemUser();
|
||||||
const options = {
|
const options = {
|
||||||
limit: query.limit ? parseInt(query.limit, 10) : 20,
|
limit: query.limit ?? 20,
|
||||||
offset: query.offset ? parseInt(query.offset, 10) : 0,
|
offset: query.offset ?? 0,
|
||||||
unreadOnly: query.unreadOnly === 'true',
|
unreadOnly: query.unreadOnly === 'true',
|
||||||
category: query.category,
|
category: query.category,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||||
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
|
||||||
import { SocketModule } from '../Socket/Socket.module';
|
import { SocketModule } from '../Socket/Socket.module';
|
||||||
import { Notification } from './models/Notification.model';
|
|
||||||
import { NotificationsController } from './Notifications.controller';
|
import { NotificationsController } from './Notifications.controller';
|
||||||
import { GetNotificationsService } from './queries/GetNotifications.service';
|
import { GetNotificationsService } from './queries/GetNotifications.service';
|
||||||
import { CreateNotificationService } from './commands/CreateNotification.service';
|
import { CreateNotificationService } from './commands/CreateNotification.service';
|
||||||
import { InventoryCostNotificationsSubscriber } from './subscribers/InventoryCostNotifications.subscriber';
|
import { InventoryCostNotificationsSubscriber } from './subscribers/InventoryCostNotifications.subscriber';
|
||||||
|
import { OverdueSaleInvoiceNotificationsJob } from './jobs/OverdueSaleInvoiceNotifications.job';
|
||||||
const models = [RegisterTenancyModel(Notification)];
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TenancyDatabaseModule, SocketModule, ...models],
|
imports: [TenancyDatabaseModule, SocketModule],
|
||||||
controllers: [NotificationsController],
|
controllers: [NotificationsController],
|
||||||
providers: [
|
providers: [
|
||||||
GetNotificationsService,
|
GetNotificationsService,
|
||||||
CreateNotificationService,
|
CreateNotificationService,
|
||||||
InventoryCostNotificationsSubscriber,
|
InventoryCostNotificationsSubscriber,
|
||||||
|
OverdueSaleInvoiceNotificationsJob,
|
||||||
TenancyContext,
|
TenancyContext,
|
||||||
],
|
],
|
||||||
exports: [GetNotificationsService, CreateNotificationService, ...models],
|
exports: [GetNotificationsService, CreateNotificationService],
|
||||||
})
|
})
|
||||||
export class NotificationsModule {}
|
export class NotificationsModule {}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Notification } from '../models/Notification.model';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantNotification } from '../models/Notification.model';
|
||||||
|
|
||||||
export interface CreateNotificationPayload {
|
export interface CreateNotificationPayload {
|
||||||
userId?: number | null;
|
userId?: number | null;
|
||||||
@@ -12,13 +13,18 @@ export interface CreateNotificationPayload {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateNotificationService {
|
export class CreateNotificationService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantNotification.name)
|
||||||
|
private readonly notificationModel: TenantModelProxy<typeof TenantNotification>,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new notification.
|
* Creates a new notification.
|
||||||
* @param {CreateNotificationPayload} payload - Notification data
|
* @param {CreateNotificationPayload} payload - Notification data
|
||||||
* @returns {Promise<Notification>}
|
* @returns {Promise<TenantNotification>}
|
||||||
*/
|
*/
|
||||||
async createNotification(payload: CreateNotificationPayload): Promise<Notification> {
|
async createNotification(payload: CreateNotificationPayload): Promise<TenantNotification> {
|
||||||
const notification = await Notification.query().insert({
|
const notification = await this.notificationModel().query().insert({
|
||||||
userId: payload.userId ?? null,
|
userId: payload.userId ?? null,
|
||||||
title: payload.title,
|
title: payload.title,
|
||||||
message: payload.message,
|
message: payload.message,
|
||||||
@@ -35,10 +41,11 @@ export class CreateNotificationService {
|
|||||||
* Marks a notification as read.
|
* Marks a notification as read.
|
||||||
* @param {number} notificationId - The notification ID
|
* @param {number} notificationId - The notification ID
|
||||||
* @param {number} userId - The user ID (for authorization)
|
* @param {number} userId - The user ID (for authorization)
|
||||||
* @returns {Promise<Notification | null>}
|
* @returns {Promise<TenantNotification | null>}
|
||||||
*/
|
*/
|
||||||
async markAsRead(notificationId: number, userId: number): Promise<Notification | null> {
|
async markAsRead(notificationId: number, userId: number): Promise<TenantNotification | null> {
|
||||||
const notification = await Notification.query()
|
const notification = await this.notificationModel()
|
||||||
|
.query()
|
||||||
.findById(notificationId)
|
.findById(notificationId)
|
||||||
.modify('forUser', userId);
|
.modify('forUser', userId);
|
||||||
|
|
||||||
@@ -46,11 +53,12 @@ export class CreateNotificationService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Notification.query()
|
await this.notificationModel()
|
||||||
|
.query()
|
||||||
.findById(notificationId)
|
.findById(notificationId)
|
||||||
.patch({ readAt: new Date() });
|
.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
|
* @returns {Promise<number>} - Number of notifications marked as read
|
||||||
*/
|
*/
|
||||||
async markAllAsRead(userId: number): Promise<number> {
|
async markAllAsRead(userId: number): Promise<number> {
|
||||||
const result = await Notification.query()
|
const result = await this.notificationModel()
|
||||||
|
.query()
|
||||||
.modify('forUser', userId)
|
.modify('forUser', userId)
|
||||||
.modify('unread')
|
.modify('unread')
|
||||||
.patch({ readAt: new Date() });
|
.patch({ readAt: new Date() });
|
||||||
@@ -74,7 +83,8 @@ export class CreateNotificationService {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async deleteNotification(notificationId: number, userId: number): Promise<boolean> {
|
async deleteNotification(notificationId: number, userId: number): Promise<boolean> {
|
||||||
const notification = await Notification.query()
|
const notification = await this.notificationModel()
|
||||||
|
.query()
|
||||||
.findById(notificationId)
|
.findById(notificationId)
|
||||||
.modify('forUser', userId);
|
.modify('forUser', userId);
|
||||||
|
|
||||||
@@ -82,7 +92,7 @@ export class CreateNotificationService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Notification.query().deleteById(notificationId);
|
await this.notificationModel().query().deleteById(notificationId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||||
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { CreateNotificationService } from '../commands/CreateNotification.service';
|
||||||
|
import { SocketGateway } from '@/modules/Socket/Socket.gateway';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once per day: notify invoice authors when a delivered, still-due invoice’s due date
|
||||||
|
* was yesterday (first calendar day overdue).
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class OverdueSaleInvoiceNotificationsJob {
|
||||||
|
private readonly logger = new Logger(OverdueSaleInvoiceNotificationsJob.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly clsService: ClsService,
|
||||||
|
@Inject(TenantModel.name)
|
||||||
|
private readonly tenantModel: typeof TenantModel,
|
||||||
|
@Inject(SaleInvoice.name)
|
||||||
|
private readonly saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
|
||||||
|
private readonly createNotificationService: CreateNotificationService,
|
||||||
|
private readonly socketGateway: SocketGateway,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Cron('0 8 * * *')
|
||||||
|
async notifyNewlyOverdueInvoices() {
|
||||||
|
const yesterday = moment().subtract(1, 'day').format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const tenants = await this.tenantModel
|
||||||
|
.query()
|
||||||
|
.whereNotNull('seededAt')
|
||||||
|
.select('organizationId');
|
||||||
|
|
||||||
|
for (const tenant of tenants) {
|
||||||
|
await this.clsService.run(async () => {
|
||||||
|
this.clsService.set('organizationId', tenant.organizationId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const invoices = await this.saleInvoiceModel()
|
||||||
|
.query()
|
||||||
|
.modify('delivered')
|
||||||
|
.modify('dueInvoices')
|
||||||
|
.where('dueDate', yesterday);
|
||||||
|
|
||||||
|
for (const invoice of invoices) {
|
||||||
|
const userId = invoice.userId;
|
||||||
|
if (!userId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = await this.createNotificationService.createNotification({
|
||||||
|
userId,
|
||||||
|
title: 'Invoice overdue',
|
||||||
|
message: `Invoice ${invoice.invoiceNo} is now overdue.`,
|
||||||
|
type: 'warning',
|
||||||
|
category: 'billing',
|
||||||
|
metadata: {
|
||||||
|
saleInvoiceId: invoice.id,
|
||||||
|
invoiceNo: invoice.invoiceNo,
|
||||||
|
dueDate: invoice.dueDate,
|
||||||
|
event: 'saleInvoice.becameOverdue',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socketGateway.emitNotification(userId, notification);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Overdue invoice notifications failed for tenant ${tenant.organizationId}`,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantUser } from '../../Tenancy/TenancyModels/models/TenantUser.model';
|
||||||
|
|
||||||
export class Notification extends TenantBaseModel {
|
export class TenantNotification extends TenantBaseModel {
|
||||||
public id!: number;
|
public id!: number;
|
||||||
public userId!: number | null;
|
public userId!: number | null;
|
||||||
public title!: string;
|
public title!: string;
|
||||||
@@ -9,7 +10,7 @@ export class Notification extends TenantBaseModel {
|
|||||||
public type!: 'success' | 'info' | 'warning' | 'error';
|
public type!: 'success' | 'info' | 'warning' | 'error';
|
||||||
public category!: 'inventory' | 'billing' | 'system' | 'export' | 'report';
|
public category!: 'inventory' | 'billing' | 'system' | 'export' | 'report';
|
||||||
public metadata!: Record<string, any> | null;
|
public metadata!: Record<string, any> | null;
|
||||||
public readAt!: Date | null;
|
public readAt!: Date | string | null;
|
||||||
public createdAt!: Date;
|
public createdAt!: Date;
|
||||||
public updatedAt!: Date;
|
public updatedAt!: Date;
|
||||||
|
|
||||||
@@ -20,6 +21,14 @@ export class Notification extends TenantBaseModel {
|
|||||||
return 'notifications';
|
return 'notifications';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON columns only. Without this, Objection infers JSON attributes from jsonSchema; `readAt`
|
||||||
|
* would match `type: object` in anyOf and get JSON.stringify'd, breaking MySQL DATETIME updates.
|
||||||
|
*/
|
||||||
|
static get jsonAttributes() {
|
||||||
|
return ['metadata'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamps columns.
|
* Timestamps columns.
|
||||||
*/
|
*/
|
||||||
@@ -96,15 +105,13 @@ export class Notification extends TenantBaseModel {
|
|||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
const { User } = require('@/modules/UsersModule/models/User.model');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* Notification belongs to a user.
|
* Notification belongs to a tenant user (`users` in the tenant database).
|
||||||
*/
|
*/
|
||||||
user: {
|
user: {
|
||||||
relation: Model.BelongsToOneRelation,
|
relation: Model.BelongsToOneRelation,
|
||||||
modelClass: User,
|
modelClass: TenantUser,
|
||||||
join: {
|
join: {
|
||||||
from: 'notifications.userId',
|
from: 'notifications.userId',
|
||||||
to: 'users.id',
|
to: 'users.id',
|
||||||
@@ -128,7 +135,14 @@ export class Notification extends TenantBaseModel {
|
|||||||
type: { type: 'string', enum: ['success', 'info', 'warning', 'error'] },
|
type: { type: 'string', enum: ['success', 'info', 'warning', 'error'] },
|
||||||
category: { type: 'string', enum: ['inventory', 'billing', 'system', 'export', 'report'] },
|
category: { type: 'string', enum: ['inventory', 'billing', 'system', 'export', 'report'] },
|
||||||
metadata: { type: ['object', 'null'] },
|
metadata: { type: ['object', 'null'] },
|
||||||
readAt: { type: ['string', 'null'], format: 'date-time' },
|
// `Date` is allowed on patch; mysql2 binds it as DATETIME (see jsonAttributes above).
|
||||||
|
readAt: {
|
||||||
|
anyOf: [
|
||||||
|
{ type: 'null' },
|
||||||
|
{ type: 'string', format: 'date-time' },
|
||||||
|
{ type: 'object' },
|
||||||
|
],
|
||||||
|
},
|
||||||
createdAt: { type: 'string', format: 'date-time' },
|
createdAt: { type: 'string', format: 'date-time' },
|
||||||
updatedAt: { type: 'string', format: 'date-time' },
|
updatedAt: { type: 'string', format: 'date-time' },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Notification } from '../models/Notification.model';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { TenantNotification } from '../models/Notification.model';
|
||||||
|
|
||||||
interface GetNotificationsOptions {
|
interface GetNotificationsOptions {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -10,20 +11,26 @@ interface GetNotificationsOptions {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetNotificationsService {
|
export class GetNotificationsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(TenantNotification.name)
|
||||||
|
private readonly notificationModel: TenantModelProxy<typeof TenantNotification>,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves notifications for a user with pagination and filtering.
|
* Retrieves notifications for a user with pagination and filtering.
|
||||||
* @param {number} userId - The user ID
|
* @param {number} userId - The user ID
|
||||||
* @param {GetNotificationsOptions} options - Query options
|
* @param {GetNotificationsOptions} options - Query options
|
||||||
* @returns {Promise<{ notifications: Notification[]; total: number; unreadCount: number }>}
|
* @returns {Promise<{ notifications: TenantNotification[]; total: number; unreadCount: number }>}
|
||||||
*/
|
*/
|
||||||
async getNotifications(
|
async getNotifications(
|
||||||
userId: number,
|
userId: number,
|
||||||
options: GetNotificationsOptions = {},
|
options: GetNotificationsOptions = {},
|
||||||
): Promise<{ notifications: Notification[]; total: number; unreadCount: number }> {
|
): Promise<{ notifications: TenantNotification[]; total: number; unreadCount: number }> {
|
||||||
const { limit = 20, offset = 0, unreadOnly = false, category } = options;
|
const { limit = 20, offset = 0, unreadOnly = false, category } = options;
|
||||||
|
|
||||||
// Build base query for user's notifications
|
// Build base query for user's notifications
|
||||||
let query = Notification.query()
|
let query = this.notificationModel()
|
||||||
|
.query()
|
||||||
.modify('forUser', userId)
|
.modify('forUser', userId)
|
||||||
.modify('newestFirst');
|
.modify('newestFirst');
|
||||||
|
|
||||||
@@ -38,7 +45,8 @@ export class GetNotificationsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get total count for pagination
|
// Get total count for pagination
|
||||||
const countQuery = Notification.query()
|
const countQuery = this.notificationModel()
|
||||||
|
.query()
|
||||||
.modify('forUser', userId)
|
.modify('forUser', userId)
|
||||||
.modify(unreadOnly ? 'unread' : 'newestFirst');
|
.modify(unreadOnly ? 'unread' : 'newestFirst');
|
||||||
|
|
||||||
@@ -65,7 +73,8 @@ export class GetNotificationsService {
|
|||||||
* @returns {Promise<number>}
|
* @returns {Promise<number>}
|
||||||
*/
|
*/
|
||||||
async getUnreadCount(userId: number): Promise<number> {
|
async getUnreadCount(userId: number): Promise<number> {
|
||||||
return Notification.query()
|
return this.notificationModel()
|
||||||
|
.query()
|
||||||
.modify('forUser', userId)
|
.modify('forUser', userId)
|
||||||
.modify('unread')
|
.modify('unread')
|
||||||
.resultSize();
|
.resultSize();
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { PaymentReceived } from '@/modules/PaymentReceived/models/PaymentReceive
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
import { ClsModule } from 'nestjs-cls';
|
import { ClsModule } from 'nestjs-cls';
|
||||||
import { TenantUser } from './models/TenantUser.model';
|
import { TenantUser } from './models/TenantUser.model';
|
||||||
|
import { TenantNotification } from '@/modules/Notifications/models/Notification.model';
|
||||||
|
|
||||||
const models = [
|
const models = [
|
||||||
Item,
|
Item,
|
||||||
@@ -80,6 +81,7 @@ const models = [
|
|||||||
PaymentReceived,
|
PaymentReceived,
|
||||||
PaymentReceivedEntry,
|
PaymentReceivedEntry,
|
||||||
TenantUser,
|
TenantUser,
|
||||||
|
TenantNotification,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon, Intent, Tooltip, Position } from '@blueprintjs/core';
|
import { Icon, Intent, Tooltip, Position } from '@blueprintjs/core';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import moment from 'moment';
|
||||||
import { Notification } from '@/hooks/query/notifications';
|
import { Notification } from '@/hooks/query/notifications';
|
||||||
import styles from './NotificationsDrawer.module.scss';
|
import styles from './NotificationsDrawer.module.scss';
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export function NotificationItem({ notification, onClick, onDelete }: Notificati
|
|||||||
position={Position.TOP}
|
position={Position.TOP}
|
||||||
>
|
>
|
||||||
<span className={styles.timestamp}>
|
<span className={styles.timestamp}>
|
||||||
{formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}
|
{moment(notification.createdAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user