Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.4 KiB
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>startedAtsi renseigné- Les entrées de temps peuvent se chevaucher
- Démarrage depuis un ticket : copie
title,project,task,typesdepuis la Task. Leuserest toujours le user connecté (pas l'assignee du ticket) - Démarrage à vide : seuls
startedAtetuser(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 champproject: Project(nécessaire pourstartFromTask) TaskCard.vue: connecter le bouton play existant àtimerStore.startFromTask(task)default.vue: intégrerSidebarTimer.vueen 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 |