@atlashub/smartstack-cli 1.10.2 → 1.12.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 +7 -2
- package/.documentation/apex.html +7 -2
- package/.documentation/business-analyse.html +7 -2
- package/.documentation/cli-commands.html +871 -0
- package/.documentation/commands.html +7 -2
- package/.documentation/efcore.html +7 -2
- package/.documentation/gitflow.html +7 -2
- package/.documentation/hooks.html +7 -2
- package/.documentation/index.html +7 -2
- package/.documentation/init.html +7 -2
- package/.documentation/installation.html +7 -2
- package/.documentation/ralph-loop.html +7 -2
- package/.documentation/test-web.html +7 -2
- package/dist/index.js +1932 -336
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/templates/agents/efcore/squash.md +67 -31
- package/templates/agents/gitflow/finish.md +68 -56
- package/templates/commands/business-analyse/0-orchestrate.md +72 -556
- package/templates/commands/business-analyse/1-init.md +23 -193
- package/templates/commands/business-analyse/2-discover.md +85 -462
- package/templates/commands/business-analyse/3-analyse.md +40 -342
- package/templates/commands/business-analyse/4-specify.md +72 -537
- package/templates/commands/business-analyse/5-validate.md +43 -237
- package/templates/commands/business-analyse/6-handoff.md +93 -682
- package/templates/commands/business-analyse/7-doc-html.md +45 -544
- package/templates/commands/business-analyse/_shared.md +176 -0
- package/templates/commands/business-analyse/bug.md +50 -257
- package/templates/commands/business-analyse/change-request.md +59 -283
- package/templates/commands/business-analyse/hotfix.md +36 -120
- package/templates/commands/business-analyse.md +55 -574
- package/templates/commands/efcore/_shared.md +206 -0
- package/templates/commands/efcore/conflicts.md +39 -201
- package/templates/commands/efcore/db-deploy.md +28 -237
- package/templates/commands/efcore/db-reset.md +41 -390
- package/templates/commands/efcore/db-seed.md +44 -323
- package/templates/commands/efcore/db-status.md +31 -210
- package/templates/commands/efcore/migration.md +45 -368
- package/templates/commands/efcore/rebase-snapshot.md +38 -241
- package/templates/commands/efcore/scan.md +35 -204
- package/templates/commands/efcore/squash.md +158 -251
- package/templates/commands/efcore.md +49 -177
- package/templates/commands/gitflow/1-init.md +94 -1318
- package/templates/commands/gitflow/10-start.md +86 -990
- package/templates/commands/gitflow/11-finish.md +264 -454
- package/templates/commands/gitflow/12-cleanup.md +40 -213
- package/templates/commands/gitflow/2-status.md +51 -386
- package/templates/commands/gitflow/3-commit.md +108 -801
- package/templates/commands/gitflow/4-plan.md +42 -13
- package/templates/commands/gitflow/5-exec.md +60 -5
- package/templates/commands/gitflow/6-abort.md +54 -277
- package/templates/commands/gitflow/7-pull-request.md +74 -717
- package/templates/commands/gitflow/8-review.md +51 -178
- package/templates/commands/gitflow/9-merge.md +74 -404
- package/templates/commands/gitflow/_shared.md +196 -0
- package/templates/commands/quickstart.md +154 -0
- package/templates/commands/ralph-loop/ralph-loop.md +104 -2
- package/templates/hooks/hooks.json +13 -0
- package/templates/hooks/ralph-mcp-logger.sh +46 -0
- package/templates/hooks/ralph-session-end.sh +69 -0
- package/templates/ralph/README.md +91 -0
- package/templates/ralph/ralph.config.yaml +113 -0
- package/templates/scripts/setup-ralph-loop.sh +173 -0
- package/templates/skills/_shared.md +117 -0
- package/templates/skills/ai-prompt/SKILL.md +87 -654
- package/templates/skills/application/SKILL.md +76 -499
- package/templates/skills/controller/SKILL.md +38 -165
- package/templates/skills/documentation/SKILL.md +2 -1
- package/templates/skills/feature-full/SKILL.md +107 -732
- package/templates/skills/notification/SKILL.md +85 -474
- package/templates/skills/ui-components/SKILL.md +62 -762
- package/templates/skills/workflow/SKILL.md +85 -489
- package/templates/commands/gitflow/rescue.md +0 -867
- package/templates/skills/business-analyse/SKILL.md +0 -191
|
@@ -12,544 +12,155 @@ description: |
|
|
|
12
12
|
|
|
13
13
|
# Skill Notification SmartStack
|
|
14
14
|
|
|
15
|
-
> **Architecture:**
|
|
16
|
-
> Les notifications push sont prevues mais pas encore implementees.
|
|
15
|
+
> **Architecture:** In-App (DB + SignalR) + Email (via Workflows)
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
**Référence:** [_shared.md](../_shared.md) pour services communs
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
## QUAND CE SKILL S'ACTIVE
|
|
21
20
|
|
|
22
21
|
| Declencheur | Exemple |
|
|
23
22
|
|-------------|---------|
|
|
24
23
|
| Demande explicite | "Notifie l'utilisateur quand un ticket est cree" |
|
|
25
|
-
| Mention notification | "Il faut alerter l'admin si..." |
|
|
26
24
|
| Event-driven | "Quand le SLA expire, prevenir l'utilisateur" |
|
|
27
25
|
| Real-time | "Afficher en temps reel les nouvelles notifications" |
|
|
28
|
-
| Mots-cles | "notifier", "alerte", "
|
|
29
|
-
|
|
30
|
-
---
|
|
26
|
+
| Mots-cles | "notifier", "alerte", "SignalR", "real-time" |
|
|
31
27
|
|
|
32
|
-
##
|
|
28
|
+
## FLOW
|
|
33
29
|
|
|
34
30
|
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
└─────────────────────────────────────────────────────────────────────────────┘
|
|
31
|
+
EVENT → INotificationService.SendNotificationAsync()
|
|
32
|
+
↓
|
|
33
|
+
DB (store) + SignalR Hub (real-time)
|
|
34
|
+
↓
|
|
35
|
+
Frontend: useSignalR() → NotificationBell
|
|
69
36
|
```
|
|
70
37
|
|
|
71
|
-
---
|
|
72
|
-
|
|
73
38
|
## TYPES DE NOTIFICATION
|
|
74
39
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
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 |
|
|
40
|
+
| Type | Usage |
|
|
41
|
+
|------|-------|
|
|
42
|
+
| `TicketCreated/Assigned/StatusChanged` | Support |
|
|
43
|
+
| `RoleAssigned/Removed/PermissionsChanged` | Admin |
|
|
44
|
+
| `SystemAnnouncement` | Global |
|
|
45
|
+
| `SlaWarning/ResponseBreached/ResolutionBreached` | SLA |
|
|
92
46
|
|
|
93
47
|
### Ajouter un Nouveau Type
|
|
94
|
-
|
|
95
48
|
```csharp
|
|
96
49
|
// 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
|
-
}
|
|
50
|
+
public enum NotificationType { ..., $NEW_TYPE = XX }
|
|
104
51
|
|
|
105
|
-
// 2. Utilisation
|
|
106
|
-
await _notificationService.SendNotificationAsync(
|
|
107
|
-
|
|
108
|
-
NotificationType.$NEW_TYPE,
|
|
109
|
-
title,
|
|
110
|
-
message,
|
|
111
|
-
relatedEntityType: "EntityName",
|
|
112
|
-
relatedEntityId: entityId,
|
|
113
|
-
actionUrl: "/path/to/entity"
|
|
114
|
-
);
|
|
52
|
+
// 2. Utilisation
|
|
53
|
+
await _notificationService.SendNotificationAsync(userId, NotificationType.$NEW_TYPE, title, message,
|
|
54
|
+
relatedEntityType: "Entity", relatedEntityId: id, actionUrl: "/path");
|
|
115
55
|
```
|
|
116
56
|
|
|
117
|
-
---
|
|
118
|
-
|
|
119
57
|
## WORKFLOW INTEGRATION
|
|
120
58
|
|
|
121
|
-
###
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
| L'utilisateur peut naviguer vers l'entite ? | → Ajouter actionUrl |
|
|
129
|
-
|
|
130
|
-
### ETAPE 2: Injection du Service
|
|
59
|
+
### 1. Identifier le Besoin
|
|
60
|
+
| Question | Action |
|
|
61
|
+
|----------|--------|
|
|
62
|
+
| Real-time ? | → In-App + SignalR |
|
|
63
|
+
| Email ? | → Workflow + EmailTemplate |
|
|
64
|
+
| Entite liee ? | → relatedEntityType/Id |
|
|
65
|
+
| Navigation ? | → actionUrl |
|
|
131
66
|
|
|
67
|
+
### 2. Envoi de Notification
|
|
132
68
|
```csharp
|
|
133
|
-
//
|
|
134
|
-
|
|
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
|
-
```
|
|
69
|
+
// Simple
|
|
70
|
+
await _notificationService.SendNotificationAsync(userId, NotificationType.TicketCreated,
|
|
71
|
+
"Nouveau ticket", $"Ticket #{ticket.Number} cree", ct);
|
|
148
72
|
|
|
149
|
-
|
|
73
|
+
// Avec entite liee
|
|
74
|
+
await _notificationService.SendNotificationAsync(userId, NotificationType.TicketAssigned,
|
|
75
|
+
"Ticket assigne", message,
|
|
76
|
+
relatedEntityType: "Ticket", relatedEntityId: ticket.Id,
|
|
77
|
+
actionUrl: $"/support/tickets/{ticket.Id}", ct);
|
|
150
78
|
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
);
|
|
79
|
+
// Multi-utilisateurs
|
|
80
|
+
await _notificationService.SendNotificationsAsync(userIds, type, title, message, ct);
|
|
160
81
|
|
|
161
|
-
//
|
|
162
|
-
await _notificationService.
|
|
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
|
-
);
|
|
82
|
+
// Par role
|
|
83
|
+
await _notificationService.SendNotificationToRoleAsync("Admin", type, title, message, ct);
|
|
190
84
|
```
|
|
191
85
|
|
|
192
|
-
###
|
|
193
|
-
|
|
86
|
+
### 3. Preferences Utilisateur
|
|
194
87
|
```csharp
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
}
|
|
88
|
+
var (shouldEmail, shouldInApp, shouldPush) = await _notificationService.ShouldNotifyAsync(userId, type);
|
|
89
|
+
if (shouldInApp) await _notificationService.SendNotificationAsync(...);
|
|
90
|
+
if (shouldEmail) await _workflowService.TriggerAsync("ticket.created", variables);
|
|
215
91
|
```
|
|
216
92
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
## FRONTEND INTEGRATION
|
|
93
|
+
## FRONTEND
|
|
220
94
|
|
|
221
95
|
### Hook useSignalR
|
|
222
|
-
|
|
223
96
|
```typescript
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
}
|
|
97
|
+
useSignalR({
|
|
98
|
+
onNotification: (notification) => { toast.info(notification.title); },
|
|
99
|
+
onUnreadCountUpdate: (count) => { setUnreadCount(count); },
|
|
100
|
+
onPermissionsChanged: () => { queryClient.invalidateQueries(['permissions']); }
|
|
101
|
+
});
|
|
244
102
|
```
|
|
245
103
|
|
|
246
104
|
### API Notifications
|
|
247
|
-
|
|
248
105
|
```typescript
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
};
|
|
106
|
+
notificationsApi.getUnread(limit)
|
|
107
|
+
notificationsApi.getAll(page, pageSize, isRead?)
|
|
108
|
+
notificationsApi.getUnreadCount()
|
|
109
|
+
notificationsApi.markAsRead(id) / markAllAsRead()
|
|
110
|
+
notificationsApi.delete(id) / deleteAll()
|
|
283
111
|
```
|
|
284
112
|
|
|
285
|
-
###
|
|
286
|
-
|
|
113
|
+
### NotificationBell Component
|
|
287
114
|
```tsx
|
|
288
|
-
//
|
|
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
|
-
}
|
|
115
|
+
<NotificationBell /> // Gere useSignalR, badge unreadCount, dropdown
|
|
336
116
|
```
|
|
337
117
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
## TEMPLATES
|
|
341
|
-
|
|
342
|
-
### Template Service (Backend)
|
|
118
|
+
## PATTERNS AVANCES
|
|
343
119
|
|
|
120
|
+
### SLA Warning (Job Hangfire)
|
|
344
121
|
```csharp
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
{
|
|
349
|
-
|
|
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();
|
|
122
|
+
var tickets = await _context.Tickets
|
|
123
|
+
.Where(t => t.SlaDeadline <= DateTime.UtcNow.AddMinutes(30) && !t.SlaWarningNotificationSent)
|
|
124
|
+
.ToListAsync();
|
|
125
|
+
foreach (var ticket in tickets) {
|
|
126
|
+
await _notificationService.SendNotificationAsync(..., NotificationType.SlaWarning, ...);
|
|
127
|
+
ticket.SlaWarningNotificationSent = true;
|
|
377
128
|
}
|
|
378
129
|
```
|
|
379
130
|
|
|
380
|
-
###
|
|
381
|
-
|
|
131
|
+
### Notification Groupee
|
|
382
132
|
```csharp
|
|
383
|
-
|
|
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
|
-
}
|
|
133
|
+
if (entities.Count == 1) await SendSingle(...);
|
|
134
|
+
else await SendBatch($"{entities.Count} nouveaux elements", ...);
|
|
406
135
|
```
|
|
407
136
|
|
|
408
|
-
|
|
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}']);
|
|
137
|
+
## CHECKLIST
|
|
426
138
|
|
|
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
139
|
```
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
140
|
+
□ Type notification identifie (existant ou nouveau enum)
|
|
141
|
+
□ INotificationService injecte
|
|
142
|
+
□ Envoi avec: userId, type, title, message, relatedEntityType?, relatedEntityId?, actionUrl?
|
|
143
|
+
□ Frontend: useSignalR configure, toast/UI sur reception
|
|
144
|
+
□ Si email: workflow configure
|
|
445
145
|
```
|
|
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
146
|
|
|
530
147
|
## REGLES ABSOLUES
|
|
531
148
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
---
|
|
149
|
+
| DO | DON'T |
|
|
150
|
+
|----|-------|
|
|
151
|
+
| INotificationService | Acces DB direct |
|
|
152
|
+
| NotificationType enum | Types hardcodes |
|
|
153
|
+
| relatedEntityType/Id + actionUrl | Notifications sans contexte |
|
|
154
|
+
| Respecter preferences utilisateur | Spam notifications |
|
|
155
|
+
| CancellationToken | Messages hardcodes (utiliser i18n) |
|
|
544
156
|
|
|
545
157
|
## FICHIERS CLES
|
|
546
158
|
|
|
547
159
|
| Fichier | Role |
|
|
548
160
|
|---------|------|
|
|
549
|
-
| `Domain/Support/Notification.cs` | Entite
|
|
550
|
-
| `Domain/Support/Enums/NotificationType.cs` | Types
|
|
551
|
-
| `Application/Common/Interfaces/INotificationService.cs` | Interface
|
|
552
|
-
| `Infrastructure/Services/Support/NotificationService.cs` | Implementation |
|
|
161
|
+
| `Domain/Support/Notification.cs` | Entite |
|
|
162
|
+
| `Domain/Support/Enums/NotificationType.cs` | Types |
|
|
163
|
+
| `Application/Common/Interfaces/INotificationService.cs` | Interface |
|
|
553
164
|
| `Infrastructure/Services/SignalR/NotificationHubService.cs` | Real-time |
|
|
554
165
|
| `web/src/hooks/useSignalR.ts` | Hook frontend |
|
|
555
|
-
| `web/src/components/notifications/NotificationBell.tsx` | UI
|
|
166
|
+
| `web/src/components/notifications/NotificationBell.tsx` | UI |
|