No description
  • PHP 65.2%
  • Vue 27.8%
  • JavaScript 3.6%
  • Shell 1.3%
  • Dockerfile 1.3%
  • Other 0.8%
Find a file
Romain 3ec0854140
Some checks failed
ci / backend (push) Failing after 2s
ci / frontend (push) Successful in 31s
Docker Build on Tag / build-and-push (push) Successful in 4m44s
20
2026-07-29 16:29:45 +02:00
.forgejo/workflows 11 2026-07-29 10:28:33 +02:00
backend 20 2026-07-29 16:29:45 +02:00
db/init 9 2026-07-29 09:24:52 +02:00
docker/php 14 2026-07-29 13:49:25 +02:00
frontend 20 2026-07-29 16:29:45 +02:00
.env.example 8 2026-07-28 16:57:48 +02:00
.gitignore init 2026-07-28 13:58:11 +02:00
compose.yml 13 2026-07-29 13:22:30 +02:00
docker-compose.yml 13 2026-07-29 13:22:30 +02:00
README.md 20 2026-07-29 16:29:45 +02:00

Application RH — Temps de travail, congés & tickets resto

Application interne multi-entités de gestion du temps de travail, des congés et des jours ouvrés (base tickets restaurant), avec pointage optionnel.

Stack

Couche Choix
Base MariaDB 11.4
Backend Symfony 7 (API REST, Doctrine, JWT, RBAC)
Front Vue 3 + PrimeVue (thème Aura, FR)
Déploiement Docker Compose, images taguées
CI/CD Forgejo Actions (build sur tag v*)

Pourquoi ces choix

  • Symfony plutôt que Slim : l'app a du métier (rôles, validation, congés, périodes de référence). Symfony fournit l'auth JWT, la validation, l'ORM Doctrine avec migrations versionnées et un système de Voters pour le RBAC fin — tout ce qu'on aurait recodé à la main avec un micro-framework.
  • Vue 3 + PrimeVue : composants riches prêts à l'emploi (DataTable avec export, DatePicker plage, formulaires) → interface "friendly use" sans design system maison.

Démarrage local

cp .env.example .env      # adapter les mots de passe
docker compose up -d      # db + backend (API HTTP) + frontend (SPA + proxy)
# UI : http://localhost:8080

Deux fichiers Compose selon l'usage :

  • docker-compose.yml — développement : contient les phases de build des images backend/frontend depuis les sources locales.
  • compose.yml — déploiement : sans build, tire les images déjà poussées par la CI depuis le registre Forgejo. Nécessite REGISTRY et APP_VERSION dans le .env.

