Collaboration
Collaboration
Périmètre : commentaires sur entités (modèle Comment), fils (réponse de
profondeur 1 via parentCommentId), mentions @user (stockées dans
Comment.mentionedUserIds), notes internes (modèle InternalNote distinct).
Le composant CommentThread est réutilisé sur les fiches métier (lease affair,
facture, maintenance…). Source de cette page : docs/14-domain-collab.md.
/api/workspace/threads/* :
POST /api/workspace/threads/comments, POST /api/workspace/threads/internal-notes,
GET /api/workspace/threads/[entity]/[entityId]. Permissions collaboration:read
et collaboration:write.CommentReaction ni de route), épinglage (pinnedAt), watch d'entité
(EntityWatch), inbox (/workspace/inbox), suppression soft (deletedAt)
et attachements. Le schéma actuel se limite à CommentThread, Comment et
InternalNote. Les sections concernées sont signalées ci-dessous.Vocabulaire & entités
| Terme | Entité / Champ | Définition | Statut |
|---|---|---|---|
| Fil | CommentThread | Pivot unique par entité (@@unique(organizationId, entity, entityId)). | implémenté |
| Commentaire | Comment | Message texte rattaché à un CommentThread (visible par tous les membres). | implémenté |
| Réponse | Comment.parentCommentId | Réponse de profondeur 1 sous un commentaire racine (null = racine). | implémenté |
| Mention | Comment.mentionedUserIds | Tableau d'IDs (String[]) parsés à la création — pas de table dédiée. | implémenté |
| Note interne | InternalNote | Modèle distinct de Comment, jamais exposé aux portails externes. | implémenté |
| Réaction | CommentReaction | Emoji posé sur un commentaire. | cible (modèle absent) |
| Épinglage | Comment.pinnedAt | Un commentaire épinglé au sommet du fil. | cible (champ absent) |
| Watch | EntityWatch | Abonnement aux nouveaux commentaires d'une entité. | cible (modèle absent) |
| Inbox | — | Vue agrégée /workspace/inbox. | cible (écran/route absents) |
Schéma Prisma détaillé dans
docs/04-data-model.md§11. Les notes internes ne sont pas un flagisInternalsurComment: ce sont deux modèles séparés (Commentpublic,InternalNoteinterne).
Écrans
| Écran | Route / Composant | Contenu | Statut |
|---|---|---|---|
Composant CommentThread | Intégré dans les fiches | Liste chronologique, composer, fil de réponses, onglet notes internes. | implémenté |
| Inbox | /workspace/inbox | Commentaires me mentionnant + fils suivis. | cible (non implémenté) |
Composant CommentThread — détail
Liste
Chronologique. Sépare les commentaires (Comment) et les notes internes
(InternalNote) renvoyés par GET /api/workspace/threads/[entity]/[entityId].
Composer
Saisie texte (max 5 000 caractères). Mention @... parsée à la soumission
(alimente Comment.mentionedUserIds).
Actions par commentaire
Réponse (parentCommentId, profondeur 1). Cible : réactions emoji,
édition, suppression soft, épinglage (non implémentés).
Modèle de données (points clés)
CommentThreadporte la clé d'entité :entity(string, ex.LeaseAffair,Invoice,MaintenanceOperation) +entityId, unique par(organizationId, entity, entityId). C'est le pivot, pasComment.Commentest rattaché à unCommentThreadviathreadId; champs :authorUserId,body(@db.Text),mentionedUserIds(String[]),parentCommentId(nullable).Comment.parentCommentIdnullable :NULL= racine, non NULL = réponse (profondeur 1, relationCommentReplies).InternalNoteest un modèle distinct rattaché au mêmeCommentThread; jamais renvoyé aux portails externes. Le champisInternaly vaut toujourstrue(explicite, pour la lisibilité des requêtes).
Comment.deletedAt (suppression soft), ni Comment.pinnedAt (épinglage), ni table
d'attachements. Les attachements via FileLink restent une cible.API
Permissions vérifiées directement dans chaque handler.
Routes implémentées
| Méthode | Route | Permission |
|---|---|---|
GET | /api/workspace/threads/[entity]/[entityId] | collaboration:read |
POST | /api/workspace/threads/comments | collaboration:write |
POST | /api/workspace/threads/internal-notes | collaboration:write |
Routes cible (non implémentées)
/api/workspace/comments
ni de permissions comment:* — les seules permissions du domaine sont
collaboration:read et collaboration:write (une permission de modération dédiée
reste à créer).| Méthode | Route cible | Permission cible |
|---|---|---|
PATCH / DELETE | /api/workspace/threads/comments/[id] | collaboration:write (+ modération à définir) |
POST / DELETE | /api/workspace/threads/comments/[id]/reactions | collaboration:write |
POST | /api/workspace/threads/comments/[id]/pin | modération à définir |
GET | /api/workspace/inbox | (authentifié) |
/api/workspace/threads/commentsAuth Crée un commentaire sur le fil d'une entité (le CommentThread est créé à la volée
si absent). Si le texte contient des mentions @userId, elles sont parsées et
stockées dans Comment.mentionedUserIds, et les notifications correspondantes
sont émises. Permission collaboration:write.
Corps (JSON)
LeaseAffair, Invoice, MaintenanceOperation…).Requête
curl -s -X POST "$API/api/workspace/threads/comments" \
-H "Content-Type: application/json" -H "Cookie: $SESSION" \
-d '{"entity":"LeaseAffair","entityId":"aff_…","body":"@alice peux-tu valider ?"}'
Réponse
{
"id": "cmt_…",
"authorUserId": "usr_…",
"authorName": "Bob Martin",
"body": "@alice peux-tu valider ?",
"mentionedUserIds": ["usr_alice"],
"parentCommentId": null,
"createdAt": "2026-06-16T10:00:00Z"
}
POST /api/workspace/threads/internal-notes
(corps entity, entityId, body ; pas de parentCommentId). C'est un modèle
distinct, ce n'est pas un flag sur le commentaire.Workflows
Mention @email
Saisie @email dans le body
→ À la soumission : parseMentions() résout les emails en userIds de l'org
→ Comment.mentionedUserIds = [userId, …] (pas de table de jointure)
→ Notification kind=MENTION (in-app + email selon préférences) aux mentionnés ≠ auteur
Réponse à un commentaire
User B répond à un commentaire racine de User A (B ≠ A)
→ Comment créé avec parentCommentId = comment racine de A
→ Notification kind=COMMENT_REPLY à A
Note interne (modèle distinct)
POST /api/workspace/threads/internal-notes { entity, entityId, body }
→ InternalNote créée sur le CommentThread de l'entité
→ Jamais renvoyée aux portails externes (modèle séparé de Comment)
Watch d'entité
EntityWatch ni de geste
« Suivre » dans le code. Les notifications partent aujourd'hui uniquement sur
mention et réponse.User clique "Suivre" sur une entité
→ INSERT EntityWatch (userId, entity, entityId)
→ Tout nouveau commentaire → notification in-app au watcher
Notifications
| Événement | Canal | Destinataire | Statut |
|---|---|---|---|
Mention dans un commentaire (kind=MENTION) | In-app + email | Utilisateur mentionné | implémenté |
Réponse à mon commentaire (kind=COMMENT_REPLY) | In-app | Auteur du commentaire racine | implémenté |
| Nouveau commentaire sur entité suivie | In-app | Watchers de l'entité | cible (pas de watch) |
Règles métier
- Isolation notes internes :
InternalNoteest un modèle distinct, jamais renvoyé par les portails externes (séparation au niveau du modèle, pas un flag). - Mentions : parsées par e-mail à la soumission ; seuls les utilisateurs de l'organisation sont résolus ; l'auteur ne se notifie pas lui-même.
- Réponses : profondeur 1 — une réponse cible un commentaire racine
(
parentCommentId: nullrequis côté parent).
deletedAt + « Commentaire supprimé »), épinglage (1 max par fil), réactions
emoji et attachements (FileLink) ne sont pas dans le code à ce stade.Fichiers
Référence technique du domaine Fichiers & GED — upload S3, URL pré-signées, Object Lock Compliance, versioning, partages temporaires et OCR.
Notifications
Référence technique du domaine Notifications — in-app, email transactionnel, SMS, templates, préférences utilisateur et queues persistantes.