@atlashub/smartstack-cli 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.documentation/agents.html +916 -916
- package/.documentation/apex.html +1018 -1018
- package/.documentation/business-analyse.html +1501 -1501
- package/.documentation/commands.html +680 -680
- package/.documentation/css/styles.css +2168 -2168
- package/.documentation/efcore.html +2505 -2505
- package/.documentation/gitflow.html +2618 -2618
- package/.documentation/hooks.html +413 -413
- package/.documentation/index.html +323 -323
- package/.documentation/installation.html +462 -462
- package/.documentation/js/app.js +794 -794
- package/.documentation/test-web.html +513 -513
- package/dist/index.js +807 -277
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/efcore/conflicts.md +44 -17
- package/templates/agents/efcore/db-status.md +27 -6
- package/templates/agents/efcore/scan.md +43 -13
- package/templates/commands/ai-prompt.md +315 -315
- package/templates/commands/application/create.md +362 -362
- package/templates/commands/controller/create.md +216 -216
- package/templates/commands/controller.md +59 -0
- package/templates/commands/documentation/module.md +202 -202
- package/templates/commands/efcore/_env-check.md +153 -153
- package/templates/commands/efcore/conflicts.md +109 -192
- package/templates/commands/efcore/db-status.md +101 -89
- package/templates/commands/efcore/migration.md +23 -11
- package/templates/commands/efcore/scan.md +115 -119
- package/templates/commands/efcore.md +54 -6
- package/templates/commands/feature-full.md +267 -267
- package/templates/commands/gitflow/11-finish.md +145 -11
- package/templates/commands/gitflow/13-sync.md +216 -216
- package/templates/commands/gitflow/14-rebase.md +251 -251
- package/templates/commands/gitflow/2-status.md +120 -10
- package/templates/commands/gitflow/3-commit.md +150 -0
- package/templates/commands/gitflow/7-pull-request.md +134 -5
- package/templates/commands/gitflow/9-merge.md +142 -1
- package/templates/commands/implement.md +663 -663
- package/templates/commands/init.md +562 -0
- package/templates/commands/mcp-integration.md +330 -0
- package/templates/commands/notification.md +129 -129
- package/templates/commands/validate.md +233 -0
- package/templates/commands/workflow.md +193 -193
- package/templates/skills/ai-prompt/SKILL.md +778 -778
- package/templates/skills/application/SKILL.md +563 -563
- package/templates/skills/application/templates-backend.md +450 -450
- package/templates/skills/application/templates-frontend.md +531 -531
- package/templates/skills/application/templates-i18n.md +520 -520
- package/templates/skills/application/templates-seed.md +647 -647
- package/templates/skills/controller/SKILL.md +240 -240
- package/templates/skills/controller/postman-templates.md +614 -614
- package/templates/skills/controller/templates.md +1468 -1468
- package/templates/skills/documentation/SKILL.md +133 -133
- package/templates/skills/documentation/templates.md +476 -476
- package/templates/skills/feature-full/SKILL.md +838 -838
- package/templates/skills/notification/SKILL.md +555 -555
- package/templates/skills/ui-components/SKILL.md +870 -870
- package/templates/skills/workflow/SKILL.md +582 -582
|
@@ -1,555 +1,555 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: notification
|
|
3
|
-
description: |
|
|
4
|
-
Integre le systeme de notifications SmartStack dans le developpement.
|
|
5
|
-
Utiliser ce skill quand:
|
|
6
|
-
- L'utilisateur veut envoyer des notifications depuis une feature
|
|
7
|
-
- L'utilisateur mentionne "notifier", "alerte", "notification", "in-app"
|
|
8
|
-
- Creation d'un module qui necessite des notifications utilisateur
|
|
9
|
-
- Integration SignalR pour real-time
|
|
10
|
-
Types: In-App, Email (via Workflow), Push (futur)
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Skill Notification SmartStack
|
|
14
|
-
|
|
15
|
-
> **Architecture:** Notifications = In-App (DB + SignalR) + Email (via Workflows)
|
|
16
|
-
> Les notifications push sont prevues mais pas encore implementees.
|
|
17
|
-
|
|
18
|
-
## QUAND CE SKILL S'ACTIVE
|
|
19
|
-
|
|
20
|
-
Claude invoque automatiquement ce skill quand il detecte :
|
|
21
|
-
|
|
22
|
-
| Declencheur | Exemple |
|
|
23
|
-
|-------------|---------|
|
|
24
|
-
| Demande explicite | "Notifie l'utilisateur quand un ticket est cree" |
|
|
25
|
-
| Mention notification | "Il faut alerter l'admin si..." |
|
|
26
|
-
| Event-driven | "Quand le SLA expire, prevenir l'utilisateur" |
|
|
27
|
-
| Real-time | "Afficher en temps reel les nouvelles notifications" |
|
|
28
|
-
| Mots-cles | "notifier", "alerte", "notification", "SignalR", "real-time" |
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## ARCHITECTURE NOTIFICATIONS
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
36
|
-
│ NOTIFICATION FLOW │
|
|
37
|
-
├─────────────────────────────────────────────────────────────────────────────┤
|
|
38
|
-
│ │
|
|
39
|
-
│ [EVENT] ─────┬──────────────────────────────────────────────────────────┐ │
|
|
40
|
-
│ │ │ │
|
|
41
|
-
│ ▼ │ │
|
|
42
|
-
│ ┌─────────────────────────┐ │ │
|
|
43
|
-
│ │ INotificationService │ │ │
|
|
44
|
-
│ │ SendNotificationAsync │ │ │
|
|
45
|
-
│ └────────────┬────────────┘ │ │
|
|
46
|
-
│ │ │ │
|
|
47
|
-
│ ┌───────┴───────┐ │ │
|
|
48
|
-
│ ▼ ▼ │ │
|
|
49
|
-
│ ┌─────────┐ ┌───────────────────┐ │ │
|
|
50
|
-
│ │ DB │ │ SignalR Hub │ │ │
|
|
51
|
-
│ │ (store) │ │ (real-time push) │ │ │
|
|
52
|
-
│ └─────────┘ └─────────┬─────────┘ │ │
|
|
53
|
-
│ │ │ │
|
|
54
|
-
│ ▼ │ │
|
|
55
|
-
│ ┌──────────────────┐ │ │
|
|
56
|
-
│ │ Frontend Hook │ │ │
|
|
57
|
-
│ │ useSignalR() │ │ │
|
|
58
|
-
│ └─────────┬────────┘ │ │
|
|
59
|
-
│ │ │ │
|
|
60
|
-
│ ▼ │ │
|
|
61
|
-
│ ┌──────────────────┐ │ │
|
|
62
|
-
│ │ NotificationBell │ │ │
|
|
63
|
-
│ │ (UI Component) │ │ │
|
|
64
|
-
│ └──────────────────┘ │ │
|
|
65
|
-
│ │ │
|
|
66
|
-
│ [EMAIL] ── IWorkflowService.TriggerAsync() ── EmailTemplate ── SMTP │ │
|
|
67
|
-
│ │ │
|
|
68
|
-
└─────────────────────────────────────────────────────────────────────────────┘
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## TYPES DE NOTIFICATION
|
|
74
|
-
|
|
75
|
-
### Enum NotificationType
|
|
76
|
-
|
|
77
|
-
| Type | Declencheur | Usage |
|
|
78
|
-
|------|-------------|-------|
|
|
79
|
-
| `TicketCreated` | Nouveau ticket | Support |
|
|
80
|
-
| `TicketAssigned` | Ticket assigne | Support |
|
|
81
|
-
| `TicketStatusChanged` | Changement statut | Support |
|
|
82
|
-
| `TicketCommentAdded` | Nouveau commentaire | Support |
|
|
83
|
-
| `TicketResolved` | Ticket resolu | Support |
|
|
84
|
-
| `RoleAssigned` | Role attribue | Admin |
|
|
85
|
-
| `RoleRemoved` | Role retire | Admin |
|
|
86
|
-
| `RolePermissionsChanged` | Permissions modifiees | Admin |
|
|
87
|
-
| `SystemAnnouncement` | Annonce systeme | Global |
|
|
88
|
-
| `AccountUpdated` | Compte modifie | User |
|
|
89
|
-
| `SlaWarning` | SLA proche expiration | SLA |
|
|
90
|
-
| `SlaResponseBreached` | SLA reponse depasse | SLA |
|
|
91
|
-
| `SlaResolutionBreached` | SLA resolution depasse | SLA |
|
|
92
|
-
|
|
93
|
-
### Ajouter un Nouveau Type
|
|
94
|
-
|
|
95
|
-
```csharp
|
|
96
|
-
// 1. Domain/Support/Enums/NotificationType.cs
|
|
97
|
-
public enum NotificationType
|
|
98
|
-
{
|
|
99
|
-
// ... existants ...
|
|
100
|
-
|
|
101
|
-
// Ajouter le nouveau type
|
|
102
|
-
$NEW_TYPE = XX, // Increment depuis le dernier
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 2. Utilisation dans le service
|
|
106
|
-
await _notificationService.SendNotificationAsync(
|
|
107
|
-
userId,
|
|
108
|
-
NotificationType.$NEW_TYPE,
|
|
109
|
-
title,
|
|
110
|
-
message,
|
|
111
|
-
relatedEntityType: "EntityName",
|
|
112
|
-
relatedEntityId: entityId,
|
|
113
|
-
actionUrl: "/path/to/entity"
|
|
114
|
-
);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## WORKFLOW INTEGRATION
|
|
120
|
-
|
|
121
|
-
### ETAPE 1: Identifier le Besoin
|
|
122
|
-
|
|
123
|
-
| Question | Reponse → Action |
|
|
124
|
-
|----------|------------------|
|
|
125
|
-
| L'utilisateur doit etre notifie en temps reel ? | → In-App + SignalR |
|
|
126
|
-
| L'utilisateur doit recevoir un email ? | → Workflow + EmailTemplate |
|
|
127
|
-
| L'action est liee a une entite ? | → Ajouter relatedEntityType/Id |
|
|
128
|
-
| L'utilisateur peut naviguer vers l'entite ? | → Ajouter actionUrl |
|
|
129
|
-
|
|
130
|
-
### ETAPE 2: Injection du Service
|
|
131
|
-
|
|
132
|
-
```csharp
|
|
133
|
-
// Dans le Controller ou Service
|
|
134
|
-
public class MyController : ControllerBase
|
|
135
|
-
{
|
|
136
|
-
private readonly INotificationService _notificationService;
|
|
137
|
-
private readonly ILogger<MyController> _logger;
|
|
138
|
-
|
|
139
|
-
public MyController(
|
|
140
|
-
INotificationService notificationService,
|
|
141
|
-
ILogger<MyController> logger)
|
|
142
|
-
{
|
|
143
|
-
_notificationService = notificationService;
|
|
144
|
-
_logger = logger;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### ETAPE 3: Envoi de Notification
|
|
150
|
-
|
|
151
|
-
```csharp
|
|
152
|
-
// Notification simple
|
|
153
|
-
await _notificationService.SendNotificationAsync(
|
|
154
|
-
userId: targetUserId,
|
|
155
|
-
type: NotificationType.TicketCreated,
|
|
156
|
-
title: "Nouveau ticket",
|
|
157
|
-
message: $"Le ticket #{ticket.Number} a ete cree",
|
|
158
|
-
cancellationToken: ct
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Notification avec entite liee
|
|
162
|
-
await _notificationService.SendNotificationAsync(
|
|
163
|
-
userId: ticket.AssignedToId.Value,
|
|
164
|
-
type: NotificationType.TicketAssigned,
|
|
165
|
-
title: "Ticket assigne",
|
|
166
|
-
message: $"Le ticket #{ticket.Number} vous a ete assigne",
|
|
167
|
-
relatedEntityType: "Ticket",
|
|
168
|
-
relatedEntityId: ticket.Id,
|
|
169
|
-
actionUrl: $"/support/tickets/{ticket.Id}",
|
|
170
|
-
cancellationToken: ct
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// Notification a plusieurs utilisateurs
|
|
174
|
-
await _notificationService.SendNotificationsAsync(
|
|
175
|
-
userIds: teamUserIds,
|
|
176
|
-
type: NotificationType.SystemAnnouncement,
|
|
177
|
-
title: "Annonce equipe",
|
|
178
|
-
message: "Reunion planifiee demain a 10h",
|
|
179
|
-
cancellationToken: ct
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
// Notification a un role
|
|
183
|
-
await _notificationService.SendNotificationToRoleAsync(
|
|
184
|
-
roleName: "Admin",
|
|
185
|
-
type: NotificationType.SystemAnnouncement,
|
|
186
|
-
title: "Alerte systeme",
|
|
187
|
-
message: "Maintenance planifiee ce soir",
|
|
188
|
-
cancellationToken: ct
|
|
189
|
-
);
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### ETAPE 4: Preferences Utilisateur
|
|
193
|
-
|
|
194
|
-
```csharp
|
|
195
|
-
// Verifier si l'utilisateur veut etre notifie
|
|
196
|
-
var (shouldEmail, shouldInApp, shouldPush) =
|
|
197
|
-
await _notificationService.ShouldNotifyAsync(userId, notificationType);
|
|
198
|
-
|
|
199
|
-
if (shouldInApp)
|
|
200
|
-
{
|
|
201
|
-
await _notificationService.SendNotificationAsync(...);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (shouldEmail)
|
|
205
|
-
{
|
|
206
|
-
// Declencher workflow email
|
|
207
|
-
await _workflowService.TriggerAsync("ticket.created", new Dictionary<string, object>
|
|
208
|
-
{
|
|
209
|
-
["ticketNumber"] = ticket.Number,
|
|
210
|
-
["ticketTitle"] = ticket.Title,
|
|
211
|
-
["userEmail"] = user.Email,
|
|
212
|
-
["userName"] = user.DisplayName
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## FRONTEND INTEGRATION
|
|
220
|
-
|
|
221
|
-
### Hook useSignalR
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
// hooks/useSignalR.ts
|
|
225
|
-
import { useSignalR } from '@/hooks/useSignalR';
|
|
226
|
-
|
|
227
|
-
function MyComponent() {
|
|
228
|
-
useSignalR({
|
|
229
|
-
onNotification: (notification) => {
|
|
230
|
-
// Notification recue en real-time
|
|
231
|
-
console.log('New notification:', notification);
|
|
232
|
-
toast.info(notification.title);
|
|
233
|
-
},
|
|
234
|
-
onUnreadCountUpdate: (count) => {
|
|
235
|
-
// Mise a jour du badge
|
|
236
|
-
setUnreadCount(count);
|
|
237
|
-
},
|
|
238
|
-
onPermissionsChanged: () => {
|
|
239
|
-
// Permissions modifiees - recharger
|
|
240
|
-
queryClient.invalidateQueries(['permissions']);
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### API Notifications
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
// services/api/notificationsApi.ts
|
|
250
|
-
import { apiClient } from './apiClient';
|
|
251
|
-
|
|
252
|
-
export const notificationsApi = {
|
|
253
|
-
// Recuperer les non-lues
|
|
254
|
-
getUnread: (limit = 10) =>
|
|
255
|
-
apiClient.get<NotificationDto[]>(`/notifications/unread?limit=${limit}`),
|
|
256
|
-
|
|
257
|
-
// Recuperer avec pagination
|
|
258
|
-
getAll: (page = 1, pageSize = 20, isRead?: boolean) =>
|
|
259
|
-
apiClient.get<PagedResult<NotificationDto>>('/notifications', {
|
|
260
|
-
params: { page, pageSize, isRead }
|
|
261
|
-
}),
|
|
262
|
-
|
|
263
|
-
// Compter les non-lues
|
|
264
|
-
getUnreadCount: () =>
|
|
265
|
-
apiClient.get<{ count: number }>('/notifications/unread/count'),
|
|
266
|
-
|
|
267
|
-
// Marquer comme lue
|
|
268
|
-
markAsRead: (id: string) =>
|
|
269
|
-
apiClient.post(`/notifications/${id}/read`),
|
|
270
|
-
|
|
271
|
-
// Marquer toutes comme lues
|
|
272
|
-
markAllAsRead: () =>
|
|
273
|
-
apiClient.post('/notifications/read-all'),
|
|
274
|
-
|
|
275
|
-
// Supprimer
|
|
276
|
-
delete: (id: string) =>
|
|
277
|
-
apiClient.delete(`/notifications/${id}`),
|
|
278
|
-
|
|
279
|
-
// Supprimer toutes
|
|
280
|
-
deleteAll: () =>
|
|
281
|
-
apiClient.delete('/notifications'),
|
|
282
|
-
};
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Composant NotificationBell
|
|
286
|
-
|
|
287
|
-
```tsx
|
|
288
|
-
// components/notifications/NotificationBell.tsx
|
|
289
|
-
import { Bell } from 'lucide-react';
|
|
290
|
-
import { useSignalR } from '@/hooks/useSignalR';
|
|
291
|
-
import { notificationsApi } from '@/services/api/notificationsApi';
|
|
292
|
-
|
|
293
|
-
export function NotificationBell() {
|
|
294
|
-
const [notifications, setNotifications] = useState<NotificationDto[]>([]);
|
|
295
|
-
const [unreadCount, setUnreadCount] = useState(0);
|
|
296
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
297
|
-
|
|
298
|
-
// Real-time updates
|
|
299
|
-
useSignalR({
|
|
300
|
-
onNotification: (notification) => {
|
|
301
|
-
setNotifications(prev => [notification, ...prev]);
|
|
302
|
-
setUnreadCount(prev => prev + 1);
|
|
303
|
-
},
|
|
304
|
-
onUnreadCountUpdate: (count) => {
|
|
305
|
-
setUnreadCount(count);
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
// Load initial data
|
|
310
|
-
useEffect(() => {
|
|
311
|
-
loadNotifications();
|
|
312
|
-
}, []);
|
|
313
|
-
|
|
314
|
-
return (
|
|
315
|
-
<div className="relative">
|
|
316
|
-
<button onClick={() => setIsOpen(!isOpen)} className="relative p-2">
|
|
317
|
-
<Bell className="w-5 h-5" />
|
|
318
|
-
{unreadCount > 0 && (
|
|
319
|
-
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
320
|
-
{unreadCount > 99 ? '99+' : unreadCount}
|
|
321
|
-
</span>
|
|
322
|
-
)}
|
|
323
|
-
</button>
|
|
324
|
-
|
|
325
|
-
{isOpen && (
|
|
326
|
-
<NotificationDropdown
|
|
327
|
-
notifications={notifications}
|
|
328
|
-
onMarkAsRead={handleMarkAsRead}
|
|
329
|
-
onMarkAllAsRead={handleMarkAllAsRead}
|
|
330
|
-
onDelete={handleDelete}
|
|
331
|
-
/>
|
|
332
|
-
)}
|
|
333
|
-
</div>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
## TEMPLATES
|
|
341
|
-
|
|
342
|
-
### Template Service (Backend)
|
|
343
|
-
|
|
344
|
-
```csharp
|
|
345
|
-
// Services/{Module}/{Module}Service.cs
|
|
346
|
-
|
|
347
|
-
public async Task<Result> Create{Entity}Async(Create{Entity}Command command, CancellationToken ct)
|
|
348
|
-
{
|
|
349
|
-
// ... creation de l'entite ...
|
|
350
|
-
|
|
351
|
-
// Notification au createur (confirmation)
|
|
352
|
-
await _notificationService.SendNotificationAsync(
|
|
353
|
-
_currentUser.Id,
|
|
354
|
-
NotificationType.{Entity}Created,
|
|
355
|
-
"{Entity} creee",
|
|
356
|
-
$"Votre {entity.Name} a ete creee avec succes",
|
|
357
|
-
relatedEntityType: "{Entity}",
|
|
358
|
-
relatedEntityId: entity.Id,
|
|
359
|
-
actionUrl: $"/{module}/{entity.Id}",
|
|
360
|
-
ct);
|
|
361
|
-
|
|
362
|
-
// Notification aux responsables
|
|
363
|
-
if (entity.AssignedToId.HasValue)
|
|
364
|
-
{
|
|
365
|
-
await _notificationService.SendNotificationAsync(
|
|
366
|
-
entity.AssignedToId.Value,
|
|
367
|
-
NotificationType.{Entity}Assigned,
|
|
368
|
-
"{Entity} assignee",
|
|
369
|
-
$"La {entity.Name} vous a ete assignee par {_currentUser.DisplayName}",
|
|
370
|
-
relatedEntityType: "{Entity}",
|
|
371
|
-
relatedEntityId: entity.Id,
|
|
372
|
-
actionUrl: $"/{module}/{entity.Id}",
|
|
373
|
-
ct);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return Result.Success();
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
### Template Controller (Backend)
|
|
381
|
-
|
|
382
|
-
```csharp
|
|
383
|
-
// Controllers/{Area}/{Module}Controller.cs
|
|
384
|
-
|
|
385
|
-
[HttpPost]
|
|
386
|
-
[RequirePermission(Permissions.{Module}.Create)]
|
|
387
|
-
[ProducesResponseType(typeof({Entity}Dto), StatusCodes.Status201Created)]
|
|
388
|
-
public async Task<ActionResult<{Entity}Dto>> Create(
|
|
389
|
-
[FromBody] Create{Entity}Request request,
|
|
390
|
-
CancellationToken ct)
|
|
391
|
-
{
|
|
392
|
-
var entity = await _service.CreateAsync(request, ct);
|
|
393
|
-
|
|
394
|
-
// Notification automatique dans le service
|
|
395
|
-
// OU notification ici si logique specifique au controller
|
|
396
|
-
|
|
397
|
-
_logger.LogInformation(
|
|
398
|
-
"User {UserId} created {EntityType} {EntityId}",
|
|
399
|
-
_currentUser.Id, nameof({Entity}), entity.Id);
|
|
400
|
-
|
|
401
|
-
return CreatedAtAction(
|
|
402
|
-
nameof(GetById),
|
|
403
|
-
new { id = entity.Id },
|
|
404
|
-
entity);
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
### Template Hook (Frontend)
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
// hooks/use{Module}Notifications.ts
|
|
412
|
-
|
|
413
|
-
import { useSignalR } from '@/hooks/useSignalR';
|
|
414
|
-
import { useQueryClient } from '@tanstack/react-query';
|
|
415
|
-
import { toast } from 'sonner';
|
|
416
|
-
|
|
417
|
-
export function use{Module}Notifications() {
|
|
418
|
-
const queryClient = useQueryClient();
|
|
419
|
-
|
|
420
|
-
useSignalR({
|
|
421
|
-
onNotification: (notification) => {
|
|
422
|
-
// Filtrer les notifications de ce module
|
|
423
|
-
if (notification.relatedEntityType === '{Entity}') {
|
|
424
|
-
// Invalider les queries pour refresh
|
|
425
|
-
queryClient.invalidateQueries(['{module}']);
|
|
426
|
-
|
|
427
|
-
// Toast avec action
|
|
428
|
-
toast.info(notification.title, {
|
|
429
|
-
description: notification.message,
|
|
430
|
-
action: notification.actionUrl ? {
|
|
431
|
-
label: 'Voir',
|
|
432
|
-
onClick: () => navigate(notification.actionUrl),
|
|
433
|
-
} : undefined,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
---
|
|
442
|
-
|
|
443
|
-
## CHECKLIST INTEGRATION
|
|
444
|
-
|
|
445
|
-
```
|
|
446
|
-
□ Type de notification identifie (existant ou nouveau)
|
|
447
|
-
□ Si nouveau type: ajoute a NotificationType enum
|
|
448
|
-
□ Service injecte: INotificationService
|
|
449
|
-
□ Notification envoyee avec tous les champs:
|
|
450
|
-
□ userId (destinataire)
|
|
451
|
-
□ type (NotificationType)
|
|
452
|
-
□ title (titre court)
|
|
453
|
-
□ message (description)
|
|
454
|
-
□ relatedEntityType (optionnel)
|
|
455
|
-
□ relatedEntityId (optionnel)
|
|
456
|
-
□ actionUrl (optionnel)
|
|
457
|
-
□ Logging ajoute (LogInformation)
|
|
458
|
-
□ Frontend: useSignalR hook configure
|
|
459
|
-
□ Frontend: toast ou UI update sur reception
|
|
460
|
-
□ Si email requis: workflow configure
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
---
|
|
464
|
-
|
|
465
|
-
## PATTERNS AVANCES
|
|
466
|
-
|
|
467
|
-
### Pattern: Notification avec Delai (SLA)
|
|
468
|
-
|
|
469
|
-
```csharp
|
|
470
|
-
// Pour les SLA warnings, utiliser un job Hangfire
|
|
471
|
-
public class SlaNotificationJob
|
|
472
|
-
{
|
|
473
|
-
public async Task CheckSlaBreaches()
|
|
474
|
-
{
|
|
475
|
-
var tickets = await _context.Tickets
|
|
476
|
-
.Where(t => t.SlaDeadline <= DateTime.UtcNow.AddMinutes(30))
|
|
477
|
-
.Where(t => !t.SlaWarningNotificationSent)
|
|
478
|
-
.ToListAsync();
|
|
479
|
-
|
|
480
|
-
foreach (var ticket in tickets)
|
|
481
|
-
{
|
|
482
|
-
await _notificationService.SendNotificationAsync(
|
|
483
|
-
ticket.AssignedToId ?? ticket.CreatedById,
|
|
484
|
-
NotificationType.SlaWarning,
|
|
485
|
-
"SLA proche expiration",
|
|
486
|
-
$"Le ticket #{ticket.Number} expire dans 30 minutes",
|
|
487
|
-
relatedEntityType: "Ticket",
|
|
488
|
-
relatedEntityId: ticket.Id,
|
|
489
|
-
actionUrl: $"/support/tickets/{ticket.Id}");
|
|
490
|
-
|
|
491
|
-
ticket.SlaWarningNotificationSent = true;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
await _context.SaveChangesAsync();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
### Pattern: Notification Groupee
|
|
500
|
-
|
|
501
|
-
```csharp
|
|
502
|
-
// Pour eviter le spam, grouper les notifications similaires
|
|
503
|
-
public async Task SendBatchNotification(
|
|
504
|
-
Guid userId,
|
|
505
|
-
NotificationType type,
|
|
506
|
-
List<EntitySummary> entities)
|
|
507
|
-
{
|
|
508
|
-
if (entities.Count == 1)
|
|
509
|
-
{
|
|
510
|
-
await _notificationService.SendNotificationAsync(
|
|
511
|
-
userId, type,
|
|
512
|
-
$"Nouveau {entities[0].Type}",
|
|
513
|
-
entities[0].Description,
|
|
514
|
-
relatedEntityType: entities[0].Type,
|
|
515
|
-
relatedEntityId: entities[0].Id);
|
|
516
|
-
}
|
|
517
|
-
else
|
|
518
|
-
{
|
|
519
|
-
await _notificationService.SendNotificationAsync(
|
|
520
|
-
userId, type,
|
|
521
|
-
$"{entities.Count} nouveaux elements",
|
|
522
|
-
$"Vous avez {entities.Count} nouveaux {entities[0].Type}s a traiter",
|
|
523
|
-
actionUrl: "/dashboard/pending");
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
---
|
|
529
|
-
|
|
530
|
-
## REGLES ABSOLUES
|
|
531
|
-
|
|
532
|
-
1. **TOUJOURS** utiliser INotificationService (jamais acces DB direct)
|
|
533
|
-
2. **TOUJOURS** utiliser les types NotificationType enum
|
|
534
|
-
3. **TOUJOURS** inclure le context (relatedEntityType/Id) quand applicable
|
|
535
|
-
4. **TOUJOURS** ajouter actionUrl pour navigation
|
|
536
|
-
5. **TOUJOURS** logger les notifications envoyees
|
|
537
|
-
6. **TOUJOURS** respecter les preferences utilisateur
|
|
538
|
-
7. **JAMAIS** envoyer de notification sans destinataire valide
|
|
539
|
-
8. **JAMAIS** hardcoder les messages (utiliser i18n ou templates)
|
|
540
|
-
9. **JAMAIS** spam l'utilisateur (grouper les notifications similaires)
|
|
541
|
-
10. **JAMAIS** oublier le CancellationToken
|
|
542
|
-
|
|
543
|
-
---
|
|
544
|
-
|
|
545
|
-
## FICHIERS CLES
|
|
546
|
-
|
|
547
|
-
| Fichier | Role |
|
|
548
|
-
|---------|------|
|
|
549
|
-
| `Domain/Support/Notification.cs` | Entite notification |
|
|
550
|
-
| `Domain/Support/Enums/NotificationType.cs` | Types de notification |
|
|
551
|
-
| `Application/Common/Interfaces/INotificationService.cs` | Interface service |
|
|
552
|
-
| `Infrastructure/Services/Support/NotificationService.cs` | Implementation |
|
|
553
|
-
| `Infrastructure/Services/SignalR/NotificationHubService.cs` | Real-time |
|
|
554
|
-
| `web/src/hooks/useSignalR.ts` | Hook frontend |
|
|
555
|
-
| `web/src/components/notifications/NotificationBell.tsx` | UI Component |
|
|
1
|
+
---
|
|
2
|
+
name: notification
|
|
3
|
+
description: |
|
|
4
|
+
Integre le systeme de notifications SmartStack dans le developpement.
|
|
5
|
+
Utiliser ce skill quand:
|
|
6
|
+
- L'utilisateur veut envoyer des notifications depuis une feature
|
|
7
|
+
- L'utilisateur mentionne "notifier", "alerte", "notification", "in-app"
|
|
8
|
+
- Creation d'un module qui necessite des notifications utilisateur
|
|
9
|
+
- Integration SignalR pour real-time
|
|
10
|
+
Types: In-App, Email (via Workflow), Push (futur)
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Skill Notification SmartStack
|
|
14
|
+
|
|
15
|
+
> **Architecture:** Notifications = In-App (DB + SignalR) + Email (via Workflows)
|
|
16
|
+
> Les notifications push sont prevues mais pas encore implementees.
|
|
17
|
+
|
|
18
|
+
## QUAND CE SKILL S'ACTIVE
|
|
19
|
+
|
|
20
|
+
Claude invoque automatiquement ce skill quand il detecte :
|
|
21
|
+
|
|
22
|
+
| Declencheur | Exemple |
|
|
23
|
+
|-------------|---------|
|
|
24
|
+
| Demande explicite | "Notifie l'utilisateur quand un ticket est cree" |
|
|
25
|
+
| Mention notification | "Il faut alerter l'admin si..." |
|
|
26
|
+
| Event-driven | "Quand le SLA expire, prevenir l'utilisateur" |
|
|
27
|
+
| Real-time | "Afficher en temps reel les nouvelles notifications" |
|
|
28
|
+
| Mots-cles | "notifier", "alerte", "notification", "SignalR", "real-time" |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## ARCHITECTURE NOTIFICATIONS
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ NOTIFICATION FLOW │
|
|
37
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ │
|
|
39
|
+
│ [EVENT] ─────┬──────────────────────────────────────────────────────────┐ │
|
|
40
|
+
│ │ │ │
|
|
41
|
+
│ ▼ │ │
|
|
42
|
+
│ ┌─────────────────────────┐ │ │
|
|
43
|
+
│ │ INotificationService │ │ │
|
|
44
|
+
│ │ SendNotificationAsync │ │ │
|
|
45
|
+
│ └────────────┬────────────┘ │ │
|
|
46
|
+
│ │ │ │
|
|
47
|
+
│ ┌───────┴───────┐ │ │
|
|
48
|
+
│ ▼ ▼ │ │
|
|
49
|
+
│ ┌─────────┐ ┌───────────────────┐ │ │
|
|
50
|
+
│ │ DB │ │ SignalR Hub │ │ │
|
|
51
|
+
│ │ (store) │ │ (real-time push) │ │ │
|
|
52
|
+
│ └─────────┘ └─────────┬─────────┘ │ │
|
|
53
|
+
│ │ │ │
|
|
54
|
+
│ ▼ │ │
|
|
55
|
+
│ ┌──────────────────┐ │ │
|
|
56
|
+
│ │ Frontend Hook │ │ │
|
|
57
|
+
│ │ useSignalR() │ │ │
|
|
58
|
+
│ └─────────┬────────┘ │ │
|
|
59
|
+
│ │ │ │
|
|
60
|
+
│ ▼ │ │
|
|
61
|
+
│ ┌──────────────────┐ │ │
|
|
62
|
+
│ │ NotificationBell │ │ │
|
|
63
|
+
│ │ (UI Component) │ │ │
|
|
64
|
+
│ └──────────────────┘ │ │
|
|
65
|
+
│ │ │
|
|
66
|
+
│ [EMAIL] ── IWorkflowService.TriggerAsync() ── EmailTemplate ── SMTP │ │
|
|
67
|
+
│ │ │
|
|
68
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## TYPES DE NOTIFICATION
|
|
74
|
+
|
|
75
|
+
### Enum NotificationType
|
|
76
|
+
|
|
77
|
+
| Type | Declencheur | Usage |
|
|
78
|
+
|------|-------------|-------|
|
|
79
|
+
| `TicketCreated` | Nouveau ticket | Support |
|
|
80
|
+
| `TicketAssigned` | Ticket assigne | Support |
|
|
81
|
+
| `TicketStatusChanged` | Changement statut | Support |
|
|
82
|
+
| `TicketCommentAdded` | Nouveau commentaire | Support |
|
|
83
|
+
| `TicketResolved` | Ticket resolu | Support |
|
|
84
|
+
| `RoleAssigned` | Role attribue | Admin |
|
|
85
|
+
| `RoleRemoved` | Role retire | Admin |
|
|
86
|
+
| `RolePermissionsChanged` | Permissions modifiees | Admin |
|
|
87
|
+
| `SystemAnnouncement` | Annonce systeme | Global |
|
|
88
|
+
| `AccountUpdated` | Compte modifie | User |
|
|
89
|
+
| `SlaWarning` | SLA proche expiration | SLA |
|
|
90
|
+
| `SlaResponseBreached` | SLA reponse depasse | SLA |
|
|
91
|
+
| `SlaResolutionBreached` | SLA resolution depasse | SLA |
|
|
92
|
+
|
|
93
|
+
### Ajouter un Nouveau Type
|
|
94
|
+
|
|
95
|
+
```csharp
|
|
96
|
+
// 1. Domain/Support/Enums/NotificationType.cs
|
|
97
|
+
public enum NotificationType
|
|
98
|
+
{
|
|
99
|
+
// ... existants ...
|
|
100
|
+
|
|
101
|
+
// Ajouter le nouveau type
|
|
102
|
+
$NEW_TYPE = XX, // Increment depuis le dernier
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Utilisation dans le service
|
|
106
|
+
await _notificationService.SendNotificationAsync(
|
|
107
|
+
userId,
|
|
108
|
+
NotificationType.$NEW_TYPE,
|
|
109
|
+
title,
|
|
110
|
+
message,
|
|
111
|
+
relatedEntityType: "EntityName",
|
|
112
|
+
relatedEntityId: entityId,
|
|
113
|
+
actionUrl: "/path/to/entity"
|
|
114
|
+
);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## WORKFLOW INTEGRATION
|
|
120
|
+
|
|
121
|
+
### ETAPE 1: Identifier le Besoin
|
|
122
|
+
|
|
123
|
+
| Question | Reponse → Action |
|
|
124
|
+
|----------|------------------|
|
|
125
|
+
| L'utilisateur doit etre notifie en temps reel ? | → In-App + SignalR |
|
|
126
|
+
| L'utilisateur doit recevoir un email ? | → Workflow + EmailTemplate |
|
|
127
|
+
| L'action est liee a une entite ? | → Ajouter relatedEntityType/Id |
|
|
128
|
+
| L'utilisateur peut naviguer vers l'entite ? | → Ajouter actionUrl |
|
|
129
|
+
|
|
130
|
+
### ETAPE 2: Injection du Service
|
|
131
|
+
|
|
132
|
+
```csharp
|
|
133
|
+
// Dans le Controller ou Service
|
|
134
|
+
public class MyController : ControllerBase
|
|
135
|
+
{
|
|
136
|
+
private readonly INotificationService _notificationService;
|
|
137
|
+
private readonly ILogger<MyController> _logger;
|
|
138
|
+
|
|
139
|
+
public MyController(
|
|
140
|
+
INotificationService notificationService,
|
|
141
|
+
ILogger<MyController> logger)
|
|
142
|
+
{
|
|
143
|
+
_notificationService = notificationService;
|
|
144
|
+
_logger = logger;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### ETAPE 3: Envoi de Notification
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
// Notification simple
|
|
153
|
+
await _notificationService.SendNotificationAsync(
|
|
154
|
+
userId: targetUserId,
|
|
155
|
+
type: NotificationType.TicketCreated,
|
|
156
|
+
title: "Nouveau ticket",
|
|
157
|
+
message: $"Le ticket #{ticket.Number} a ete cree",
|
|
158
|
+
cancellationToken: ct
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Notification avec entite liee
|
|
162
|
+
await _notificationService.SendNotificationAsync(
|
|
163
|
+
userId: ticket.AssignedToId.Value,
|
|
164
|
+
type: NotificationType.TicketAssigned,
|
|
165
|
+
title: "Ticket assigne",
|
|
166
|
+
message: $"Le ticket #{ticket.Number} vous a ete assigne",
|
|
167
|
+
relatedEntityType: "Ticket",
|
|
168
|
+
relatedEntityId: ticket.Id,
|
|
169
|
+
actionUrl: $"/support/tickets/{ticket.Id}",
|
|
170
|
+
cancellationToken: ct
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Notification a plusieurs utilisateurs
|
|
174
|
+
await _notificationService.SendNotificationsAsync(
|
|
175
|
+
userIds: teamUserIds,
|
|
176
|
+
type: NotificationType.SystemAnnouncement,
|
|
177
|
+
title: "Annonce equipe",
|
|
178
|
+
message: "Reunion planifiee demain a 10h",
|
|
179
|
+
cancellationToken: ct
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Notification a un role
|
|
183
|
+
await _notificationService.SendNotificationToRoleAsync(
|
|
184
|
+
roleName: "Admin",
|
|
185
|
+
type: NotificationType.SystemAnnouncement,
|
|
186
|
+
title: "Alerte systeme",
|
|
187
|
+
message: "Maintenance planifiee ce soir",
|
|
188
|
+
cancellationToken: ct
|
|
189
|
+
);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### ETAPE 4: Preferences Utilisateur
|
|
193
|
+
|
|
194
|
+
```csharp
|
|
195
|
+
// Verifier si l'utilisateur veut etre notifie
|
|
196
|
+
var (shouldEmail, shouldInApp, shouldPush) =
|
|
197
|
+
await _notificationService.ShouldNotifyAsync(userId, notificationType);
|
|
198
|
+
|
|
199
|
+
if (shouldInApp)
|
|
200
|
+
{
|
|
201
|
+
await _notificationService.SendNotificationAsync(...);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (shouldEmail)
|
|
205
|
+
{
|
|
206
|
+
// Declencher workflow email
|
|
207
|
+
await _workflowService.TriggerAsync("ticket.created", new Dictionary<string, object>
|
|
208
|
+
{
|
|
209
|
+
["ticketNumber"] = ticket.Number,
|
|
210
|
+
["ticketTitle"] = ticket.Title,
|
|
211
|
+
["userEmail"] = user.Email,
|
|
212
|
+
["userName"] = user.DisplayName
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## FRONTEND INTEGRATION
|
|
220
|
+
|
|
221
|
+
### Hook useSignalR
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// hooks/useSignalR.ts
|
|
225
|
+
import { useSignalR } from '@/hooks/useSignalR';
|
|
226
|
+
|
|
227
|
+
function MyComponent() {
|
|
228
|
+
useSignalR({
|
|
229
|
+
onNotification: (notification) => {
|
|
230
|
+
// Notification recue en real-time
|
|
231
|
+
console.log('New notification:', notification);
|
|
232
|
+
toast.info(notification.title);
|
|
233
|
+
},
|
|
234
|
+
onUnreadCountUpdate: (count) => {
|
|
235
|
+
// Mise a jour du badge
|
|
236
|
+
setUnreadCount(count);
|
|
237
|
+
},
|
|
238
|
+
onPermissionsChanged: () => {
|
|
239
|
+
// Permissions modifiees - recharger
|
|
240
|
+
queryClient.invalidateQueries(['permissions']);
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### API Notifications
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// services/api/notificationsApi.ts
|
|
250
|
+
import { apiClient } from './apiClient';
|
|
251
|
+
|
|
252
|
+
export const notificationsApi = {
|
|
253
|
+
// Recuperer les non-lues
|
|
254
|
+
getUnread: (limit = 10) =>
|
|
255
|
+
apiClient.get<NotificationDto[]>(`/notifications/unread?limit=${limit}`),
|
|
256
|
+
|
|
257
|
+
// Recuperer avec pagination
|
|
258
|
+
getAll: (page = 1, pageSize = 20, isRead?: boolean) =>
|
|
259
|
+
apiClient.get<PagedResult<NotificationDto>>('/notifications', {
|
|
260
|
+
params: { page, pageSize, isRead }
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
// Compter les non-lues
|
|
264
|
+
getUnreadCount: () =>
|
|
265
|
+
apiClient.get<{ count: number }>('/notifications/unread/count'),
|
|
266
|
+
|
|
267
|
+
// Marquer comme lue
|
|
268
|
+
markAsRead: (id: string) =>
|
|
269
|
+
apiClient.post(`/notifications/${id}/read`),
|
|
270
|
+
|
|
271
|
+
// Marquer toutes comme lues
|
|
272
|
+
markAllAsRead: () =>
|
|
273
|
+
apiClient.post('/notifications/read-all'),
|
|
274
|
+
|
|
275
|
+
// Supprimer
|
|
276
|
+
delete: (id: string) =>
|
|
277
|
+
apiClient.delete(`/notifications/${id}`),
|
|
278
|
+
|
|
279
|
+
// Supprimer toutes
|
|
280
|
+
deleteAll: () =>
|
|
281
|
+
apiClient.delete('/notifications'),
|
|
282
|
+
};
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Composant NotificationBell
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
// components/notifications/NotificationBell.tsx
|
|
289
|
+
import { Bell } from 'lucide-react';
|
|
290
|
+
import { useSignalR } from '@/hooks/useSignalR';
|
|
291
|
+
import { notificationsApi } from '@/services/api/notificationsApi';
|
|
292
|
+
|
|
293
|
+
export function NotificationBell() {
|
|
294
|
+
const [notifications, setNotifications] = useState<NotificationDto[]>([]);
|
|
295
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
296
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
297
|
+
|
|
298
|
+
// Real-time updates
|
|
299
|
+
useSignalR({
|
|
300
|
+
onNotification: (notification) => {
|
|
301
|
+
setNotifications(prev => [notification, ...prev]);
|
|
302
|
+
setUnreadCount(prev => prev + 1);
|
|
303
|
+
},
|
|
304
|
+
onUnreadCountUpdate: (count) => {
|
|
305
|
+
setUnreadCount(count);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Load initial data
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
loadNotifications();
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="relative">
|
|
316
|
+
<button onClick={() => setIsOpen(!isOpen)} className="relative p-2">
|
|
317
|
+
<Bell className="w-5 h-5" />
|
|
318
|
+
{unreadCount > 0 && (
|
|
319
|
+
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
320
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
321
|
+
</span>
|
|
322
|
+
)}
|
|
323
|
+
</button>
|
|
324
|
+
|
|
325
|
+
{isOpen && (
|
|
326
|
+
<NotificationDropdown
|
|
327
|
+
notifications={notifications}
|
|
328
|
+
onMarkAsRead={handleMarkAsRead}
|
|
329
|
+
onMarkAllAsRead={handleMarkAllAsRead}
|
|
330
|
+
onDelete={handleDelete}
|
|
331
|
+
/>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## TEMPLATES
|
|
341
|
+
|
|
342
|
+
### Template Service (Backend)
|
|
343
|
+
|
|
344
|
+
```csharp
|
|
345
|
+
// Services/{Module}/{Module}Service.cs
|
|
346
|
+
|
|
347
|
+
public async Task<Result> Create{Entity}Async(Create{Entity}Command command, CancellationToken ct)
|
|
348
|
+
{
|
|
349
|
+
// ... creation de l'entite ...
|
|
350
|
+
|
|
351
|
+
// Notification au createur (confirmation)
|
|
352
|
+
await _notificationService.SendNotificationAsync(
|
|
353
|
+
_currentUser.Id,
|
|
354
|
+
NotificationType.{Entity}Created,
|
|
355
|
+
"{Entity} creee",
|
|
356
|
+
$"Votre {entity.Name} a ete creee avec succes",
|
|
357
|
+
relatedEntityType: "{Entity}",
|
|
358
|
+
relatedEntityId: entity.Id,
|
|
359
|
+
actionUrl: $"/{module}/{entity.Id}",
|
|
360
|
+
ct);
|
|
361
|
+
|
|
362
|
+
// Notification aux responsables
|
|
363
|
+
if (entity.AssignedToId.HasValue)
|
|
364
|
+
{
|
|
365
|
+
await _notificationService.SendNotificationAsync(
|
|
366
|
+
entity.AssignedToId.Value,
|
|
367
|
+
NotificationType.{Entity}Assigned,
|
|
368
|
+
"{Entity} assignee",
|
|
369
|
+
$"La {entity.Name} vous a ete assignee par {_currentUser.DisplayName}",
|
|
370
|
+
relatedEntityType: "{Entity}",
|
|
371
|
+
relatedEntityId: entity.Id,
|
|
372
|
+
actionUrl: $"/{module}/{entity.Id}",
|
|
373
|
+
ct);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return Result.Success();
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Template Controller (Backend)
|
|
381
|
+
|
|
382
|
+
```csharp
|
|
383
|
+
// Controllers/{Area}/{Module}Controller.cs
|
|
384
|
+
|
|
385
|
+
[HttpPost]
|
|
386
|
+
[RequirePermission(Permissions.{Module}.Create)]
|
|
387
|
+
[ProducesResponseType(typeof({Entity}Dto), StatusCodes.Status201Created)]
|
|
388
|
+
public async Task<ActionResult<{Entity}Dto>> Create(
|
|
389
|
+
[FromBody] Create{Entity}Request request,
|
|
390
|
+
CancellationToken ct)
|
|
391
|
+
{
|
|
392
|
+
var entity = await _service.CreateAsync(request, ct);
|
|
393
|
+
|
|
394
|
+
// Notification automatique dans le service
|
|
395
|
+
// OU notification ici si logique specifique au controller
|
|
396
|
+
|
|
397
|
+
_logger.LogInformation(
|
|
398
|
+
"User {UserId} created {EntityType} {EntityId}",
|
|
399
|
+
_currentUser.Id, nameof({Entity}), entity.Id);
|
|
400
|
+
|
|
401
|
+
return CreatedAtAction(
|
|
402
|
+
nameof(GetById),
|
|
403
|
+
new { id = entity.Id },
|
|
404
|
+
entity);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Template Hook (Frontend)
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// hooks/use{Module}Notifications.ts
|
|
412
|
+
|
|
413
|
+
import { useSignalR } from '@/hooks/useSignalR';
|
|
414
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
415
|
+
import { toast } from 'sonner';
|
|
416
|
+
|
|
417
|
+
export function use{Module}Notifications() {
|
|
418
|
+
const queryClient = useQueryClient();
|
|
419
|
+
|
|
420
|
+
useSignalR({
|
|
421
|
+
onNotification: (notification) => {
|
|
422
|
+
// Filtrer les notifications de ce module
|
|
423
|
+
if (notification.relatedEntityType === '{Entity}') {
|
|
424
|
+
// Invalider les queries pour refresh
|
|
425
|
+
queryClient.invalidateQueries(['{module}']);
|
|
426
|
+
|
|
427
|
+
// Toast avec action
|
|
428
|
+
toast.info(notification.title, {
|
|
429
|
+
description: notification.message,
|
|
430
|
+
action: notification.actionUrl ? {
|
|
431
|
+
label: 'Voir',
|
|
432
|
+
onClick: () => navigate(notification.actionUrl),
|
|
433
|
+
} : undefined,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## CHECKLIST INTEGRATION
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
□ Type de notification identifie (existant ou nouveau)
|
|
447
|
+
□ Si nouveau type: ajoute a NotificationType enum
|
|
448
|
+
□ Service injecte: INotificationService
|
|
449
|
+
□ Notification envoyee avec tous les champs:
|
|
450
|
+
□ userId (destinataire)
|
|
451
|
+
□ type (NotificationType)
|
|
452
|
+
□ title (titre court)
|
|
453
|
+
□ message (description)
|
|
454
|
+
□ relatedEntityType (optionnel)
|
|
455
|
+
□ relatedEntityId (optionnel)
|
|
456
|
+
□ actionUrl (optionnel)
|
|
457
|
+
□ Logging ajoute (LogInformation)
|
|
458
|
+
□ Frontend: useSignalR hook configure
|
|
459
|
+
□ Frontend: toast ou UI update sur reception
|
|
460
|
+
□ Si email requis: workflow configure
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## PATTERNS AVANCES
|
|
466
|
+
|
|
467
|
+
### Pattern: Notification avec Delai (SLA)
|
|
468
|
+
|
|
469
|
+
```csharp
|
|
470
|
+
// Pour les SLA warnings, utiliser un job Hangfire
|
|
471
|
+
public class SlaNotificationJob
|
|
472
|
+
{
|
|
473
|
+
public async Task CheckSlaBreaches()
|
|
474
|
+
{
|
|
475
|
+
var tickets = await _context.Tickets
|
|
476
|
+
.Where(t => t.SlaDeadline <= DateTime.UtcNow.AddMinutes(30))
|
|
477
|
+
.Where(t => !t.SlaWarningNotificationSent)
|
|
478
|
+
.ToListAsync();
|
|
479
|
+
|
|
480
|
+
foreach (var ticket in tickets)
|
|
481
|
+
{
|
|
482
|
+
await _notificationService.SendNotificationAsync(
|
|
483
|
+
ticket.AssignedToId ?? ticket.CreatedById,
|
|
484
|
+
NotificationType.SlaWarning,
|
|
485
|
+
"SLA proche expiration",
|
|
486
|
+
$"Le ticket #{ticket.Number} expire dans 30 minutes",
|
|
487
|
+
relatedEntityType: "Ticket",
|
|
488
|
+
relatedEntityId: ticket.Id,
|
|
489
|
+
actionUrl: $"/support/tickets/{ticket.Id}");
|
|
490
|
+
|
|
491
|
+
ticket.SlaWarningNotificationSent = true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await _context.SaveChangesAsync();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Pattern: Notification Groupee
|
|
500
|
+
|
|
501
|
+
```csharp
|
|
502
|
+
// Pour eviter le spam, grouper les notifications similaires
|
|
503
|
+
public async Task SendBatchNotification(
|
|
504
|
+
Guid userId,
|
|
505
|
+
NotificationType type,
|
|
506
|
+
List<EntitySummary> entities)
|
|
507
|
+
{
|
|
508
|
+
if (entities.Count == 1)
|
|
509
|
+
{
|
|
510
|
+
await _notificationService.SendNotificationAsync(
|
|
511
|
+
userId, type,
|
|
512
|
+
$"Nouveau {entities[0].Type}",
|
|
513
|
+
entities[0].Description,
|
|
514
|
+
relatedEntityType: entities[0].Type,
|
|
515
|
+
relatedEntityId: entities[0].Id);
|
|
516
|
+
}
|
|
517
|
+
else
|
|
518
|
+
{
|
|
519
|
+
await _notificationService.SendNotificationAsync(
|
|
520
|
+
userId, type,
|
|
521
|
+
$"{entities.Count} nouveaux elements",
|
|
522
|
+
$"Vous avez {entities.Count} nouveaux {entities[0].Type}s a traiter",
|
|
523
|
+
actionUrl: "/dashboard/pending");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## REGLES ABSOLUES
|
|
531
|
+
|
|
532
|
+
1. **TOUJOURS** utiliser INotificationService (jamais acces DB direct)
|
|
533
|
+
2. **TOUJOURS** utiliser les types NotificationType enum
|
|
534
|
+
3. **TOUJOURS** inclure le context (relatedEntityType/Id) quand applicable
|
|
535
|
+
4. **TOUJOURS** ajouter actionUrl pour navigation
|
|
536
|
+
5. **TOUJOURS** logger les notifications envoyees
|
|
537
|
+
6. **TOUJOURS** respecter les preferences utilisateur
|
|
538
|
+
7. **JAMAIS** envoyer de notification sans destinataire valide
|
|
539
|
+
8. **JAMAIS** hardcoder les messages (utiliser i18n ou templates)
|
|
540
|
+
9. **JAMAIS** spam l'utilisateur (grouper les notifications similaires)
|
|
541
|
+
10. **JAMAIS** oublier le CancellationToken
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## FICHIERS CLES
|
|
546
|
+
|
|
547
|
+
| Fichier | Role |
|
|
548
|
+
|---------|------|
|
|
549
|
+
| `Domain/Support/Notification.cs` | Entite notification |
|
|
550
|
+
| `Domain/Support/Enums/NotificationType.cs` | Types de notification |
|
|
551
|
+
| `Application/Common/Interfaces/INotificationService.cs` | Interface service |
|
|
552
|
+
| `Infrastructure/Services/Support/NotificationService.cs` | Implementation |
|
|
553
|
+
| `Infrastructure/Services/SignalR/NotificationHubService.cs` | Real-time |
|
|
554
|
+
| `web/src/hooks/useSignalR.ts` | Hook frontend |
|
|
555
|
+
| `web/src/components/notifications/NotificationBell.tsx` | UI Component |
|