Architecture des conteneurs (3 services) :

  • backend — l'API Symfony est servie en HTTP (nginx + php-fpm intégrés à l'image, port 80 interne).
  • frontend — image autonome : nginx sert la SPA Vue (fichiers dist contenus dans l'image) et proxifie /api vers le backend. Elle se met à jour comme n'importe quelle image : nouveau tag = contenu entièrement remplacé, sans volume partagé ni accumulation de fichiers.
  • db — MariaDB.
# déploiement d'une version publiée
docker compose -f compose.yml --env-file .env up -d

Rôles (RBAC)

  • admin : configuration globale, entités, activation du pointage
  • directeur : validation congés + rapports de son entité
  • rh : gestion utilisateurs, congés, exports
  • utilisateur : ses données, demandes de congés, pointage

Concepts clés

Pointage optionnel et flexible

Le pointage se pilote à 3 niveaux, du plus général au plus précis :

  1. Globalsettings.timeclock_global_enabled (admin)
  2. Par profil de contratcontract_types.timeclock_mode (disabled / optional / required) → un cadre forfait jour = disabled, un employé = required
  3. Par utilisateurusers.timeclock_override pour les exceptions (inherit = on suit le profil)

Semaine type & temps partiel

Le temps partiel est modélisé à deux niveaux complémentaires :

  • Quotité contractuelleusers.work_ratio (100 / 80 / 60…) : sert au prorata des droits à congés et aux contrôles de cohérence.
  • Détail réelwork_schedules, une ligne par (utilisateur, jour) avec durée théorique et flag is_worked_day. C'est la source de vérité.

Ça couvre les deux formes de temps partiel :

  • jour(s) non travaillé(s) : ex. 80 % avec le mercredi off → is_worked_day=0 sur le mercredi ;
  • journées raccourcies : ex. 6 h/jour au lieu de 7 h → minutes_worked réduit.

La base hebdo du contrat (35 h / 39 h) est portée par contract_types.full_time_weekly_minutes (2100 / 2340 min), ce qui permet de vérifier qu'une semaine type est cohérente avec la quotité annoncée. Versionnable dans le temps via valid_from / valid_to.

Période de référence des congés

Configurable par entité (leave_period_start_month / _day) : année civile (1/1→31/12), 1/6→31/5, etc. LeavePeriodService :

  • calcule la fenêtre [début, fin] contenant une date donnée ;
  • proratise le droit acquis selon la quotité et la date d'entrée (1re année incomplète). Les soldes sont matérialisés dans leave_balances par (user, type, période).

Jours travaillés / tickets resto

Calculés par WorkedDaysCalculator en croisant : semaine type × pointage (si actif) × congés × jours fériés. La source dépend du profil (counts_worked_days = schedule ou timeclock). Seuil de présence configurable (ticket_resto_min_minutes).

Congés

Types configurables (CP, RTT, maladie…). Chaque type porte son mode de validation (approvalMode) :

  • none — auto-validé (ex. télétravail) ;
  • manager — validation par le manager / directeur / RH ;
  • rh — validation RH obligatoire (ex. CP) ;
  • two_stepdouble validation : visa du manager (statut manager_ok) puis validation RH finale.

Le LeaveRequestVoter gère qui peut valider ; pour two_step, l'étape finale est réservée à RH/admin.

Soldes de congés

LeaveBalanceService gère les soldes par (utilisateur, type, période) :

  • Calcul automatique du droit acquis = droit annuel du contrat × quotité, proratisé si entrée en cours de période (via LeavePeriodService).
  • Décompte automatique à chaque validation finale d'une demande d'un type « à solde » (tracksBalance), et ré-crédit en cas d'annulation.
  • Initialisation manuelle (Admin/RH) pour la reprise d'existant ou une nouvelle embauche : on pose explicitement acquis / pris / report.

Endpoints :

Méthode Route Rôle Usage
GET /api/balances user mes soldes
GET /api/balances/user/{id} RH+ soldes d'un user
POST /api/balances/user/{id}/bootstrap RH+ créer soldes (droit auto)
POST /api/balances/initialize RH+ init manuelle (reprise)

Côté front : « Mes soldes » (utilisateur) et « Soldes (RH) » (initialisation : bouton Initialiser pour une embauche, édition inline pour une reprise).

Report de reliquat (carry-over)

Le report du solde non consommé en fin de période est configurable à trois niveaux, résolus du plus restrictif au moins restrictif :

  1. Profil (contract_types.carry_over_override) : never (jamais), allow (force l'autorisation en ignorant l'entité), inherit (défaut).
  2. Entité (entities.allow_carry_over + carry_over_cap) : interrupteur global de l'entité et plafond par défaut.
  3. Type de congé (leave_types.carry_over_policy) : none (perdu, ex. RTT), full (report intégral, ex. CP), capped (plafonné à carry_over_cap).

Le plafond effectif est le minimum des plafonds définis (type et entité) ; -1 signifie illimité. CarryOverService applique cette résolution.

Exécution en fin de période via une commande à planifier :

php bin/console app:carry-over --date=2025-12-31   # exécute le report
php bin/console app:carry-over --dry-run           # simulation, aucune écriture

Le report crédite le champ carried_over du solde de la période suivante.

Multi-entité

Toutes les données sont rattachées à une entity. Jours fériés et paramètres peuvent être globaux (entity_id NULL) ou spécifiques (Alsace-Moselle, DOM…).

Exports / rapports

Endpoint : GET /api/reports/worked-days?from=YYYY-MM-DD&to=YYYY-MM-DD&format=json|csv|xlsx|pdf&office=ID (réservé RH / directeur / admin).

  • CSV — via fputcsv
  • Excel — via PhpSpreadsheet
  • PDF — via Dompdf + template Twig templates/reports/worked_days.html.twig

Ces librairies requièrent les extensions PHP gd et zip (installées dans l'image backend et dans le job CI).

Le service WorkedDaysReportService assemble, pour chaque utilisateur actif : semaine type × congés validés × pointages × jours fériés, en choisissant la source (schedule / timeclock) selon le profil de contrat, puis délègue le comptage à WorkedDaysCalculator.

Principaux endpoints API

Méthode Route Rôle Rôle métier
POST /api/login public auth JWT
GET /api/health public monitoring
GET/POST /api/leaves utilisateur congés
POST /api/leaves/{id}/decision valideur* validation
GET/PUT /api/schedule utilisateur semaine type
POST /api/timeclock/{in,out} utilisateur pointage
GET /api/timeclock/today utilisateur pointage
GET /api/leave-types utilisateur référentiel
GET/PUT /api/admin/settings admin config
GET /api/reports/worked-days RH+ export

* fin du grain géré par LeaveRequestVoter.

Compte administrateur

Créé via php bin/console app:create-admin (login admin par défaut, mot de passe depuis ADMIN_PASSWORD). Voir la section « Mise en route backend ».

Administration (CRUD)

L'écran Administration (réservé admin) est organisé en onglets :

  • Utilisateurs — création, édition, désactivation, attribution d'un modèle de semaine type. À la création, les soldes de congés sont initialisés.
  • Entités — bureaux, période de référence des congés, politique de report.
  • Profils — types de contrat : pointage, base horaire, congés annuels.
  • Types de congés — CRUD : ajout/édition/désactivation, couleur, mode de validation, suivi de solde, et pose en demi-journée autorisée ou non.
  • Semaines types — modèles réutilisables (ex. « Temps plein 35h », « 80% mercredi off ») définis ici puis attribués aux utilisateurs.
  • Configuration — réglages globaux (pointage, ticket resto, report entité).

Endpoints sous /api/admin (réservés ROLE_ADMIN) : users, offices, contract-types, week-templates (chacun en GET/POST/PUT/DELETE).

Navigation selon le rôle

Le front récupère l'identité via /api/me et masque les onglets selon le rôle : personnel pour tous ; « À valider » / « Rapports » pour l'encadrement (admin, directeur, RH) ; « Soldes (RH) » pour RH/admin ; « Administration » pour admin. Les routes sont aussi gardées côté client.

Semaines types (modèles)

La semaine type n'est plus éditée par chaque utilisateur : ce sont des modèles gérés en admin et attribués. « Ma semaine » affiche le modèle attribué en lecture seule. Le calcul des jours travaillés en dérive.

Demi-journées

Chaque type de congé indique s'il peut être posé en demi-journée (allowHalfDay). Le formulaire de demande n'affiche les cases demi-journée que pour les types qui l'autorisent, et le back refuse une demi-journée sur un type qui ne la permet pas.

Ajustement des soldes (fiche utilisateur)

Depuis l'onglet Utilisateurs, le bouton portefeuille ouvre l'ajustement manuel des soldes de congés (acquis / pris / report), réservé RH-admin.

Validation & cas de l'admin

Un admin n'ayant personne au-dessus de lui, ses propres demandes de congés sont auto-validées (sinon elles resteraient bloquées).

Calendrier

La vue « Calendrier » affiche les congés du mois (grille mensuelle, navigation, code couleur par statut).

Export par périmètre

Le rapport jours travaillés se filtre par périmètre : tout le monde, une entité, ou un utilisateur (/api/reports/worked-days?...&office=ID ou &user=ID).

Thème clair / sombre

Bascule via le bouton lune/soleil (barre de navigation), mémorisée en localStorage. Preset PrimeVue personnalisé (primaire indigo) ; toutes les couleurs suivent les tokens --p-* et s'adaptent aux deux modes.

Qualité : tests, lint, CI

Tests (PHPUnit)

Tests unitaires sur la logique métier pure (backend/tests/Unit) :

  • WorkedDaysCalculatorTest — jours travaillés : semaine pleine, fériés, congés (comptés ou non), temps partiel, mode pointage, priorité férié.
  • LeavePeriodServiceTest — périodes de référence (année civile, juin→mai avant/après bascule) et prorata quotité + date d'entrée.
  • CarryOverServiceTest — résolution du report sur les trois niveaux et calcul du plafond effectif.
  • LeaveRequestTest — décompte des jours avec demi-journées.
cd backend
composer test          # phpunit
composer phpstan       # analyse statique niveau 6
composer cs-check      # style (php-cs-fixer, dry-run)
composer cs-fix        # corrige le style

Lint frontend

cd frontend
npm run lint           # eslint (flat config, plugin Vue)
npm run lint:fix       # corrige ce qui peut l'être

Intégration continue (Forgejo Actions)

  • .forgejo/workflows/ci.yml — sur push / PR vers main/develop : backend (composer install → cs-check → phpstan → phpunit) et frontend (npm install → lint → build).
  • .forgejo/workflows/build.yml — sur tag * : build et push des deux images (rh-backend, rh-frontend) vers le registre Forgejo local. Tourne dans un conteneur docker:latest via le socket Docker, avec docker login sur le registre Forgejo. Secret attendu : REPO_TOKEN. Le chemin du registre (REGISTRY dans le script) est à adapter à ton organisation.

Livraison versionnée

Un tag vX.Y.Z déclenche build.yml. Pour déployer, l'opérateur pose dans son .env REGISTRY=<host>/<org>/rh-app et APP_VERSION=<tag> : le docker-compose.yml tire alors les images taguées correspondantes. La table app_versions trace les versions déployées.

Arborescence

.
├── docker-compose.yml          # dev (avec build)
├── compose.yml                 # déploiement (sans build, images du registre)
├── .env.example
├── .gitignore
├── .forgejo/workflows/
│   ├── ci.yml                  # lint + tests (push / PR)
│   └── build.yml               # build images (tag v*)
├── backend/                    # API Symfony 7
│   ├── composer.json
│   ├── bin/console             # CLI Symfony (commandes, migrations)
│   ├── docker/entrypoint.sh    # init auto au démarrage (migrations, JWT, admin)
│   ├── phpunit.xml.dist
│   ├── phpstan.neon.dist
│   ├── .php-cs-fixer.dist.php
│   ├── config/                 # security, doctrine, jwt, twig, services
│   ├── migrations/             # baseline + seed (source de vérité schéma)
│   ├── templates/reports/      # gabarit PDF Twig
│   ├── tests/Unit/             # tests logique métier (PHPUnit)
│   ├── public/index.php
│   └── src/
│       ├── Entity/             # Office, User, ContractType, WorkSchedule,
│       │                       #   TimeEntry, Leave*, Setting, PublicHoliday
│       ├── Controller/         # Health, Leave, Schedule, Timeclock,
│       │                       #   Config, Export, Balance
│       ├── Command/            # CarryOverCommand, CreateAdminCommand
│       ├── Security/           # LeaveRequestVoter (RBAC fin)
│       └── Service/            # WorkedDaysCalculator, LeavePeriodService,
│                               #   WorkedDaysReportService, LeaveBalanceService,
│                               #   CarryOverService
├── frontend/                   # SPA Vue 3 + PrimeVue
│   ├── package.json
│   ├── eslint.config.js
│   └── src/
│       ├── api/client.js       # axios + intercepteur JWT
│       ├── router/             # routes + garde d'auth
│       └── views/              # Login, Leaves, Balances, Approvals, Schedule,
│                               #   Timeclock, Reports, BalanceAdmin, Admin
├── docker/php/                 # Dockerfile backend (nginx + php-fpm)
└── db/init/                    # (note) schéma géré par migrations Doctrine

Mise en route backend

En déploiement (compose.yml), rien à lancer à la main. Au démarrage, le conteneur backend exécute automatiquement, de façon idempotente :

  1. attente de la disponibilité de la base ;
  2. génération des clés JWT si absentes (persistées dans le volume jwt_keys) ;
  3. application des migrations Doctrine ;
  4. création du compte admin si absent (mot de passe depuis ADMIN_PASSWORD).

Il suffit donc de renseigner le .env puis :

docker compose -f compose.yml --env-file .env up -d
# se connecter ensuite avec  admin  /  <ADMIN_PASSWORD>

Le mot de passe admin est resynchronisé à chaque démarrage depuis ADMIN_PASSWORD : si tu changes cette valeur dans le .env et relances up -d, le mot de passe du compte admin est mis à jour automatiquement.

Après une modification du code backend (comme l'ajout de bin/console), il faut reconstruire et republier l'image avec un nouveau tag, puis déployer ce tag (APP_VERSION). Une image déjà poussée ne se met pas à jour toute seule.

Le script est backend/docker/entrypoint.sh (défini comme ENTRYPOINT de l'image). Comme chaque étape est protégée, un up répété ne casse rien : les migrations déjà jouées sont ignorées, les clés existantes conservées, et l'admin n'est pas recréé s'il existe.

Commandes manuelles (si besoin)

Pour agir ponctuellement (dev, ou reset), les commandes restent disponibles :

docker compose exec backend php bin/console doctrine:migrations:migrate
docker compose exec backend php bin/console app:create-admin --force  # reset mdp

En développement local (sur les sources)

cd backend
composer install
php bin/console lexik:jwt:generate-keypair
php bin/console doctrine:migrations:migrate
php bin/console app:create-admin

La source de vérité du schéma est constituée par les migrations Doctrine (backend/migrations/). Le dossier db/init/ ne contient plus de schéma SQL (voir db/init/README.md), pour éviter deux définitions divergentes.

Le seed ne crée plus de compte admin (plus de hash figé). Le compte administrateur se crée avec une commande dédiée qui lit le mot de passe depuis l'environnement et le hashe à l'exécution :

# ADMIN_PASSWORD doit être défini dans le .env (voir .env.example)
php bin/console app:create-admin                 # login "admin" par défaut
php bin/console app:create-admin --username=rh   # autre identifiant
php bin/console app:create-admin --force         # réinitialise le mot de passe

Connexion — l'identifiant est un champ dédié (username), distinct de l'email (optionnel, car tous les utilisateurs n'en ont pas forcément). Le login par défaut est admin, le mot de passe est celui d'ADMIN_PASSWORD.