Files
Lesstime/docs/superpowers/specs/2026-03-10-time-tracking-design.md
matthieu c72f17eb93
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
docs : add time tracking design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:59:02 +01:00

7.4 KiB

Time Tracking (Toggl-style Timer)

Résumé

Système de suivi de temps type Toggl intégré à Lesstime. Permet de démarrer des timers depuis les tickets (TaskCard) ou à vide depuis la sidebar, visualiser les temps sur un calendrier semaine/jour, et gérer les entrées de temps (drag, resize, copier-coller).

Modèle de données

Entité TimeEntry

Champ Type Contraintes
id integer PK, auto-increment
title string(255) nullable
description text nullable
startedAt datetimetz_immutable requis (stocké en UTC)
stoppedAt datetimetz_immutable nullable (null = timer actif, stocké en UTC)
user ManyToOne → User requis, CASCADE on delete
project ManyToOne → Project nullable, SET NULL on delete
task ManyToOne → Task nullable, SET NULL on delete
types ManyToMany → TaskType join table time_entry_task_type

Règles métier

  • Un seul timer actif (stoppedAt = null) par user à la fois
  • stoppedAt > startedAt si renseigné
  • Les entrées de temps peuvent se chevaucher
  • Démarrage depuis un ticket : copie title, project, task, types depuis la Task. Le user est toujours le user connecté (pas l'assignee du ticket)
  • Démarrage à vide : seuls startedAt et user (connecté) sont renseignés, le reste peut être complété après
  • Unicité timer actif : index partiel unique sur (user_id) WHERE stopped_at IS NULL
  • Entrées traversant minuit : tronquées visuellement à la fin du jour, la suite s'affiche dans la colonne du jour suivant
  • Toutes les dates sont stockées et échangées en UTC. Le frontend convertit en heure locale pour l'affichage

API Endpoints

Préfixe /api.

Sécurité / Autorisations

  • Tout user authentifié peut lire les entrées de tous les users (filtrage par user côté frontend)
  • Un user peut créer/modifier/supprimer ses propres entrées
  • Un ROLE_ADMIN peut créer/modifier/supprimer les entrées de n'importe qui
  • Assigner un temps à un autre user (user ≠ soi-même) requiert ROLE_ADMIN
Méthode Route Description
GET /api/time_entries Liste avec filtres : user, project, startedAt[after], startedAt[before], types
POST /api/time_entries Créer une entrée ou démarrer un timer
PATCH /api/time_entries/{id} Modifier (stopper, compléter, redimensionner, déplacer)
DELETE /api/time_entries/{id} Supprimer
GET /api/time_entries/active Timer actif du user connecté (custom Provider, uriTemplate avec priorité > item route)

Frontend

Store Pinia useTimerStore

state: {
    activeEntry: TimeEntry | null
}

getters: {
    isRunning: boolean        // activeEntry !== null
    elapsed: number           // calculé via setInterval: now - activeEntry.startedAt
}

actions: {
    fetchActive()             // GET /api/time_entries/active — appelé au chargement app
    start()                   // POST à vide (startedAt: now, user: currentUser)
    startFromTask(task: Task) // Stoppe le timer actif si existant, puis POST avec données du ticket (user = connecté, pas assignee)
    stop()                    // PATCH stoppedAt: now
}

Le temps est fiable même si le navigateur est fermé : startedAt est en base, le compteur affiche toujours now - startedAt au rechargement.

Timer dans la sidebar (bas à gauche)

  • Inactif : affiche 00:00:00 + bouton play (démarrage à vide)
  • Actif : compteur temps réel + bouton stop
  • Toujours visible, dans le layout default.vue

Bouton play sur TaskCard

  • Bouton play existant sur les cartes du kanban
  • Clic → timerStore.startFromTask(task)
  • Si un timer est déjà actif : stop automatique de l'ancien, puis démarrage du nouveau

Page "Suivi des temps"

Route : /time-tracking Lien sidebar : "Suivi de temps" (icône horloge)

Header

  • Titre "Suivi des temps"
  • Mois/année en orange
  • Toggle vue : Semaine / Jour avec flèches < >
  • Filtres : User (select, défaut = user connecté), Type (select TaskType)
  • Bouton "+ Ajouter une Activité"

Grille calendrier

  • Axe Y : 00:00 → 23:59 (minuit à minuit)
  • Axe X : 7 colonnes (semaine, Lun→Dim) ou 1 colonne (jour)
  • Chaque colonne : jour + date + total heures sous la date

Blocs de temps

  • Couleur = couleur du projet
  • Contenu : titre, nom du projet (petit), badge type coloré, durée
  • Les blocs peuvent se chevaucher

Interactions

Action Comportement
Clic sur un bloc Ouvre le drawer en mode édition
Drag & drop d'un bloc Déplacer vers un autre créneau ou autre jour
Resize (bord bas) Redimensionner la durée (modifie stoppedAt)
Clic sur créneau vide Ouvre le drawer en mode création avec heure début pré-remplie
Clic droit sur un bloc Menu contextuel : Copier, Supprimer
Clic droit sur créneau vide Menu contextuel : Coller (si un bloc copié)
Bouton "+ Ajouter une Activité" Ouvre le drawer en mode création

Drawer "Ajouter/Modifier un temps"

Utilise le composant AppDrawer existant.

Champs :

  • Titre (input text)
  • Description (textarea)
  • Heure début (datetime picker)
  • Heure fin (datetime picker)
  • User (select, défaut = user connecté, peut assigner à un autre)
  • Projet (select)
  • Type (select TaskType)
  • Bouton Enregistrer

En mode édition : champs pré-remplis avec les données du TimeEntry.

Service frontend

useTimeEntryService()

getByDateRange(params: { after: string, before: string, user?: number, types?: number[] }): Promise<TimeEntry[]>
getActive(): Promise<TimeEntry | null>
create(payload: TimeEntryWrite): Promise<TimeEntry>
update(id: number, payload: Partial<TimeEntryWrite>): Promise<TimeEntry>
remove(id: number): Promise<void>

DTO TimeEntry

type TimeEntry = {
    id: number
    '@id'?: string
    title: string | null
    description: string | null
    startedAt: string           // ISO datetime
    stoppedAt: string | null    // null = timer actif
    user: UserData
    project: Project | null
    task: Task | null
    types: TaskType[]
}

type TimeEntryWrite = {
    title?: string | null
    description?: string | null
    startedAt: string
    stoppedAt?: string | null
    user: string                // IRI
    project?: string | null     // IRI
    task?: string | null        // IRI
    types?: string[]            // IRIs
}

Modifications sur l'existant

  • DTO Task : ajouter le champ project: Project (nécessaire pour startFromTask)
  • TaskCard.vue : connecter le bouton play existant à timerStore.startFromTask(task)
  • default.vue : intégrer SidebarTimer.vue en bas de la sidebar (au-dessus du bouton collapse). En mode collapsed : afficher uniquement le bouton play/stop sans le compteur texte
  • Sidebar links : ajouter le lien "Suivi de temps" vers /time-tracking

Composants frontend

Composant Rôle
TimeTrackingCalendar.vue Grille calendrier (semaine/jour) avec blocs
TimeEntryBlock.vue Bloc de temps individuel (drag, resize)
TimeEntryDrawer.vue Drawer ajout/modification
TimeEntryContextMenu.vue Menu contextuel (copier, coller, supprimer)
SidebarTimer.vue Widget timer dans la sidebar