Skip to content

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

ChampRequisSémantique
titleLibellé affiché partout
dueAtDate d'échéance — sert au groupement temporel
doneAtnull = en attente ; Date = marqué comme terminé à cet instant
contactIdRéférence optionnelle — ON DELETE SetNull
companyIdRé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.
  • Reminder est dans SCOPED_MODELS de PrismaService : il porte enterpriseId et ownerUserId, filtrés automatiquement par le middleware tenant.

3. API REST

Base URL : /api/reminders

MéthodeRouteDescriptionAuth
GET/remindersRécupère les rappels groupés (200)
POST/remindersCrée un rappel (201)
PATCH/reminders/:idModifie un rappel (200)
DELETE/reminders/:idSupprime un rappel (204)

Paramètre GET /reminders

ParamètreTypeDéfautDescription
includeDonebooleanfalseInclure 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: truedoneAt = new Date() (timestamp serveur)
  • done: falsedoneAt = null (réouverture)

Codes d'erreur

CodeSituation
400Champ requis manquant ou invalide (title vide, dueAt non ISO)
401Token absent ou invalide
403contactId ou companyId inexistant ou hors scope tenant
404Rappel introuvable (PATCH/DELETE sur un ID inconnu)

4. Backend — Architecture

RemindersService

Fichier : apps/api/src/reminders/reminders.service.ts

list(ctx, includeDone)

  1. Calcule todayStart et tomorrowStart en temps local (new Date(y, m, d)).
  2. prisma.reminder.findMany() — avec where: { doneAt: null } si includeDone=false.
  3. Groupe en JavaScript par comparaison de dueAt aux frontières de minuit :
    • overdue : !doneAt && dueAt < todayStart
    • today : !doneAt && dueAt >= todayStart && dueAt < tomorrowStart
    • upcoming : !doneAt && dueAt >= tomorrowStart
    • done : !!doneAt (retourné seulement si includeDone=true)

create(dto, ctx)

  1. Si dto.contactId est fourni : assertContactAccess(contactId) — vérifie l'existence dans le scope tenant (via prisma.contact.findUnique scopé par middleware). Lève ForbiddenException si null.
  2. Si dto.companyId est fourni : assertCompanyAccess(companyId) — idem via prisma.company.findFirst. Lève ForbiddenException si null.
  3. prisma.reminder.create() — injecte enterpriseId et ownerUserId depuis ctx.

update(id, dto)

  1. prisma.reminder.findFirst({ where: { id } }) — scopé tenant via middleware. Lève NotFoundException si null.
  2. prisma.reminder.update() avec spread conditionnel : un champ absent du dto n'écrase pas la valeur existante.
    • dto.done === truedoneAt: new Date()
    • dto.done === falsedoneAt: null

remove(id)

  1. prisma.reminder.findFirst({ where: { id } }) — lève NotFoundException si null.
  2. prisma.reminder.delete({ where: { id } }).

Méthodes privées

MéthodeRô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

DTOFichierChamps requis
CreateReminderDtoreminders/dto/create-reminder.dto.tstitle, dueAt
UpdateReminderDtoreminders/dto/update-reminder.dto.tsaucun (tous optionnels)

Schémas Zod miroirs dans packages/shared/src/schemas/reminder.schema.ts.


5. Frontend — Architecture

Composants

ComposantRôleUser story
pages/reminders/index.vuePage globale : groupes, toggle "Afficher terminés", nouveau rappelUS-5.3
components/reminder/ReminderGroup.vueSection d'un groupe (En retard / Aujourd'hui / À venir / Terminés)US-5.3
components/reminder/ReminderItem.vueUne ligne de rappel : checkbox, titre, date, liens, kebabUS-5.4
components/reminder/ReminderFormModal.vueModale création/édition (titre, date, description, liaison)US-5.1/5.2
components/reminder/ReminderActionsMenu.vueMenu kebab (Modifier / Supprimer)US-5.1/5.4
components/contact/ContactRemindersPanel.vueBloc rappels actifs dans la fiche contactUS-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-done vers la page parente.

ReminderItem

  • isDone : computed depuis reminder.doneAt !== null.
  • Classe .reminder-item--done appliquée si isDone → opacité 0.6 + titre barré (CSS text-decoration: line-through).
  • .done-checkbox : bouton cliquable (pas <input type="checkbox">) pour émettre toggle-done.
  • links : computed calculant les liens vers contact et/ou entreprise (icône + texte).
  • Affiche "Sans contact lié" si links est vide.

ReminderFormModal

  • Props prefillContactId / prefillCompanyId : préremplissage depuis la fiche contact ou entreprise.
  • linkType ref : 'none' | 'contact' | 'company' — piloté par les boutons radio.
  • toDateTimeLocal() : convertit un ISO string en format YYYY-MM-DDTHH:mm pour l'input datetime-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

PageIntégration E5
pages/reminders/index.vuePage dédiée aux rappels avec groupes et formulaire
pages/contacts/[id].vueInclut &lt;ContactRemindersPanel&gt; avec les rappels actifs du contact
pages/companies/[id].vueBouton "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 ownerUserId

La 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/false
  • create() — injection enterpriseId/ownerUserId depuis ctx, ForbiddenException contactId hors scope, ForbiddenException companyId hors scope, création avec entité valide
  • update()NotFoundException ID inconnu, done: truedoneAt: Date, done: falsedoneAt: null, mise à jour conditionnelle (description absente non écrasée)
  • remove() — suppression réussie, NotFoundException ID 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 et includeDone
  • 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

USCritère d'acceptationImplémentationStatut
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 }) — si dto.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 avec includeDone=true, la réponse peut devenir volumineuse. À traiter post-MVP.

Document v1 · mai 2026 · à synchroniser avec les user stories (heriade-contacts-USER-STORIES.md)