Appearance
Module Rappels — Documentation technique
Epic E5 — Rappels & tâches
User stories : US-5.1, US-5.2, US-5.3, US-5.4
1. Responsabilités du module
Le module reminders permet à François de poser des rappels sur ses contacts, ses entreprises, ou de façon autonome. L'objectif est de répondre à « qui dois-je relancer cette semaine ? » sans dépendre d'un outil externe.
Ce que le module fait :
- Créer un rappel lié à un contact, à une entreprise, ou sans entité (autonome)
- Vue globale groupée par échéance : en retard, aujourd'hui, à venir
- Marquer un rappel comme terminé (avec
doneAt) et le rouvrir - Modifier et supprimer un rappel existant
- Intégration dans la fiche contact (panel
ContactRemindersPanel)
Ce que le module ne fait pas :
- Notifications push ou email automatiques — manuel uniquement en v1
- Rappels récurrents
- Assignation à un autre utilisateur
2. Schéma de données
prisma
model Reminder {
id String @id @default(cuid())
enterpriseId Int
ownerUserId String
contactId String?
companyId String?
title String
dueAt DateTime
doneAt DateTime?
description String?
createdAt DateTime @default(now())
contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull)
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
@@index([enterpriseId, ownerUserId, dueAt])
@@index([contactId])
@@index([companyId])
}Sémantique des champs clés
| Champ | Requis | Sémantique |
|---|---|---|
title | ✅ | Libellé affiché partout |
dueAt | ✅ | Date d'échéance — sert au groupement temporel |
doneAt | — | null = en attente ; Date = marqué comme terminé à cet instant |
contactId | — | Référence optionnelle — ON DELETE SetNull |
companyId | — | Référence optionnelle — ON DELETE SetNull |
Relations
Reminder → Contact: N:1, nullable,ON DELETE SetNull— supprimer un contact détache ses rappels (ils deviennent autonomes).Reminder → Company: N:1, nullable,ON DELETE SetNull— même comportement.Reminderest dansSCOPED_MODELSdePrismaService: il porteenterpriseIdetownerUserId, filtrés automatiquement par le middleware tenant.
3. API REST
Base URL : /api/reminders
| Méthode | Route | Description | Auth |
|---|---|---|---|
GET | /reminders | Récupère les rappels groupés (200) | ✅ |
POST | /reminders | Crée un rappel (201) | ✅ |
PATCH | /reminders/:id | Modifie un rappel (200) | ✅ |
DELETE | /reminders/:id | Supprime un rappel (204) | ✅ |
Paramètre GET /reminders
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
includeDone | boolean | false | Inclure le groupe done dans la réponse |
Réponse GET /reminders
json
{
"overdue": [...],
"today": [...],
"upcoming": [...],
"done": [...]
}Chaque entrée inclut les relations contact et company avec id + champs d'affichage.
Corps POST /reminders
json
{
"title": "Relancer Sophie pour démo T3",
"dueAt": "2024-07-01T10:00:00Z",
"description": "Mentionner le pricing Q3",
"contactId": "cjld2cjxh0000qzrmn831i7rn",
"companyId": null
}contactId et companyId sont mutuellement indépendants et tous les deux optionnels. Un rappel avec les deux à null est valide (rappel autonome).
Corps PATCH /reminders/:id
Tous les champs sont optionnels. Le champ done est un raccourci pour basculer doneAt :
json
{
"title": "Titre corrigé",
"dueAt": "2024-07-05T09:00:00Z",
"description": "Détails mis à jour",
"done": true
}done: true→doneAt = new Date()(timestamp serveur)done: false→doneAt = null(réouverture)
Codes d'erreur
| Code | Situation |
|---|---|
400 | Champ requis manquant ou invalide (title vide, dueAt non ISO) |
401 | Token absent ou invalide |
403 | contactId ou companyId inexistant ou hors scope tenant |
404 | Rappel introuvable (PATCH/DELETE sur un ID inconnu) |
4. Backend — Architecture
RemindersService
Fichier : apps/api/src/reminders/reminders.service.ts
list(ctx, includeDone)
- Calcule
todayStartettomorrowStarten temps local (new Date(y, m, d)). prisma.reminder.findMany()— avecwhere: { doneAt: null }siincludeDone=false.- Groupe en JavaScript par comparaison de
dueAtaux frontières de minuit :overdue:!doneAt && dueAt < todayStarttoday:!doneAt && dueAt >= todayStart && dueAt < tomorrowStartupcoming:!doneAt && dueAt >= tomorrowStartdone:!!doneAt(retourné seulement siincludeDone=true)
create(dto, ctx)
- Si
dto.contactIdest fourni :assertContactAccess(contactId)— vérifie l'existence dans le scope tenant (viaprisma.contact.findUniquescopé par middleware). LèveForbiddenExceptionsi null. - Si
dto.companyIdest fourni :assertCompanyAccess(companyId)— idem viaprisma.company.findFirst. LèveForbiddenExceptionsi null. prisma.reminder.create()— injecteenterpriseIdetownerUserIddepuisctx.
update(id, dto)
prisma.reminder.findFirst({ where: { id } })— scopé tenant via middleware. LèveNotFoundExceptionsi null.prisma.reminder.update()avec spread conditionnel : un champ absent du dto n'écrase pas la valeur existante.dto.done === true→doneAt: new Date()dto.done === false→doneAt: null
remove(id)
prisma.reminder.findFirst({ where: { id } })— lèveNotFoundExceptionsi null.prisma.reminder.delete({ where: { id } }).
Méthodes privées
| Méthode | Rôle |
|---|---|
assertContactAccess(contactId) | findUnique Contact (scopé tenant). Lève 403 si hors scope. |
assertCompanyAccess(companyId) | findFirst Company (scopé tenant). Lève 403 si hors scope. |
DTOs
| DTO | Fichier | Champs requis |
|---|---|---|
CreateReminderDto | reminders/dto/create-reminder.dto.ts | title, dueAt |
UpdateReminderDto | reminders/dto/update-reminder.dto.ts | aucun (tous optionnels) |
Schémas Zod miroirs dans packages/shared/src/schemas/reminder.schema.ts.
5. Frontend — Architecture
Composants
| Composant | Rôle | User story |
|---|---|---|
pages/reminders/index.vue | Page globale : groupes, toggle "Afficher terminés", nouveau rappel | US-5.3 |
components/reminder/ReminderGroup.vue | Section d'un groupe (En retard / Aujourd'hui / À venir / Terminés) | US-5.3 |
components/reminder/ReminderItem.vue | Une ligne de rappel : checkbox, titre, date, liens, kebab | US-5.4 |
components/reminder/ReminderFormModal.vue | Modale création/édition (titre, date, description, liaison) | US-5.1/5.2 |
components/reminder/ReminderActionsMenu.vue | Menu kebab (Modifier / Supprimer) | US-5.1/5.4 |
components/contact/ContactRemindersPanel.vue | Bloc rappels actifs dans la fiche contact | US-5.1 |
pages/reminders/index.vue
REMINDER_GROUPS— config statique des 4 groupes (key, title, variant, icon).visibleGroups— computed : filtre les groupes à items non vides pour ne pas afficher de sections vides.isEmpty— affiche l'état vide « Tout est sous contrôle ! 🎉 » si tous les groupes sont vides.summary—"X rappels à traiter · Y en retard"dans le sous-titre de page.includeDone(ref booléen) — piloté par la checkbox, passé àuseReminders().
ReminderGroup
- Prop
variant:'danger'(rouge) pour "En retard",'default','done'(atténué). - Propage les événements
edit,delete,toggle-donevers la page parente.
ReminderItem
isDone: computed depuisreminder.doneAt !== null.- Classe
.reminder-item--doneappliquée siisDone→ opacité 0.6 + titre barré (CSStext-decoration: line-through). .done-checkbox: bouton cliquable (pas<input type="checkbox">) pour émettretoggle-done.links: computed calculant les liens vers contact et/ou entreprise (icône + texte).- Affiche "Sans contact lié" si
linksest vide.
ReminderFormModal
- Props
prefillContactId/prefillCompanyId: préremplissage depuis la fiche contact ou entreprise. linkTyperef :'none'|'contact'|'company'— piloté par les boutons radio.toDateTimeLocal(): convertit un ISO string en formatYYYY-MM-DDTHH:mmpour l'inputdatetime-local.- Validation client avant appel API : titre requis, date requise, entité requise si linkType non
'none'.
Composable useReminders
Fichier : apps/web/composables/useReminders.ts
ts
useReminders(includeDone: Ref<boolean>) // GET /reminders?includeDone=...
useCreateReminder() // POST /reminders — invalide ['reminders']
useUpdateReminder() // PATCH /reminders/:id — invalide ['reminders']
useDeleteReminder() // DELETE /reminders/:id — invalide ['reminders']Composable useReminderDate
Fichier : apps/web/composables/useReminderDate.ts
formatReminderDate(dueAt, isOverdue) — formate la date en français :
- Overdue à J-1 :
"Hier — en retard" - Overdue à J-n :
"Il y a N jours — en retard" - Aujourd'hui : heure en
HH:mm - À venir : date longue (
"lundi 15 mai")
Pages concernées
| Page | Intégration E5 |
|---|---|
pages/reminders/index.vue | Page dédiée aux rappels avec groupes et formulaire |
pages/contacts/[id].vue | Inclut <ContactRemindersPanel> avec les rappels actifs du contact |
pages/companies/[id].vue | Bouton "Rappel" dans la fiche entreprise (préremplissage companyId) |
6. Multi-tenancy
Reminder est dans SCOPED_MODELS de PrismaService — il porte enterpriseId et ownerUserId. Le middleware Prisma injecte automatiquement les filtres tenant sur toutes les opérations CRUD.
requête CRUD reminder
│
▼
PrismaService.tenantMiddleware
│
├── create/createMany → injecte enterpriseId + ownerUserId depuis AsyncLocalStorage
│
└── find*/update*/delete* → ajoute { enterpriseId, ownerUserId? } au where
└── admin : filtre uniquement sur enterpriseId
└── membre : filtre sur enterpriseId ET ownerUserIdLa vérification d'accès aux entités liées (assertContactAccess, assertCompanyAccess) est réalisée avant la création — si le contact ou l'entreprise est hors scope, le rappel n'est jamais persisté.
7. Tests
Tests unitaires
Fichier : apps/api/src/reminders/reminders.service.spec.ts
Couverture :
list()— groupement aux frontières de minuit (overdue/today/upcoming), frontière exacte tomorrowStart, dernière seconde de "today", rappels faits exclus des groupes actifs,includeDone=true/falsecreate()— injectionenterpriseId/ownerUserIddepuis ctx,ForbiddenExceptioncontactId hors scope,ForbiddenExceptioncompanyId hors scope, création avec entité valideupdate()—NotFoundExceptionID inconnu,done: true→doneAt: Date,done: false→doneAt: null, mise à jour conditionnelle (description absente non écrasée)remove()— suppression réussie,NotFoundExceptionID inconnu
bash
yarn workspace @heriade/api test:unit --testPathPattern="reminders.service"Tests E2E Playwright
Fichier : apps/e2e/tests/reminders.spec.ts
Couverture :
- API CRUD — POST 201/400/401/403, PATCH 200/404 avec toggle
done, DELETE 204/404, GET groupes etincludeDone - US-5.1 — bouton accessible, modale ouvre, validation titre, création avec toast
- US-5.2 — rappel autonome (contactId=null, companyId=null), rappel lié à une entreprise
- US-5.3 — groupes "En retard" et "À venir" visibles, compteurs affichés
- US-5.4 — checkbox visible, toggle done → toast, "Afficher terminés" → groupe Terminés, réouverture → toast "Rappel rouvert.", suppression avec confirmation, modification via kebab
bash
# Nécessite api :3001 et web :3000 démarrés
yarn test:e2e --grep "Reminders"
yarn test:e2e --grep "US-5"8. Écarts avec les user stories
| US | Critère d'acceptation | Implémentation | Statut |
|---|---|---|---|
| US-5.3 | "Si je n'ai aucun rappel : état vide avec 'Tout est sous contrôle 🎉'" | État vide affiché si tous les groupes visibles sont vides | ✅ Conforme |
| US-5.4 | "En cochant : le rappel passe en barré" | Classe CSS --done appliquée + text-decoration: line-through | ✅ Conforme |
| US-5.4 | "Les rappels faits ne s'affichent pas par défaut (toggle)" | includeDone=false par défaut, toggle checkbox dans le header | ✅ Conforme |
| US-5.2 | "Depuis la page Rappels globale : choisir contact OU entreprise OU rien" | Boutons radio dans ReminderFormModal (3 options) | ✅ Conforme |
Point d'attention (non bloquant)
update()ignore un titre vide :...(dto.title && { title: dto.title })— sidto.title = ""est passé, le champ n'est pas mis à jour (pas d'erreur, pas de changement). Comportement acceptable en v1 car la validation client côté formulaire rejette les titres vides avant d'appeler l'API.- Pagination absente :
list()ne pagine pas les rappels. Si un utilisateur accumule plusieurs centaines de rappels terminés avecincludeDone=true, la réponse peut devenir volumineuse. À traiter post-MVP.
Document v1 · mai 2026 · à synchroniser avec les user stories (heriade-contacts-USER-STORIES.md)
