Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
H
hrsystem
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
hrsystem
Commits
31c97f0d
Commit
31c97f0d
authored
Apr 01, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 23 files via Son of Anton
parent
fcbd2e22
Changes
23
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
2236 additions
and
0 deletions
+2236
-0
app.module.ts
backend/src/app.module.ts
+9
-0
create-conversation.dto.ts
backend/src/modules/messages/dto/create-conversation.dto.ts
+16
-0
message-response.dto.ts
backend/src/modules/messages/dto/message-response.dto.ts
+35
-0
send-message.dto.ts
backend/src/modules/messages/dto/send-message.dto.ts
+35
-0
messages.controller.ts
backend/src/modules/messages/messages.controller.ts
+99
-0
messages.gateway.ts
backend/src/modules/messages/messages.gateway.ts
+190
-0
messages.module.ts
backend/src/modules/messages/messages.module.ts
+14
-0
messages.service.ts
backend/src/modules/messages/messages.service.ts
+539
-0
create-notice.dto.ts
backend/src/modules/notices/dto/create-notice.dto.ts
+35
-0
notice-response.dto.ts
backend/src/modules/notices/dto/notice-response.dto.ts
+19
-0
update-notice.dto.ts
backend/src/modules/notices/dto/update-notice.dto.ts
+18
-0
notices.controller.ts
backend/src/modules/notices/notices.controller.ts
+76
-0
notices.module.ts
backend/src/modules/notices/notices.module.ts
+10
-0
notices.service.ts
backend/src/modules/notices/notices.service.ts
+336
-0
create-notification.dto.ts
.../src/modules/notifications/dto/create-notification.dto.ts
+82
-0
notification-filter.dto.ts
.../src/modules/notifications/dto/notification-filter.dto.ts
+23
-0
notification-response.dto.ts
...rc/modules/notifications/dto/notification-response.dto.ts
+22
-0
notifications.controller.ts
...end/src/modules/notifications/notifications.controller.ts
+72
-0
notifications.gateway.ts
backend/src/modules/notifications/notifications.gateway.ts
+117
-0
notifications.module.ts
backend/src/modules/notifications/notifications.module.ts
+15
-0
notifications.service.ts
backend/src/modules/notifications/notifications.service.ts
+248
-0
schema-communications.prisma
prisma/schema-communications.prisma
+167
-0
communication.events.ts
shared/src/events/communication.events.ts
+59
-0
No files found.
backend/src/app.module.ts
View file @
31c97f0d
...
...
@@ -33,6 +33,11 @@ import { BountiesModule } from './modules/bounties/bounties.module';
import
{
AdjustmentsModule
}
from
'./modules/adjustments/adjustments.module'
;
import
{
PayrollModule
}
from
'./modules/payroll/payroll.module'
;
// ─── Phase 1E: Communication & Notifications ────────────────
import
{
NotificationsModule
}
from
'./modules/notifications/notifications.module'
;
import
{
MessagesModule
}
from
'./modules/messages/messages.module'
;
import
{
NoticesModule
}
from
'./modules/notices/notices.module'
;
import
{
JwtAuthGuard
}
from
'./common/guards/jwt-auth.guard'
;
import
{
RolesGuard
}
from
'./common/guards/roles.guard'
;
import
{
TransformInterceptor
}
from
'./common/interceptors/transform.interceptor'
;
...
...
@@ -69,6 +74,10 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
BountiesModule
,
AdjustmentsModule
,
PayrollModule
,
// Phase 1E
NotificationsModule
,
MessagesModule
,
NoticesModule
,
],
providers
:
[
{
provide
:
APP_GUARD
,
useClass
:
JwtAuthGuard
},
...
...
backend/src/modules/messages/dto/create-conversation.dto.ts
0 → 100644
View file @
31c97f0d
import
{
IsString
,
IsOptional
,
IsArray
,
MinLength
}
from
'class-validator'
;
export
class
CreateConversationDto
{
@
IsArray
()
@
IsString
({
each
:
true
})
participantIds
:
string
[];
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
name
?:
string
;
// For group conversations
@
IsOptional
()
@
IsString
()
initialMessage
?:
string
;
}
\ No newline at end of file
backend/src/modules/messages/dto/message-response.dto.ts
0 → 100644
View file @
31c97f0d
export
class
ConversationResponseDto
{
id
:
string
;
type
:
string
;
name
:
string
|
null
;
participants
:
Array
<
{
id
:
string
;
userId
:
string
;
firstName
:
string
;
lastName
:
string
;
avatar
:
string
|
null
;
unreadCount
:
number
;
lastReadAt
:
string
|
null
;
}
>
;
lastMessageAt
:
string
|
null
;
lastMessageText
:
string
|
null
;
createdAt
:
string
;
}
export
class
MessageResponseDto
{
id
:
string
;
conversationId
:
string
;
senderId
:
string
;
senderName
:
string
;
senderAvatar
:
string
|
null
;
content
:
string
|
null
;
type
:
string
;
fileUrl
:
string
|
null
;
fileName
:
string
|
null
;
replyToId
:
string
|
null
;
replyToPreview
:
any
|
null
;
mentions
:
string
[]
|
null
;
isEdited
:
boolean
;
isPinned
:
boolean
;
createdAt
:
string
;
}
\ No newline at end of file
backend/src/modules/messages/dto/send-message.dto.ts
0 → 100644
View file @
31c97f0d
import
{
IsString
,
IsOptional
,
MinLength
}
from
'class-validator'
;
export
class
SendMessageDto
{
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
content
?:
string
;
@
IsOptional
()
@
IsString
()
type
?:
string
;
// TEXT, FILE, SYSTEM
@
IsOptional
()
@
IsString
()
replyToId
?:
string
;
@
IsOptional
()
mentions
?:
string
[];
// Array of user IDs
// File fields (when type is FILE)
@
IsOptional
()
@
IsString
()
fileUrl
?:
string
;
@
IsOptional
()
@
IsString
()
fileName
?:
string
;
@
IsOptional
()
@
IsString
()
fileMimeType
?:
string
;
@
IsOptional
()
fileSizeBytes
?:
number
;
}
\ No newline at end of file
backend/src/modules/messages/messages.controller.ts
0 → 100644
View file @
31c97f0d
import
{
Controller
,
Get
,
Post
,
Delete
,
Body
,
Param
,
Query
,
HttpCode
,
HttpStatus
,
}
from
'@nestjs/common'
;
import
{
MessagesService
}
from
'./messages.service'
;
import
{
CreateConversationDto
}
from
'./dto/create-conversation.dto'
;
import
{
SendMessageDto
}
from
'./dto/send-message.dto'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
@
Controller
(
'conversations'
)
export
class
MessagesController
{
constructor
(
private
readonly
messagesService
:
MessagesService
)
{}
@
Post
()
async
createConversation
(
@
Body
()
dto
:
CreateConversationDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
messagesService
.
createConversation
(
dto
,
user
);
}
@
Get
()
async
getConversations
(
@
CurrentUser
()
user
:
RequestUser
,
@
Query
(
'page'
)
page
?:
string
,
@
Query
(
'limit'
)
limit
?:
string
,
)
{
return
this
.
messagesService
.
getConversations
(
user
,
page
?
parseInt
(
page
,
10
)
:
1
,
limit
?
parseInt
(
limit
,
10
)
:
20
,
);
}
@
Get
(
':id'
)
async
getConversation
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
messagesService
.
getConversationById
(
id
,
user
);
}
@
Get
(
':id/messages'
)
async
getMessages
(
@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
@
Query
(
'page'
)
page
?:
string
,
@
Query
(
'limit'
)
limit
?:
string
,
)
{
return
this
.
messagesService
.
getMessages
(
id
,
user
,
page
?
parseInt
(
page
,
10
)
:
1
,
limit
?
parseInt
(
limit
,
10
)
:
50
,
);
}
@
Post
(
':id/messages'
)
async
sendMessage
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
SendMessageDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
messagesService
.
sendMessage
(
id
,
dto
,
user
);
}
@
Post
(
':id/participants'
)
async
addParticipant
(
@
Param
(
'id'
)
id
:
string
,
@
Body
(
'userId'
)
userId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
messagesService
.
addParticipant
(
id
,
userId
,
user
);
}
@
Delete
(
':id/participants/:userId'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
removeParticipant
(
@
Param
(
'id'
)
id
:
string
,
@
Param
(
'userId'
)
userId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
await
this
.
messagesService
.
removeParticipant
(
id
,
userId
,
user
);
return
{
message
:
'Participant removed'
};
}
@
Delete
(
'messages/:messageId'
)
@
Roles
(
'SUPER_ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
deleteMessage
(@
Param
(
'messageId'
)
messageId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
messagesService
.
deleteMessage
(
messageId
,
user
);
return
{
message
:
'Message deleted'
};
}
}
\ No newline at end of file
backend/src/modules/messages/messages.gateway.ts
0 → 100644
View file @
31c97f0d
import
{
WebSocketGateway
,
WebSocketServer
,
SubscribeMessage
,
OnGatewayConnection
,
OnGatewayDisconnect
,
ConnectedSocket
,
MessageBody
,
}
from
'@nestjs/websockets'
;
import
{
Server
,
Socket
}
from
'socket.io'
;
import
{
Logger
}
from
'@nestjs/common'
;
import
{
JwtService
}
from
'@nestjs/jwt'
;
import
{
ConfigService
}
from
'@nestjs/config'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
@
WebSocketGateway
({
namespace
:
'/messages'
,
cors
:
{
origin
:
'*'
,
credentials
:
true
},
})
export
class
MessagesGateway
implements
OnGatewayConnection
,
OnGatewayDisconnect
{
@
WebSocketServer
()
server
:
Server
;
private
readonly
logger
=
new
Logger
(
MessagesGateway
.
name
);
private
userSockets
=
new
Map
<
string
,
Set
<
string
>>
();
// userId -> Set<socketId>
constructor
(
private
readonly
jwtService
:
JwtService
,
private
readonly
configService
:
ConfigService
,
private
readonly
prisma
:
PrismaService
,
)
{}
async
handleConnection
(
client
:
Socket
):
Promise
<
void
>
{
try
{
const
token
=
client
.
handshake
.
auth
?.
token
||
client
.
handshake
.
headers
?.
authorization
?.
replace
(
'Bearer '
,
''
);
if
(
!
token
)
{
client
.
disconnect
();
return
;
}
const
payload
=
this
.
jwtService
.
verify
(
token
,
{
secret
:
this
.
configService
.
get
<
string
>
(
'jwt.secret'
),
});
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
payload
.
sub
},
select
:
{
id
:
true
,
role
:
true
,
status
:
true
},
});
if
(
!
user
||
user
.
status
===
'OFFBOARDED'
)
{
client
.
disconnect
();
return
;
}
(
client
as
any
).
userId
=
user
.
id
;
(
client
as
any
).
userRole
=
user
.
role
;
// Track socket
if
(
!
this
.
userSockets
.
has
(
user
.
id
))
{
this
.
userSockets
.
set
(
user
.
id
,
new
Set
());
}
this
.
userSockets
.
get
(
user
.
id
)
!
.
add
(
client
.
id
);
// Auto-join all user's conversation rooms
const
participations
=
await
this
.
prisma
.
conversationParticipant
.
findMany
({
where
:
{
userId
:
user
.
id
},
select
:
{
conversationId
:
true
},
});
for
(
const
p
of
participations
)
{
client
.
join
(
`conversation:
${
p
.
conversationId
}
`
);
}
this
.
logger
.
log
(
`Messages client connected:
${
user
.
id
}
`
);
}
catch
(
err
)
{
this
.
logger
.
warn
(
`Messages connection failed:
${
err
.
message
}
`
);
client
.
disconnect
();
}
}
handleDisconnect
(
client
:
Socket
):
void
{
const
userId
=
(
client
as
any
).
userId
;
if
(
userId
)
{
const
sockets
=
this
.
userSockets
.
get
(
userId
);
if
(
sockets
)
{
sockets
.
delete
(
client
.
id
);
if
(
sockets
.
size
===
0
)
{
this
.
userSockets
.
delete
(
userId
);
}
}
this
.
logger
.
log
(
`Messages client disconnected:
${
userId
}
`
);
}
}
@
SubscribeMessage
(
'message:join_conversation'
)
handleJoinConversation
(
@
ConnectedSocket
()
client
:
Socket
,
@
MessageBody
()
data
:
{
conversationId
:
string
},
):
void
{
client
.
join
(
`conversation:
${
data
.
conversationId
}
`
);
}
@
SubscribeMessage
(
'message:leave_conversation'
)
handleLeaveConversation
(
@
ConnectedSocket
()
client
:
Socket
,
@
MessageBody
()
data
:
{
conversationId
:
string
},
):
void
{
client
.
leave
(
`conversation:
${
data
.
conversationId
}
`
);
}
@
SubscribeMessage
(
'message:typing'
)
handleTyping
(
@
ConnectedSocket
()
client
:
Socket
,
@
MessageBody
()
data
:
{
conversationId
:
string
},
):
void
{
const
userId
=
(
client
as
any
).
userId
;
client
.
to
(
`conversation:
${
data
.
conversationId
}
`
).
emit
(
'message:typing'
,
{
conversationId
:
data
.
conversationId
,
userId
,
});
}
@
SubscribeMessage
(
'message:stop_typing'
)
handleStopTyping
(
@
ConnectedSocket
()
client
:
Socket
,
@
MessageBody
()
data
:
{
conversationId
:
string
},
):
void
{
const
userId
=
(
client
as
any
).
userId
;
client
.
to
(
`conversation:
${
data
.
conversationId
}
`
).
emit
(
'message:stop_typing'
,
{
conversationId
:
data
.
conversationId
,
userId
,
});
}
@
SubscribeMessage
(
'message:read'
)
async
handleRead
(
@
ConnectedSocket
()
client
:
Socket
,
@
MessageBody
()
data
:
{
conversationId
:
string
},
):
Promise
<
void
>
{
const
userId
=
(
client
as
any
).
userId
;
await
this
.
prisma
.
conversationParticipant
.
update
({
where
:
{
conversationId_userId
:
{
conversationId
:
data
.
conversationId
,
userId
}
},
data
:
{
lastReadAt
:
new
Date
(),
unreadCount
:
0
},
});
this
.
server
.
to
(
`conversation:
${
data
.
conversationId
}
`
).
emit
(
'message:read'
,
{
conversationId
:
data
.
conversationId
,
userId
,
readAt
:
new
Date
().
toISOString
(),
});
}
// ─── Push Methods ─────────────────────────────────────
sendNewMessage
(
conversationId
:
string
,
message
:
any
):
void
{
this
.
server
.
to
(
`conversation:
${
conversationId
}
`
).
emit
(
'message:new'
,
{
conversationId
,
message
,
});
}
sendMessageDeleted
(
conversationId
:
string
,
messageId
:
string
):
void
{
this
.
server
.
to
(
`conversation:
${
conversationId
}
`
).
emit
(
'message:deleted'
,
{
conversationId
,
messageId
,
});
}
sendParticipantAdded
(
conversationId
:
string
,
participant
:
any
):
void
{
this
.
server
.
to
(
`conversation:
${
conversationId
}
`
).
emit
(
'message:participant_added'
,
{
conversationId
,
participant
,
});
}
sendParticipantRemoved
(
conversationId
:
string
,
userId
:
string
):
void
{
this
.
server
.
to
(
`conversation:
${
conversationId
}
`
).
emit
(
'message:participant_removed'
,
{
conversationId
,
userId
,
});
}
isUserOnline
(
userId
:
string
):
boolean
{
return
this
.
userSockets
.
has
(
userId
)
&&
this
.
userSockets
.
get
(
userId
)
!
.
size
>
0
;
}
}
\ No newline at end of file
backend/src/modules/messages/messages.module.ts
0 → 100644
View file @
31c97f0d
import
{
Module
}
from
'@nestjs/common'
;
import
{
JwtModule
}
from
'@nestjs/jwt'
;
import
{
ConfigModule
}
from
'@nestjs/config'
;
import
{
MessagesController
}
from
'./messages.controller'
;
import
{
MessagesService
}
from
'./messages.service'
;
import
{
MessagesGateway
}
from
'./messages.gateway'
;
@
Module
({
imports
:
[
JwtModule
,
ConfigModule
],
controllers
:
[
MessagesController
],
providers
:
[
MessagesService
,
MessagesGateway
],
exports
:
[
MessagesService
,
MessagesGateway
],
})
export
class
MessagesModule
{}
\ No newline at end of file
backend/src/modules/messages/messages.service.ts
0 → 100644
View file @
31c97f0d
This diff is collapsed.
Click to expand it.
backend/src/modules/notices/dto/create-notice.dto.ts
0 → 100644
View file @
31c97f0d
import
{
IsString
,
IsOptional
,
IsBoolean
,
IsArray
,
MinLength
,
MaxLength
}
from
'class-validator'
;
export
class
CreateNoticeDto
{
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
200
)
title
:
string
;
@
IsString
()
@
MinLength
(
1
)
content
:
string
;
@
IsString
()
type
:
string
;
// GENERAL_ANNOUNCEMENT, OFFICIAL_WARNING, POLICY_UPDATE, CUSTOM
@
IsOptional
()
@
IsString
()
priority
?:
string
;
// NORMAL, HIGH
@
IsOptional
()
@
IsBoolean
()
isBlocking
?:
boolean
;
@
IsString
()
recipientType
:
string
;
// ALL_USERS, ALL_CONTRACTORS, SPECIFIC_USERS, BY_BOARD, BY_ROLE
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
recipientIds
?:
string
[];
@
IsOptional
()
@
IsString
()
expiresAt
?:
string
;
}
\ No newline at end of file
backend/src/modules/notices/dto/notice-response.dto.ts
0 → 100644
View file @
31c97f0d
export
class
NoticeResponseDto
{
id
:
string
;
title
:
string
;
content
:
string
;
type
:
string
;
priority
:
string
;
isBlocking
:
boolean
;
recipientType
:
string
;
createdBy
:
{
id
:
string
;
firstName
:
string
;
lastName
:
string
;
};
publishedAt
:
string
|
null
;
expiresAt
:
string
|
null
;
acknowledgmentCount
:
number
;
totalRecipients
:
number
;
createdAt
:
string
;
}
\ No newline at end of file
backend/src/modules/notices/dto/update-notice.dto.ts
0 → 100644
View file @
31c97f0d
import
{
IsString
,
IsOptional
,
IsBoolean
,
MinLength
,
MaxLength
}
from
'class-validator'
;
export
class
UpdateNoticeDto
{
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
200
)
title
?:
string
;
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
content
?:
string
;
@
IsOptional
()
@
IsString
()
priority
?:
string
;
}
\ No newline at end of file
backend/src/modules/notices/notices.controller.ts
0 → 100644
View file @
31c97f0d
import
{
Controller
,
Get
,
Post
,
Put
,
Delete
,
Body
,
Param
,
Query
,
HttpCode
,
HttpStatus
,
}
from
'@nestjs/common'
;
import
{
NoticesService
}
from
'./notices.service'
;
import
{
CreateNoticeDto
}
from
'./dto/create-notice.dto'
;
import
{
UpdateNoticeDto
}
from
'./dto/update-notice.dto'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
@
Controller
(
'notices'
)
export
class
NoticesController
{
constructor
(
private
readonly
noticesService
:
NoticesService
)
{}
@
Post
()
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
create
(@
Body
()
dto
:
CreateNoticeDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
noticesService
.
create
(
dto
,
user
);
}
@
Get
()
async
findAll
(
@
CurrentUser
()
user
:
RequestUser
,
@
Query
(
'page'
)
page
?:
string
,
@
Query
(
'limit'
)
limit
?:
string
,
)
{
return
this
.
noticesService
.
findAll
(
user
,
page
?
parseInt
(
page
,
10
)
:
1
,
limit
?
parseInt
(
limit
,
10
)
:
20
,
);
}
@
Get
(
':id'
)
async
findById
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
noticesService
.
findById
(
id
,
user
);
}
@
Put
(
':id'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
update
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UpdateNoticeDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
noticesService
.
update
(
id
,
dto
,
user
);
}
@
Delete
(
':id'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
delete
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
noticesService
.
delete
(
id
,
user
);
return
{
message
:
'Notice deleted'
};
}
@
Post
(
':id/acknowledge'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
acknowledge
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
noticesService
.
acknowledge
(
id
,
user
);
}
@
Get
(
':id/acknowledgments'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
getAcknowledgmentStatus
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
noticesService
.
getAcknowledgmentStatus
(
id
,
user
);
}
}
\ No newline at end of file
backend/src/modules/notices/notices.module.ts
0 → 100644
View file @
31c97f0d
import
{
Module
}
from
'@nestjs/common'
;
import
{
NoticesController
}
from
'./notices.controller'
;
import
{
NoticesService
}
from
'./notices.service'
;
@
Module
({
controllers
:
[
NoticesController
],
providers
:
[
NoticesService
],
exports
:
[
NoticesService
],
})
export
class
NoticesModule
{}
\ No newline at end of file
backend/src/modules/notices/notices.service.ts
0 → 100644
View file @
31c97f0d
This diff is collapsed.
Click to expand it.
backend/src/modules/notifications/dto/create-notification.dto.ts
0 → 100644
View file @
31c97f0d
import
{
IsString
,
IsOptional
,
IsBoolean
,
IsArray
}
from
'class-validator'
;
export
class
CreateNotificationDto
{
@
IsString
()
userId
:
string
;
@
IsString
()
type
:
string
;
// BLOCKING, IMPORTANT, INFORMATIONAL
@
IsString
()
category
:
string
;
@
IsString
()
title
:
string
;
@
IsString
()
message
:
string
;
@
IsOptional
()
@
IsString
()
actionUrl
?:
string
;
@
IsOptional
()
metadata
?:
any
;
@
IsOptional
()
@
IsBoolean
()
isBlocking
?:
boolean
;
@
IsOptional
()
@
IsString
()
entityType
?:
string
;
@
IsOptional
()
@
IsString
()
entityId
?:
string
;
@
IsOptional
()
@
IsString
()
triggeredById
?:
string
;
}
export
class
CreateBulkNotificationDto
{
@
IsArray
()
@
IsString
({
each
:
true
})
userIds
:
string
[];
@
IsString
()
type
:
string
;
@
IsString
()
category
:
string
;
@
IsString
()
title
:
string
;
@
IsString
()
message
:
string
;
@
IsOptional
()
@
IsString
()
actionUrl
?:
string
;
@
IsOptional
()
metadata
?:
any
;
@
IsOptional
()
@
IsBoolean
()
isBlocking
?:
boolean
;
@
IsOptional
()
@
IsString
()
entityType
?:
string
;
@
IsOptional
()
@
IsString
()
entityId
?:
string
;
@
IsOptional
()
@
IsString
()
triggeredById
?:
string
;
}
\ No newline at end of file
backend/src/modules/notifications/dto/notification-filter.dto.ts
0 → 100644
View file @
31c97f0d
import
{
IsOptional
,
IsString
,
IsBoolean
}
from
'class-validator'
;
import
{
Type
}
from
'class-transformer'
;
import
{
PaginationDto
}
from
'../../../common/dto/pagination.dto'
;
export
class
NotificationFilterDto
extends
PaginationDto
{
@
IsOptional
()
@
IsString
()
type
?:
string
;
@
IsOptional
()
@
IsString
()
category
?:
string
;
@
IsOptional
()
@
Type
(()
=>
Boolean
)
@
IsBoolean
()
isRead
?:
boolean
;
@
IsOptional
()
@
Type
(()
=>
Boolean
)
@
IsBoolean
()
isBlocking
?:
boolean
;
}
\ No newline at end of file
backend/src/modules/notifications/dto/notification-response.dto.ts
0 → 100644
View file @
31c97f0d
export
class
NotificationResponseDto
{
id
:
string
;
type
:
string
;
category
:
string
;
title
:
string
;
message
:
string
;
actionUrl
:
string
|
null
;
metadata
:
any
;
isRead
:
boolean
;
readAt
:
string
|
null
;
acknowledgedAt
:
string
|
null
;
isBlocking
:
boolean
;
entityType
:
string
|
null
;
entityId
:
string
|
null
;
createdAt
:
string
;
}
export
class
NotificationCountDto
{
total
:
number
;
unread
:
number
;
blocking
:
number
;
}
\ No newline at end of file
backend/src/modules/notifications/notifications.controller.ts
0 → 100644
View file @
31c97f0d
import
{
Controller
,
Get
,
Post
,
Put
,
Query
,
Param
,
Body
,
HttpCode
,
HttpStatus
,
}
from
'@nestjs/common'
;
import
{
NotificationsService
}
from
'./notifications.service'
;
import
{
NotificationFilterDto
}
from
'./dto/notification-filter.dto'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
@
Controller
(
'notifications'
)
export
class
NotificationsController
{
constructor
(
private
readonly
notificationsService
:
NotificationsService
)
{}
@
Get
()
async
findAll
(@
Query
()
filter
:
NotificationFilterDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
findAll
(
filter
,
user
);
}
@
Get
(
'counts'
)
async
getCounts
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
getCounts
(
user
.
id
);
}
@
Get
(
'blocking'
)
async
getBlocking
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
getUnacknowledgedBlocking
(
user
.
id
);
}
@
Put
(
':id/read'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
markAsRead
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
markAsRead
(
id
,
user
);
}
@
Put
(
':id/acknowledge'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
acknowledge
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
acknowledge
(
id
,
user
);
}
@
Put
(
'read-all'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
markAllAsRead
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
markAllAsRead
(
user
);
}
@
Post
(
'send'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
sendNotification
(@
Body
()
body
:
any
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
notificationsService
.
create
({
...
body
,
triggeredById
:
user
.
id
,
});
}
@
Post
(
'send-bulk'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
sendBulkNotification
(@
Body
()
body
:
any
,
@
CurrentUser
()
user
:
RequestUser
)
{
const
count
=
await
this
.
notificationsService
.
createBulk
({
...
body
,
triggeredById
:
user
.
id
,
});
return
{
sent
:
count
};
}
}
\ No newline at end of file
backend/src/modules/notifications/notifications.gateway.ts
0 → 100644
View file @
31c97f0d
import
{
WebSocketGateway
,
WebSocketServer
,
SubscribeMessage
,
OnGatewayConnection
,
OnGatewayDisconnect
,
ConnectedSocket
,
}
from
'@nestjs/websockets'
;
import
{
Server
,
Socket
}
from
'socket.io'
;
import
{
Logger
}
from
'@nestjs/common'
;
import
{
JwtService
}
from
'@nestjs/jwt'
;
import
{
ConfigService
}
from
'@nestjs/config'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
@
WebSocketGateway
({
namespace
:
'/notifications'
,
cors
:
{
origin
:
'*'
,
credentials
:
true
},
})
export
class
NotificationsGateway
implements
OnGatewayConnection
,
OnGatewayDisconnect
{
@
WebSocketServer
()
server
:
Server
;
private
readonly
logger
=
new
Logger
(
NotificationsGateway
.
name
);
constructor
(
private
readonly
jwtService
:
JwtService
,
private
readonly
configService
:
ConfigService
,
private
readonly
prisma
:
PrismaService
,
)
{}
async
handleConnection
(
client
:
Socket
):
Promise
<
void
>
{
try
{
const
token
=
client
.
handshake
.
auth
?.
token
||
client
.
handshake
.
headers
?.
authorization
?.
replace
(
'Bearer '
,
''
);
if
(
!
token
)
{
client
.
disconnect
();
return
;
}
const
payload
=
this
.
jwtService
.
verify
(
token
,
{
secret
:
this
.
configService
.
get
<
string
>
(
'jwt.secret'
),
});
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
payload
.
sub
},
select
:
{
id
:
true
,
role
:
true
,
status
:
true
},
});
if
(
!
user
||
user
.
status
===
'OFFBOARDED'
)
{
client
.
disconnect
();
return
;
}
(
client
as
any
).
userId
=
user
.
id
;
(
client
as
any
).
userRole
=
user
.
role
;
// Auto-join personal notification room
client
.
join
(
`notifications:
${
user
.
id
}
`
);
this
.
logger
.
log
(
`Notification client connected:
${
user
.
id
}
`
);
// Push any unacknowledged blocking notifications immediately
const
blocking
=
await
this
.
prisma
.
notification
.
findMany
({
where
:
{
userId
:
user
.
id
,
isBlocking
:
true
,
acknowledgedAt
:
null
},
orderBy
:
{
createdAt
:
'asc'
},
});
if
(
blocking
.
length
>
0
)
{
client
.
emit
(
'notification:blocking_queue'
,
blocking
);
}
// Push unread count
const
unreadCount
=
await
this
.
prisma
.
notification
.
count
({
where
:
{
userId
:
user
.
id
,
isRead
:
false
},
});
const
blockingCount
=
blocking
.
length
;
client
.
emit
(
'notification:count_update'
,
{
unread
:
unreadCount
,
blocking
:
blockingCount
,
});
}
catch
(
err
)
{
this
.
logger
.
warn
(
`Notification connection failed:
${
err
.
message
}
`
);
client
.
disconnect
();
}
}
handleDisconnect
(
client
:
Socket
):
void
{
const
userId
=
(
client
as
any
).
userId
;
if
(
userId
)
{
this
.
logger
.
log
(
`Notification client disconnected:
${
userId
}
`
);
}
}
@
SubscribeMessage
(
'notification:mark_read'
)
async
handleMarkRead
(
@
ConnectedSocket
()
client
:
Socket
,
):
Promise
<
void
>
{
// Count update is pushed via sendCountUpdate after service calls
}
// ─── Push Methods (called by NotificationsService) ────────
sendNotification
(
userId
:
string
,
notification
:
any
):
void
{
this
.
server
.
to
(
`notifications:
${
userId
}
`
).
emit
(
'notification:new'
,
notification
);
}
sendBlockingNotification
(
userId
:
string
,
notification
:
any
):
void
{
this
.
server
.
to
(
`notifications:
${
userId
}
`
).
emit
(
'notification:blocking'
,
notification
);
}
sendCountUpdate
(
userId
:
string
,
counts
:
{
unread
:
number
;
blocking
:
number
}):
void
{
this
.
server
.
to
(
`notifications:
${
userId
}
`
).
emit
(
'notification:count_update'
,
counts
);
}
}
\ No newline at end of file
backend/src/modules/notifications/notifications.module.ts
0 → 100644
View file @
31c97f0d
import
{
Module
,
Global
}
from
'@nestjs/common'
;
import
{
JwtModule
}
from
'@nestjs/jwt'
;
import
{
ConfigModule
}
from
'@nestjs/config'
;
import
{
NotificationsController
}
from
'./notifications.controller'
;
import
{
NotificationsService
}
from
'./notifications.service'
;
import
{
NotificationsGateway
}
from
'./notifications.gateway'
;
@
Global
()
@
Module
({
imports
:
[
JwtModule
,
ConfigModule
],
controllers
:
[
NotificationsController
],
providers
:
[
NotificationsService
,
NotificationsGateway
],
exports
:
[
NotificationsService
,
NotificationsGateway
],
})
export
class
NotificationsModule
{}
\ No newline at end of file
backend/src/modules/notifications/notifications.service.ts
0 → 100644
View file @
31c97f0d
import
{
Injectable
,
NotFoundException
,
ForbiddenException
,
BadRequestException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
CreateNotificationDto
,
CreateBulkNotificationDto
}
from
'./dto/create-notification.dto'
;
import
{
NotificationFilterDto
}
from
'./dto/notification-filter.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
getSkip
,
buildPaginatedResponse
,
PaginatedResult
}
from
'../../common/utils/pagination.util'
;
@
Injectable
()
export
class
NotificationsService
{
private
readonly
logger
=
new
Logger
(
NotificationsService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
create
(
dto
:
CreateNotificationDto
):
Promise
<
any
>
{
const
validTypes
=
[
'BLOCKING'
,
'IMPORTANT'
,
'INFORMATIONAL'
];
if
(
!
validTypes
.
includes
(
dto
.
type
))
{
throw
new
BadRequestException
(
`Type must be one of:
${
validTypes
.
join
(
', '
)}
`
);
}
const
user
=
await
this
.
prisma
.
user
.
findFirst
({
where
:
{
id
:
dto
.
userId
,
deletedAt
:
null
},
});
if
(
!
user
)
{
this
.
logger
.
warn
(
`Cannot create notification for non-existent user
${
dto
.
userId
}
`
);
return
null
;
}
const
isBlocking
=
dto
.
isBlocking
??
dto
.
type
===
'BLOCKING'
;
const
notification
=
await
this
.
prisma
.
notification
.
create
({
data
:
{
userId
:
dto
.
userId
,
type
:
dto
.
type
,
category
:
dto
.
category
,
title
:
dto
.
title
,
message
:
dto
.
message
,
actionUrl
:
dto
.
actionUrl
||
null
,
metadata
:
dto
.
metadata
||
null
,
isBlocking
,
entityType
:
dto
.
entityType
||
null
,
entityId
:
dto
.
entityId
||
null
,
triggeredById
:
dto
.
triggeredById
||
null
,
},
});
this
.
logger
.
log
(
`Notification created: [
${
dto
.
type
}
] "
${
dto
.
title
}
" for user
${
dto
.
userId
}
`
,
);
return
notification
;
}
async
createBulk
(
dto
:
CreateBulkNotificationDto
):
Promise
<
number
>
{
let
count
=
0
;
for
(
const
userId
of
dto
.
userIds
)
{
try
{
await
this
.
create
({
userId
,
type
:
dto
.
type
,
category
:
dto
.
category
,
title
:
dto
.
title
,
message
:
dto
.
message
,
actionUrl
:
dto
.
actionUrl
,
metadata
:
dto
.
metadata
,
isBlocking
:
dto
.
isBlocking
,
entityType
:
dto
.
entityType
,
entityId
:
dto
.
entityId
,
triggeredById
:
dto
.
triggeredById
,
});
count
++
;
}
catch
(
err
)
{
this
.
logger
.
warn
(
`Failed to create notification for user
${
userId
}
:
${
err
.
message
}
`
);
}
}
return
count
;
}
async
createForAllActiveContractors
(
dto
:
Omit
<
CreateNotificationDto
,
'userId'
>
,
):
Promise
<
number
>
{
const
contractors
=
await
this
.
prisma
.
user
.
findMany
({
where
:
{
role
:
'CONTRACTOR'
,
status
:
{
in
:
[
'ACTIVE'
,
'ON_PIP'
,
'SUSPENDED'
]
},
deletedAt
:
null
,
},
select
:
{
id
:
true
},
});
return
this
.
createBulk
({
userIds
:
contractors
.
map
((
c
)
=>
c
.
id
),
...
dto
,
}
as
CreateBulkNotificationDto
);
}
async
createForAllUsers
(
dto
:
Omit
<
CreateNotificationDto
,
'userId'
>
,
):
Promise
<
number
>
{
const
users
=
await
this
.
prisma
.
user
.
findMany
({
where
:
{
status
:
{
notIn
:
[
'OFFBOARDED'
]
},
deletedAt
:
null
,
},
select
:
{
id
:
true
},
});
return
this
.
createBulk
({
userIds
:
users
.
map
((
u
)
=>
u
.
id
),
...
dto
,
}
as
CreateBulkNotificationDto
);
}
async
findAll
(
filter
:
NotificationFilterDto
,
currentUser
:
RequestUser
):
Promise
<
PaginatedResult
<
any
>>
{
const
page
=
filter
.
page
||
1
;
const
limit
=
filter
.
limit
||
20
;
const
where
:
any
=
{
userId
:
currentUser
.
id
};
if
(
filter
.
type
)
where
.
type
=
filter
.
type
;
if
(
filter
.
category
)
where
.
category
=
filter
.
category
;
if
(
filter
.
isRead
!==
undefined
)
where
.
isRead
=
filter
.
isRead
;
if
(
filter
.
isBlocking
!==
undefined
)
where
.
isBlocking
=
filter
.
isBlocking
;
if
(
filter
.
search
)
{
where
.
OR
=
[
{
title
:
{
contains
:
filter
.
search
,
mode
:
'insensitive'
}
},
{
message
:
{
contains
:
filter
.
search
,
mode
:
'insensitive'
}
},
];
}
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
notification
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
createdAt
:
'desc'
},
}),
this
.
prisma
.
notification
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
async
getUnacknowledgedBlocking
(
userId
:
string
):
Promise
<
any
[]
>
{
return
this
.
prisma
.
notification
.
findMany
({
where
:
{
userId
,
isBlocking
:
true
,
acknowledgedAt
:
null
,
},
orderBy
:
{
createdAt
:
'asc'
},
});
}
async
getCounts
(
userId
:
string
):
Promise
<
{
total
:
number
;
unread
:
number
;
blocking
:
number
}
>
{
const
[
total
,
unread
,
blocking
]
=
await
Promise
.
all
([
this
.
prisma
.
notification
.
count
({
where
:
{
userId
}
}),
this
.
prisma
.
notification
.
count
({
where
:
{
userId
,
isRead
:
false
}
}),
this
.
prisma
.
notification
.
count
({
where
:
{
userId
,
isBlocking
:
true
,
acknowledgedAt
:
null
},
}),
]);
return
{
total
,
unread
,
blocking
};
}
async
markAsRead
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
notification
=
await
this
.
prisma
.
notification
.
findUnique
({
where
:
{
id
}
});
if
(
!
notification
)
throw
new
NotFoundException
(
'Notification not found'
);
if
(
notification
.
userId
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only mark your own notifications as read'
);
}
if
(
notification
.
isRead
)
return
notification
;
return
this
.
prisma
.
notification
.
update
({
where
:
{
id
},
data
:
{
isRead
:
true
,
readAt
:
new
Date
()
},
});
}
async
acknowledge
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
notification
=
await
this
.
prisma
.
notification
.
findUnique
({
where
:
{
id
}
});
if
(
!
notification
)
throw
new
NotFoundException
(
'Notification not found'
);
if
(
notification
.
userId
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only acknowledge your own notifications'
);
}
if
(
!
notification
.
isBlocking
)
{
throw
new
BadRequestException
(
'Only blocking notifications require acknowledgment'
);
}
if
(
notification
.
acknowledgedAt
)
{
return
notification
;
}
const
updated
=
await
this
.
prisma
.
notification
.
update
({
where
:
{
id
},
data
:
{
acknowledgedAt
:
new
Date
(),
isRead
:
true
,
readAt
:
notification
.
readAt
||
new
Date
(),
},
});
this
.
logger
.
log
(
`Blocking notification
${
id
}
acknowledged by
${
currentUser
.
id
}
`
);
return
updated
;
}
async
markAllAsRead
(
currentUser
:
RequestUser
):
Promise
<
{
count
:
number
}
>
{
const
result
=
await
this
.
prisma
.
notification
.
updateMany
({
where
:
{
userId
:
currentUser
.
id
,
isRead
:
false
,
isBlocking
:
false
,
// Don't auto-read blocking notifications
},
data
:
{
isRead
:
true
,
readAt
:
new
Date
()
},
});
return
{
count
:
result
.
count
};
}
async
deleteOld
(
daysOld
:
number
):
Promise
<
number
>
{
const
cutoff
=
new
Date
(
Date
.
now
()
-
daysOld
*
24
*
60
*
60
*
1000
);
const
result
=
await
this
.
prisma
.
notification
.
deleteMany
({
where
:
{
createdAt
:
{
lt
:
cutoff
},
isBlocking
:
false
,
isRead
:
true
,
},
});
this
.
logger
.
log
(
`Cleaned up
${
result
.
count
}
old notifications older than
${
daysOld
}
days`
);
return
result
.
count
;
}
}
\ No newline at end of file
prisma/schema-communications.prisma
0 → 100644
View file @
31c97f0d
//
═══════════════════════════════════════════════════
//
PHASE
1
E
:
COMMUNICATIONS
&
NOTIFICATIONS
//
═══════════════════════════════════════════════════
//
───
NOTIFICATIONS
─────────────────────────────────
model
Notification
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
userId
String
user
User
@
relation
(
"UserNotifications"
,
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
type
String
//
BLOCKING
,
IMPORTANT
,
INFORMATIONAL
category
String
//
CARD
,
BOARD
,
SALARY
,
DEDUCTION
,
BOUNTY
,
ADJUSTMENT
,
PAYROLL
,
REPORT
,
EVALUATION
,
PIP
,
MESSAGE
,
MEETING
,
NOTICE
,
POLICY
,
ONBOARDING
,
UNAVAILABILITY
,
SCHEDULE
,
SYSTEM
title
String
message
String
actionUrl
String
?
metadata
Json
?
isRead
Boolean
@
default
(
false
)
readAt
DateTime
?
acknowledgedAt
DateTime
?
//
For
blocking
notifications
isBlocking
Boolean
@
default
(
false
)
//
Source
tracking
entityType
String
?
entityId
String
?
triggeredById
String
?
@@
index
([
userId
,
isRead
])
@@
index
([
userId
,
isBlocking
,
acknowledgedAt
])
@@
index
([
userId
,
createdAt
])
@@
index
([
type
])
@@
index
([
category
])
}
//
───
CONVERSATIONS
&
MESSAGES
──────────────────────
model
Conversation
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
type
String
//
DIRECT
,
GROUP
name
String
?
//
For
group
conversations
avatar
String
?
//
For
group
conversations
createdById
String
createdBy
User
@
relation
(
"ConversationCreator"
,
fields
:
[
createdById
],
references
:
[
id
],
onDelete
:
Restrict
)
participants
ConversationParticipant
[]
messages
Message
[]
lastMessageAt
DateTime
?
lastMessageText
String
?
@@
index
([
createdById
])
@@
index
([
lastMessageAt
])
}
model
ConversationParticipant
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
conversationId
String
conversation
Conversation
@
relation
(
fields
:
[
conversationId
],
references
:
[
id
],
onDelete
:
Cascade
)
userId
String
user
User
@
relation
(
"ConversationParticipants"
,
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
lastReadAt
DateTime
?
unreadCount
Int
@
default
(
0
)
isMuted
Boolean
@
default
(
false
)
@@
unique
([
conversationId
,
userId
])
@@
index
([
userId
])
@@
index
([
conversationId
])
}
model
Message
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
conversationId
String
conversation
Conversation
@
relation
(
fields
:
[
conversationId
],
references
:
[
id
],
onDelete
:
Cascade
)
senderId
String
sender
User
@
relation
(
"MessageSender"
,
fields
:
[
senderId
],
references
:
[
id
],
onDelete
:
Restrict
)
content
String
?
type
String
@
default
(
"TEXT"
)
//
TEXT
,
FILE
,
SYSTEM
//
File
attachment
fileUrl
String
?
fileName
String
?
fileMimeType
String
?
fileSizeBytes
Int
?
//
Reply
threading
replyToId
String
?
replyTo
Message
?
@
relation
(
"MessageReplies"
,
fields
:
[
replyToId
],
references
:
[
id
],
onDelete
:
SetNull
)
replies
Message
[]
@
relation
(
"MessageReplies"
)
//
Mentions
mentions
Json
?
//
Array
of
user
IDs
mentioned
isEdited
Boolean
@
default
(
false
)
isPinned
Boolean
@
default
(
false
)
deletedAt
DateTime
?
@@
index
([
conversationId
,
createdAt
])
@@
index
([
senderId
])
}
//
───
NOTICES
&
ANNOUNCEMENTS
───────────────────────
model
Notice
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
title
String
content
String
//
Rich
text
type
String
//
GENERAL_ANNOUNCEMENT
,
OFFICIAL_WARNING
,
POLICY_UPDATE
,
CUSTOM
priority
String
@
default
(
"NORMAL"
)
//
NORMAL
,
HIGH
isBlocking
Boolean
@
default
(
false
)
//
Recipients
targeting
recipientType
String
//
ALL_USERS
,
ALL_CONTRACTORS
,
SPECIFIC_USERS
,
BY_BOARD
,
BY_ROLE
recipientIds
Json
?
//
Array
of
user
IDs
,
board
IDs
,
or
role
strings
depending
on
recipientType
createdById
String
createdBy
User
@
relation
(
"NoticeCreator"
,
fields
:
[
createdById
],
references
:
[
id
],
onDelete
:
Restrict
)
publishedAt
DateTime
?
expiresAt
DateTime
?
acknowledgments
NoticeAcknowledgment
[]
@@
index
([
createdById
])
@@
index
([
type
])
@@
index
([
publishedAt
])
}
model
NoticeAcknowledgment
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
noticeId
String
notice
Notice
@
relation
(
fields
:
[
noticeId
],
references
:
[
id
],
onDelete
:
Cascade
)
userId
String
user
User
@
relation
(
"NoticeAcknowledgments"
,
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
acknowledgedAt
DateTime
@
default
(
now
())
@@
unique
([
noticeId
,
userId
])
@@
index
([
noticeId
])
@@
index
([
userId
])
}
\ No newline at end of file
shared/src/events/communication.events.ts
0 → 100644
View file @
31c97f0d
export
interface
NotificationNewPayload
{
notification
:
{
id
:
string
;
type
:
string
;
category
:
string
;
title
:
string
;
message
:
string
;
actionUrl
:
string
|
null
;
isBlocking
:
boolean
;
createdAt
:
string
;
};
}
export
interface
NotificationBlockingPayload
{
notification
:
{
id
:
string
;
type
:
string
;
category
:
string
;
title
:
string
;
message
:
string
;
actionUrl
:
string
|
null
;
metadata
:
any
;
createdAt
:
string
;
};
}
export
interface
NotificationCountUpdatePayload
{
unread
:
number
;
blocking
:
number
;
}
export
interface
MessageNewPayload
{
conversationId
:
string
;
message
:
{
id
:
string
;
senderId
:
string
;
senderName
:
string
;
senderAvatar
:
string
|
null
;
content
:
string
|
null
;
type
:
string
;
fileUrl
:
string
|
null
;
fileName
:
string
|
null
;
replyToId
:
string
|
null
;
replyToPreview
:
any
|
null
;
mentions
:
string
[]
|
null
;
createdAt
:
string
;
};
}
export
interface
MessageTypingPayload
{
conversationId
:
string
;
userId
:
string
;
}
export
interface
MessageReadPayload
{
conversationId
:
string
;
userId
:
string
;
readAt
:
string
;
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment