743 lines
22 KiB
Markdown
743 lines
22 KiB
Markdown
# 🔔 Architecture Simplifiée des Notifications
|
|
|
|
## 💡 Concept : Notifications Dépendantes des Widgets
|
|
|
|
Au lieu d'avoir des **adapters séparés** qui pollent directement les services externes, les notifications sont **déclenchées par les widgets** quand ils détectent de nouveaux éléments.
|
|
|
|
---
|
|
|
|
## 🎯 Avantages de cette Approche
|
|
|
|
### ✅ **Simplification Majeure**
|
|
- **Pas de duplication** : Les widgets font déjà le fetch, pourquoi refaire ?
|
|
- **Source unique de vérité** : Les notifications reflètent exactement ce que les widgets affichent
|
|
- **Moins de code** : Supprime les adapters complexes (LeantimeAdapter, RocketChatAdapter, EmailAdapter)
|
|
- **Moins de maintenance** : Un seul endroit à maintenir (les widgets)
|
|
|
|
### ✅ **Performance**
|
|
- **Pas de polling séparé** : Les widgets pollent déjà, pas besoin de doubler
|
|
- **Event-driven** : Notifications déclenchées uniquement quand nécessaire
|
|
- **Réduction de 70-80% des appels API** : Pas de fetch séparés pour les notifications
|
|
|
|
### ✅ **Cohérence**
|
|
- **Notifications = Widgets** : Si le widget voit quelque chose, la notification le voit aussi
|
|
- **Pas de désynchronisation** : Impossible d'avoir des notifications sans widget correspondant
|
|
- **UX cohérente** : Les utilisateurs voient les mêmes données partout
|
|
|
|
### ✅ **Extensibilité**
|
|
- **Facile d'ajouter de nouveaux widgets** : Il suffit d'ajouter un trigger
|
|
- **Pas besoin de créer un adapter** : Juste déclencher une notification
|
|
- **Architecture simple** : Un système d'événements centralisé
|
|
|
|
---
|
|
|
|
## 🏗️ Architecture Proposée
|
|
|
|
### Architecture Actuelle ❌
|
|
|
|
```
|
|
NotificationService
|
|
├─> LeantimeAdapter (polling direct)
|
|
├─> RocketChatAdapter (polling direct)
|
|
└─> EmailAdapter (polling direct)
|
|
└─> Fetch séparé des données
|
|
└─> Agrégation
|
|
└─> Badge mis à jour
|
|
```
|
|
|
|
**Problèmes** :
|
|
- Duplication de logique (widgets + adapters)
|
|
- Polling séparé = charge serveur double
|
|
- Risque de désynchronisation
|
|
- Complexité élevée
|
|
|
|
### Architecture Simplifiée ✅
|
|
|
|
```
|
|
Widget Courrier
|
|
└─> Détecte nouvel email
|
|
└─> triggerNotification('email', { count: unreadCount })
|
|
└─> NotificationService.recordCount('email', count)
|
|
└─> Badge mis à jour
|
|
|
|
Widget Parole
|
|
└─> Détecte nouveau message
|
|
└─> triggerNotification('rocketchat', { count: unreadCount })
|
|
└─> NotificationService.recordCount('rocketchat', count)
|
|
└─> Badge mis à jour
|
|
|
|
Widget Devoirs
|
|
└─> Détecte nouvelle tâche
|
|
└─> triggerNotification('leantime', { count: overdueTasks })
|
|
└─> NotificationService.recordCount('leantime', count)
|
|
└─> Badge mis à jour
|
|
|
|
Widget Agenda
|
|
└─> Détecte nouvel événement
|
|
└─> triggerNotification('calendar', { count: upcomingEvents })
|
|
└─> NotificationService.recordCount('calendar', count)
|
|
└─> Badge mis à jour
|
|
```
|
|
|
|
**Avantages** :
|
|
- ✅ Pas de duplication
|
|
- ✅ Source unique de vérité
|
|
- ✅ Event-driven (pas de polling séparé)
|
|
- ✅ Simple et maintenable
|
|
|
|
---
|
|
|
|
## 🔧 Implémentation
|
|
|
|
### 1. Nouveau Hook : `useWidgetNotification`
|
|
|
|
**Fichier :** `hooks/use-widget-notification.ts`
|
|
|
|
```typescript
|
|
import { useCallback, useRef } from 'react';
|
|
import { useSession } from 'next-auth/react';
|
|
|
|
interface NotificationData {
|
|
source: 'email' | 'rocketchat' | 'leantime' | 'calendar';
|
|
count: number;
|
|
items?: Array<{
|
|
id: string;
|
|
title: string;
|
|
message: string;
|
|
link?: string;
|
|
timestamp: Date;
|
|
}>;
|
|
}
|
|
|
|
export function useWidgetNotification() {
|
|
const { data: session } = useSession();
|
|
const lastUpdateRef = useRef<Record<string, number>>({});
|
|
const DEBOUNCE_MS = 1000; // 1 second debounce per source
|
|
|
|
const triggerNotification = useCallback(async (data: NotificationData) => {
|
|
if (!session?.user?.id) return;
|
|
|
|
const { source, count, items } = data;
|
|
const now = Date.now();
|
|
const lastUpdate = lastUpdateRef.current[source] || 0;
|
|
|
|
// Debounce per source
|
|
if (now - lastUpdate < DEBOUNCE_MS) {
|
|
return;
|
|
}
|
|
lastUpdateRef.current[source] = now;
|
|
|
|
try {
|
|
// Envoyer les données au service de notifications
|
|
await fetch('/api/notifications/update', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
source,
|
|
count,
|
|
items: items || [],
|
|
}),
|
|
});
|
|
|
|
// Dispatch event pour mise à jour immédiate du badge
|
|
window.dispatchEvent(new CustomEvent('notification-updated', {
|
|
detail: { source, count }
|
|
}));
|
|
} catch (error) {
|
|
console.error('[useWidgetNotification] Error updating notification:', error);
|
|
}
|
|
}, [session?.user?.id]);
|
|
|
|
return { triggerNotification };
|
|
}
|
|
```
|
|
|
|
### 2. Nouveau Service : `NotificationRegistry`
|
|
|
|
**Fichier :** `lib/services/notifications/notification-registry.ts`
|
|
|
|
```typescript
|
|
import { getRedisClient } from '@/lib/redis';
|
|
import { logger } from '@/lib/logger';
|
|
import { NotificationCount } from '@/lib/types/notification';
|
|
|
|
export class NotificationRegistry {
|
|
private static instance: NotificationRegistry;
|
|
private static COUNT_CACHE_KEY = (userId: string) => `notifications:count:${userId}`;
|
|
private static COUNT_CACHE_TTL = 30; // 30 seconds
|
|
|
|
public static getInstance(): NotificationRegistry {
|
|
if (!NotificationRegistry.instance) {
|
|
NotificationRegistry.instance = new NotificationRegistry();
|
|
}
|
|
return NotificationRegistry.instance;
|
|
}
|
|
|
|
/**
|
|
* Enregistre le count d'une source (appelé par les widgets)
|
|
*/
|
|
async recordCount(
|
|
userId: string,
|
|
source: string,
|
|
count: number,
|
|
items?: Array<{ id: string; title: string; message: string; link?: string; timestamp: Date }>
|
|
): Promise<void> {
|
|
const redis = getRedisClient();
|
|
const cacheKey = NotificationRegistry.COUNT_CACHE_KEY(userId);
|
|
|
|
// Récupérer le count actuel
|
|
let currentCount: NotificationCount = {
|
|
total: 0,
|
|
unread: 0,
|
|
sources: {},
|
|
};
|
|
|
|
try {
|
|
const cached = await redis.get(cacheKey);
|
|
if (cached) {
|
|
currentCount = JSON.parse(cached);
|
|
}
|
|
} catch (error) {
|
|
logger.error('[NOTIFICATION_REGISTRY] Error reading cache', { error });
|
|
}
|
|
|
|
// Mettre à jour le count pour cette source
|
|
const previousSourceCount = currentCount.sources[source]?.unread || 0;
|
|
currentCount.sources[source] = {
|
|
total: count,
|
|
unread: count,
|
|
};
|
|
|
|
// Recalculer le total
|
|
currentCount.unread = Object.values(currentCount.sources).reduce(
|
|
(sum, s) => sum + s.unread,
|
|
0
|
|
);
|
|
currentCount.total = currentCount.unread;
|
|
|
|
// Stocker dans le cache
|
|
try {
|
|
await redis.set(
|
|
cacheKey,
|
|
JSON.stringify(currentCount),
|
|
'EX',
|
|
NotificationRegistry.COUNT_CACHE_TTL
|
|
);
|
|
|
|
logger.debug('[NOTIFICATION_REGISTRY] Count updated', {
|
|
userId,
|
|
source,
|
|
count,
|
|
totalUnread: currentCount.unread,
|
|
previousCount: previousSourceCount,
|
|
});
|
|
} catch (error) {
|
|
logger.error('[NOTIFICATION_REGISTRY] Error updating cache', { error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère le count agrégé (appelé par le badge)
|
|
*/
|
|
async getCount(userId: string): Promise<NotificationCount> {
|
|
const redis = getRedisClient();
|
|
const cacheKey = NotificationRegistry.COUNT_CACHE_KEY(userId);
|
|
|
|
try {
|
|
const cached = await redis.get(cacheKey);
|
|
if (cached) {
|
|
return JSON.parse(cached);
|
|
}
|
|
} catch (error) {
|
|
logger.error('[NOTIFICATION_REGISTRY] Error reading cache', { error });
|
|
}
|
|
|
|
// Si pas de cache, retourner count vide
|
|
return {
|
|
total: 0,
|
|
unread: 0,
|
|
sources: {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Récupère les notifications (items) de toutes les sources
|
|
*/
|
|
async getNotifications(userId: string, limit: number = 20): Promise<any[]> {
|
|
// Les widgets stockent leurs items dans Redis avec une clé spécifique
|
|
const redis = getRedisClient();
|
|
const sources = ['email', 'rocketchat', 'leantime', 'calendar'];
|
|
const allItems: any[] = [];
|
|
|
|
for (const source of sources) {
|
|
try {
|
|
const itemsKey = `notifications:items:${userId}:${source}`;
|
|
const items = await redis.get(itemsKey);
|
|
if (items) {
|
|
const parsed = JSON.parse(items);
|
|
allItems.push(...parsed);
|
|
}
|
|
} catch (error) {
|
|
logger.error('[NOTIFICATION_REGISTRY] Error reading items', { source, error });
|
|
}
|
|
}
|
|
|
|
// Trier par timestamp (plus récent en premier)
|
|
allItems.sort((a, b) =>
|
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
);
|
|
|
|
return allItems.slice(0, limit);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Nouvelle API Route : `/api/notifications/update`
|
|
|
|
**Fichier :** `app/api/notifications/update/route.ts`
|
|
|
|
```typescript
|
|
import { NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
import { NotificationRegistry } from '@/lib/services/notifications/notification-registry';
|
|
import { getRedisClient } from '@/lib/redis';
|
|
import { logger } from '@/lib/logger';
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
}
|
|
|
|
const { source, count, items } = await request.json();
|
|
|
|
if (!source || typeof count !== 'number') {
|
|
return NextResponse.json(
|
|
{ error: "Invalid request: source and count required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const registry = NotificationRegistry.getInstance();
|
|
|
|
// Enregistrer le count
|
|
await registry.recordCount(session.user.id, source, count, items);
|
|
|
|
// Si des items sont fournis, les stocker aussi
|
|
if (items && Array.isArray(items)) {
|
|
const redis = getRedisClient();
|
|
const itemsKey = `notifications:items:${session.user.id}:${source}`;
|
|
|
|
// Stocker les items (limiter à 50 par source)
|
|
await redis.set(
|
|
itemsKey,
|
|
JSON.stringify(items.slice(0, 50)),
|
|
'EX',
|
|
300 // 5 minutes TTL
|
|
);
|
|
}
|
|
|
|
logger.debug('[NOTIFICATIONS_UPDATE] Count updated', {
|
|
userId: session.user.id,
|
|
source,
|
|
count,
|
|
itemsCount: items?.length || 0,
|
|
});
|
|
|
|
return NextResponse.json({ success: true });
|
|
} catch (error: any) {
|
|
logger.error('[NOTIFICATIONS_UPDATE] Error', { error: error.message });
|
|
return NextResponse.json(
|
|
{ error: "Internal server error", message: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Modification des Widgets
|
|
|
|
#### Widget Courrier
|
|
|
|
```typescript
|
|
// components/email.tsx
|
|
import { useWidgetNotification } from '@/hooks/use-widget-notification';
|
|
|
|
export function Email() {
|
|
const { triggerNotification } = useWidgetNotification();
|
|
const [unreadCount, setUnreadCount] = useState<number>(0);
|
|
const [emails, setEmails] = useState<Email[]>([]);
|
|
const lastUnreadCountRef = useRef<number>(-1);
|
|
|
|
const fetchEmails = async (forceRefresh = false) => {
|
|
// ... fetch emails ...
|
|
|
|
// Calculer le unread count
|
|
const currentUnreadCount = emails.filter(e => !e.read).length;
|
|
|
|
// Si le count a changé, déclencher notification
|
|
if (currentUnreadCount !== lastUnreadCountRef.current) {
|
|
lastUnreadCountRef.current = currentUnreadCount;
|
|
|
|
// Préparer les items pour les notifications
|
|
const notificationItems = emails
|
|
.filter(e => !e.read)
|
|
.slice(0, 10)
|
|
.map(email => ({
|
|
id: email.id,
|
|
title: email.subject,
|
|
message: `De ${email.fromName || email.from}`,
|
|
link: `/courrier`,
|
|
timestamp: new Date(email.date),
|
|
}));
|
|
|
|
// Déclencher notification
|
|
await triggerNotification({
|
|
source: 'email',
|
|
count: currentUnreadCount,
|
|
items: notificationItems,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Widget Parole
|
|
|
|
```typescript
|
|
// components/parole.tsx
|
|
import { useWidgetNotification } from '@/hooks/use-widget-notification';
|
|
|
|
export function Parole() {
|
|
const { triggerNotification } = useWidgetNotification();
|
|
const [unreadCount, setUnreadCount] = useState<number>(0);
|
|
const lastUnreadCountRef = useRef<number>(-1);
|
|
|
|
const fetchMessages = async (forceRefresh = false) => {
|
|
// ... fetch messages ...
|
|
|
|
const currentUnreadCount = data.totalUnreadCount || 0;
|
|
|
|
// Si le count a changé, déclencher notification
|
|
if (currentUnreadCount !== lastUnreadCountRef.current) {
|
|
lastUnreadCountRef.current = currentUnreadCount;
|
|
|
|
// Préparer les items pour les notifications
|
|
const notificationItems = messages
|
|
.slice(0, 10)
|
|
.map(msg => ({
|
|
id: msg.id,
|
|
title: msg.sender.name,
|
|
message: msg.text,
|
|
link: `/parole`,
|
|
timestamp: new Date(msg.rawTimestamp),
|
|
}));
|
|
|
|
await triggerNotification({
|
|
source: 'rocketchat',
|
|
count: currentUnreadCount,
|
|
items: notificationItems,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Widget Devoirs
|
|
|
|
```typescript
|
|
// components/flow.tsx
|
|
import { useWidgetNotification } from '@/hooks/use-widget-notification';
|
|
|
|
export function Duties() {
|
|
const { triggerNotification } = useWidgetNotification();
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const lastTaskCountRef = useRef<number>(-1);
|
|
|
|
const fetchTasks = async (forceRefresh = false) => {
|
|
// ... fetch tasks ...
|
|
|
|
const currentTaskCount = filteredTasks.length;
|
|
|
|
// Si le count a changé, déclencher notification
|
|
if (currentTaskCount !== lastTaskCountRef.current) {
|
|
lastTaskCountRef.current = currentTaskCount;
|
|
|
|
// Préparer les items pour les notifications
|
|
const notificationItems = filteredTasks
|
|
.slice(0, 10)
|
|
.map(task => ({
|
|
id: task.id.toString(),
|
|
title: task.headline,
|
|
message: `Due: ${formatDate(task.dateToFinish)}`,
|
|
link: task.source === 'twenty-crm'
|
|
? (task as any).url
|
|
: `https://agilite.slm-lab.net/tickets/showTicket/${task.id}`,
|
|
timestamp: new Date(task.dateToFinish || Date.now()),
|
|
}));
|
|
|
|
await triggerNotification({
|
|
source: 'leantime',
|
|
count: currentTaskCount,
|
|
items: notificationItems,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
#### Widget Agenda
|
|
|
|
```typescript
|
|
// components/calendar/calendar-widget.tsx
|
|
import { useWidgetNotification } from '@/hooks/use-widget-notification';
|
|
|
|
export function CalendarWidget() {
|
|
const { triggerNotification } = useWidgetNotification();
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const lastEventCountRef = useRef<number>(-1);
|
|
|
|
const fetchUpcomingEvents = async () => {
|
|
// ... fetch events ...
|
|
|
|
// Filtrer les événements à venir (aujourd'hui et demain)
|
|
const now = new Date();
|
|
const tomorrow = addDays(now, 1);
|
|
const upcomingEvents = allEvents
|
|
.filter(event => event.start >= now && event.start <= tomorrow)
|
|
.slice(0, 10);
|
|
|
|
const currentEventCount = upcomingEvents.length;
|
|
|
|
// Si le count a changé, déclencher notification
|
|
if (currentEventCount !== lastEventCountRef.current) {
|
|
lastEventCountRef.current = currentEventCount;
|
|
|
|
// Préparer les items pour les notifications
|
|
const notificationItems = upcomingEvents.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
message: `Le ${format(event.start, 'dd/MM à HH:mm')}`,
|
|
link: `/agenda`,
|
|
timestamp: event.start,
|
|
}));
|
|
|
|
await triggerNotification({
|
|
source: 'calendar',
|
|
count: currentEventCount,
|
|
items: notificationItems,
|
|
});
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
### 5. Simplification de `/api/notifications/count`
|
|
|
|
**Fichier :** `app/api/notifications/count/route.ts`
|
|
|
|
```typescript
|
|
import { NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
import { NotificationRegistry } from '@/lib/services/notifications/notification-registry';
|
|
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
}
|
|
|
|
const registry = NotificationRegistry.getInstance();
|
|
const counts = await registry.getCount(session.user.id);
|
|
|
|
return NextResponse.json(counts);
|
|
} catch (error: any) {
|
|
return NextResponse.json(
|
|
{ error: "Internal server error", message: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Simplification de `/api/notifications`
|
|
|
|
**Fichier :** `app/api/notifications/route.ts`
|
|
|
|
```typescript
|
|
import { NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
import { NotificationRegistry } from '@/lib/services/notifications/notification-registry';
|
|
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
|
}
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const limit = parseInt(searchParams.get('limit') || '20', 10);
|
|
|
|
const registry = NotificationRegistry.getInstance();
|
|
const notifications = await registry.getNotifications(session.user.id, limit);
|
|
|
|
return NextResponse.json({
|
|
notifications,
|
|
total: notifications.length,
|
|
});
|
|
} catch (error: any) {
|
|
return NextResponse.json(
|
|
{ error: "Internal server error", message: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 Comparaison : Avant vs Après
|
|
|
|
### Avant ❌
|
|
|
|
**Code** :
|
|
- `LeantimeAdapter` : ~550 lignes
|
|
- `RocketChatAdapter` : ~540 lignes
|
|
- `EmailAdapter` : ~410 lignes
|
|
- `NotificationService` : ~425 lignes
|
|
- **Total** : ~1925 lignes
|
|
|
|
**Appels API** :
|
|
- Polling notifications : 1 toutes les 30s
|
|
- Polling Leantime : 1 toutes les 30s (via adapter)
|
|
- Polling RocketChat : 1 toutes les 30s (via adapter)
|
|
- Polling Email : 1 toutes les 30s (via adapter)
|
|
- **Total** : 4 appels toutes les 30s
|
|
|
|
**Complexité** :
|
|
- 3 adapters à maintenir
|
|
- Logique de fetch dupliquée
|
|
- Risque de désynchronisation
|
|
|
|
### Après ✅
|
|
|
|
**Code** :
|
|
- `NotificationRegistry` : ~150 lignes
|
|
- `useWidgetNotification` : ~50 lignes
|
|
- Modifications widgets : ~20 lignes chacun
|
|
- **Total** : ~280 lignes (-85% de code)
|
|
|
|
**Appels API** :
|
|
- Widgets pollent déjà (pas de changement)
|
|
- Notifications : Event-driven uniquement
|
|
- **Total** : 0 appels supplémentaires (réduction de 100%)
|
|
|
|
**Complexité** :
|
|
- 1 registry simple
|
|
- Pas de duplication
|
|
- Source unique de vérité
|
|
|
|
---
|
|
|
|
## 🎯 Avantages Spécifiques
|
|
|
|
### 1. **Simplicité**
|
|
- ✅ **-85% de code** : De ~1925 lignes à ~280 lignes
|
|
- ✅ **Pas d'adapters complexes** : Suppression de LeantimeAdapter, RocketChatAdapter, EmailAdapter
|
|
- ✅ **Architecture claire** : Widgets → Registry → Badge
|
|
|
|
### 2. **Performance**
|
|
- ✅ **0 appels API supplémentaires** : Les widgets font déjà le travail
|
|
- ✅ **Event-driven** : Notifications uniquement quand nécessaire
|
|
- ✅ **Cache optimisé** : Un seul cache au lieu de 4
|
|
|
|
### 3. **Maintenance**
|
|
- ✅ **Un seul endroit à modifier** : Les widgets
|
|
- ✅ **Pas de désynchronisation** : Impossible d'avoir des notifications sans widget
|
|
- ✅ **Facile à étendre** : Ajouter un widget = ajouter un trigger
|
|
|
|
### 4. **Cohérence**
|
|
- ✅ **Source unique de vérité** : Les widgets sont la source
|
|
- ✅ **Notifications = Widgets** : Si le widget voit, la notification voit
|
|
- ✅ **UX cohérente** : Mêmes données partout
|
|
|
|
---
|
|
|
|
## 🚨 Points d'Attention
|
|
|
|
### 1. **Initialisation**
|
|
- Au premier chargement, les widgets doivent déclencher les notifications
|
|
- Le badge doit attendre que les widgets soient chargés
|
|
|
|
**Solution** :
|
|
- Les widgets déclenchent au mount
|
|
- Le badge affiche 0 si pas encore initialisé
|
|
|
|
### 2. **Widgets non chargés**
|
|
- Si un widget n'est pas sur la page, pas de notifications pour cette source
|
|
|
|
**Solution** :
|
|
- C'est acceptable : si le widget n'est pas visible, pas besoin de notification
|
|
- Les notifications reflètent ce que l'utilisateur voit
|
|
|
|
### 3. **Marquer comme lu**
|
|
- Comment marquer une notification comme lue si elle vient d'un widget ?
|
|
|
|
**Solution** :
|
|
- Quand l'utilisateur clique sur une notification, ouvrir le widget
|
|
- Le widget met à jour son état (email lu, message lu, etc.)
|
|
- Le widget redéclenche la notification avec le nouveau count
|
|
- Le badge se met à jour automatiquement
|
|
|
|
### 4. **Historique**
|
|
- Comment garder un historique des notifications si elles viennent des widgets ?
|
|
|
|
**Solution** :
|
|
- Stocker les items dans Redis (déjà prévu dans le code)
|
|
- TTL de 5 minutes pour les items
|
|
- Le dropdown affiche les items stockés
|
|
|
|
---
|
|
|
|
## 📝 Plan de Migration
|
|
|
|
### Phase 1 : Créer la nouvelle infrastructure
|
|
1. ✅ Créer `useWidgetNotification` hook
|
|
2. ✅ Créer `NotificationRegistry` service
|
|
3. ✅ Créer `/api/notifications/update` endpoint
|
|
4. ✅ Simplifier `/api/notifications/count`
|
|
5. ✅ Simplifier `/api/notifications`
|
|
|
|
### Phase 2 : Intégrer dans les widgets
|
|
6. ✅ Modifier Widget Courrier
|
|
7. ✅ Modifier Widget Parole
|
|
8. ✅ Modifier Widget Devoirs
|
|
9. ✅ Modifier Widget Agenda
|
|
|
|
### Phase 3 : Nettoyer l'ancien code
|
|
10. ❌ Supprimer `LeantimeAdapter`
|
|
11. ❌ Supprimer `RocketChatAdapter`
|
|
12. ❌ Supprimer `EmailAdapter`
|
|
13. ❌ Simplifier `NotificationService` (ou le supprimer)
|
|
|
|
### Phase 4 : Tests et validation
|
|
14. ✅ Tester chaque widget
|
|
15. ✅ Tester le badge
|
|
16. ✅ Tester le dropdown
|
|
17. ✅ Vérifier les performances
|
|
|
|
---
|
|
|
|
## ✅ Conclusion
|
|
|
|
Cette architecture simplifiée est **beaucoup plus maintenable** et **performante** que l'actuelle. Elle élimine la duplication de code, réduit les appels API, et garantit la cohérence entre widgets et notifications.
|
|
|
|
**Recommandation** : Implémenter cette architecture pour simplifier significativement le système de notifications.
|