feat(#195): SSE realtime Cartes (stream + emit audience)
GET /api/v1/cards/stream (Bearer ou ?access_token=). Events card.created|updated|deleted, response.added + heartbeat. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
MajCarteDto,
|
||||
RepondreCarteDto,
|
||||
} from './dto/cards.dto';
|
||||
import { CardsRealtimeService } from './cards-realtime.service';
|
||||
|
||||
@Injectable()
|
||||
export class CardsService {
|
||||
@@ -49,6 +50,7 @@ export class CardsService {
|
||||
@InjectRepository(ParentsChildren)
|
||||
private readonly parentsChildrenRepo: Repository<ParentsChildren>,
|
||||
private readonly absencesService: AbsencesGardeService,
|
||||
private readonly realtime: CardsRealtimeService,
|
||||
) {}
|
||||
|
||||
async listerTypes(role: RoleType): Promise<CardType[]> {
|
||||
@@ -164,7 +166,9 @@ export class CardsService {
|
||||
await this.buildAudience(saved, type, placement, userId, role);
|
||||
|
||||
const full = await this.loadCard(saved.id);
|
||||
return this.toDto(full, userId);
|
||||
const dtoOut = this.toDto(full, userId);
|
||||
this.emitAudience(full, 'card.created', dtoOut);
|
||||
return dtoOut;
|
||||
}
|
||||
|
||||
async repondre(
|
||||
@@ -228,7 +232,14 @@ export class CardsService {
|
||||
|
||||
card.purge_at = this.purgeAt(card.type.retention_days, card.statut);
|
||||
await this.cardsRepo.save(card);
|
||||
return this.toDto(await this.loadCard(cardId), userId);
|
||||
const full = await this.loadCard(cardId);
|
||||
const dtoOut = this.toDto(full, userId);
|
||||
this.emitAudience(full, 'response.added', {
|
||||
action: dto.action,
|
||||
card: dtoOut,
|
||||
});
|
||||
this.emitAudience(full, 'card.updated', dtoOut);
|
||||
return dtoOut;
|
||||
}
|
||||
|
||||
async majEnAttente(
|
||||
@@ -273,7 +284,10 @@ export class CardsService {
|
||||
card.statut = CardInstanceStatutType.OUVERTE;
|
||||
card.purge_at = this.purgeAt(card.type.retention_days, card.statut);
|
||||
await this.cardsRepo.save(card);
|
||||
return this.toDto(await this.loadCard(cardId), userId);
|
||||
const full = await this.loadCard(cardId);
|
||||
const dtoOut = this.toDto(full, userId);
|
||||
this.emitAudience(full, 'card.updated', dtoOut);
|
||||
return dtoOut;
|
||||
}
|
||||
|
||||
async supprimer(
|
||||
@@ -285,6 +299,7 @@ export class CardsService {
|
||||
if (card.cree_par !== userId) {
|
||||
throw new ForbiddenException('Seul le créateur peut supprimer cette carte');
|
||||
}
|
||||
const audienceIds = await this.audienceUserIds(card);
|
||||
if (card.id_absence) {
|
||||
const absStatut =
|
||||
card.statut === CardInstanceStatutType.TRAITEE
|
||||
@@ -300,6 +315,27 @@ export class CardsService {
|
||||
void absStatut;
|
||||
}
|
||||
await this.cardsRepo.delete({ id: cardId });
|
||||
this.realtime.emitToUsers(audienceIds, 'card.deleted', cardId, {
|
||||
id: cardId,
|
||||
});
|
||||
}
|
||||
|
||||
private emitAudience(
|
||||
card: CardInstance,
|
||||
event: 'card.created' | 'card.updated' | 'response.added',
|
||||
data: unknown,
|
||||
): void {
|
||||
const ids = (card.audience ?? []).map((a) => a.id_utilisateur);
|
||||
if (ids.length === 0) return;
|
||||
this.realtime.emitToUsers(ids, event, card.id, data);
|
||||
}
|
||||
|
||||
private async audienceUserIds(card: CardInstance): Promise<string[]> {
|
||||
if (card.audience?.length) {
|
||||
return card.audience.map((a) => a.id_utilisateur);
|
||||
}
|
||||
const rows = await this.audienceRepo.find({ where: { id_card: card.id } });
|
||||
return rows.map((r) => r.id_utilisateur);
|
||||
}
|
||||
|
||||
private mapAbsenceType(typeCode: string): TypeAbsenceGardeType {
|
||||
|
||||
Reference in New Issue
Block a user