- PHP 65.2%
- Vue 27.8%
- JavaScript 3.6%
- Shell 1.3%
- Dockerfile 1.3%
- Other 0.8%
| .forgejo/workflows | ||
| backend | ||
| db/init | ||
| docker/php | ||
| frontend | ||
| .env.example | ||
| .gitignore | ||
| compose.yml | ||
| docker-compose.yml | ||
| README.md | ||
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 debuilddes 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écessiteREGISTRYetAPP_VERSIONdans 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
distcontenus dans l'image) et proxifie/apivers 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 :
- Global —
settings.timeclock_global_enabled(admin) - Par profil de contrat —
contract_types.timeclock_mode(disabled/optional/required) → un cadre forfait jour =disabled, un employé =required - Par utilisateur —
users.timeclock_overridepour les exceptions (inherit= on suit le profil)
Semaine type & temps partiel
Le temps partiel est modélisé à deux niveaux complémentaires :
- Quotité contractuelle —
users.work_ratio(100 / 80 / 60…) : sert au prorata des droits à congés et aux contrôles de cohérence. - Détail réel —
work_schedules, une ligne par (utilisateur, jour) avec durée théorique et flagis_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=0sur le mercredi ; - journées raccourcies : ex. 6 h/jour au lieu de 7 h →
minutes_workedré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_balancespar (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_step— double validation : visa du manager (statutmanager_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 :
- Profil (
contract_types.carry_over_override) :never(jamais),allow(force l'autorisation en ignorant l'entité),inherit(défaut). - Entité (
entities.allow_carry_over+carry_over_cap) : interrupteur global de l'entité et plafond par défaut. - 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 versmain/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 conteneurdocker:latestvia le socket Docker, avecdocker loginsur le registre Forgejo. Secret attendu :REPO_TOKEN. Le chemin du registre (REGISTRYdans 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 :
- attente de la disponibilité de la base ;
- génération des clés JWT si absentes (persistées dans le volume
jwt_keys) ; - application des migrations Doctrine ;
- 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 estadmin, le mot de passe est celui d'ADMIN_PASSWORD.