Compare commits

...

6 Commits

Author SHA1 Message Date
gitea-actions 7e32e4c013 chore: bump version to v0.4.14
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 52s
2026-05-26 09:36:14 +00:00
Matthieu 8fb5b80d8d fix(absences) : afficher le solde de CP avec décimales (8,75) sans arrondir
Auto Tag Develop / tag (push) Successful in 8s
Le solde était arrondi à la demi-journée (Math.round(n*2)/2), affichant
9 au lieu de 8,75 : un salarié pouvait croire à un droit supérieur au
réel. Formatage via Intl.NumberFormat fr-FR (virgule, max 2 décimales,
zéros superflus retirés) dans formatDays et les cartes de solde.
2026-05-26 11:36:04 +02:00
gitea-actions 96e25c2390 chore: bump version to v0.4.13
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 48s
2026-05-26 09:33:27 +00:00
Matthieu 02ac151ac0 feat(users) : ajout prénom et nom sur l'utilisateur
Auto Tag Develop / tag (push) Successful in 7s
Deux colonnes nullable firstName/lastName sur User (groupes me:read,
user:list, user:write), éditables dans le drawer utilisateur (admin).
L'affichage reste basé sur le username pour l'instant. Migration +
valeurs de démo dans les fixtures.
2026-05-26 11:33:18 +02:00
gitea-actions 1991c43f8c chore: bump version to v0.4.12
Auto Tag Develop / tag (push) Successful in 8s
Build & Push Docker Image / build (push) Successful in 57s
2026-05-26 09:08:30 +00:00
Matthieu e9ca00aeb2 fix(ui) : espace vertical dans le corps des drawers (champ focus rogné)
Auto Tag Develop / tag (push) Successful in 10s
Le body scrollable de MalioDrawer (overflow-y-auto) n'avait aucun
padding vertical : le label flottant du premier champ, qui remonte au
focus, dépassait le bord haut et se faisait rogner sous l'entête. Idem
pour le dernier champ en bas.

Ajoute une feuille app.css (hors thème sombre) qui donne 1rem de padding
haut/bas au corps de tous les drawers, via l'API data-test stable de
@malio/layer-ui (sans modifier la lib ni chaque drawer).
2026-05-26 11:08:19 +02:00
10 changed files with 123 additions and 6 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.4.11' app.version: '0.4.14'
+21
View File
@@ -0,0 +1,21 @@
/*
* App-level layout fixes (not theme-related).
*/
/*
* MalioDrawer : donne au corps scrollable un peu d'espace vertical.
*
* Le body du drawer est `overflow-y-auto` sans padding vertical. Or le label
* flottant d'un champ Malio remonte (-1.25rem) au focus/remplissage : pour le
* PREMIER champ, collé en haut du body, ce label dépasse le bord supérieur et
* se fait rogner (il « grossit et passe sous l'entête »). Le dernier champ
* (popover de date, hint) souffre du même rognage en bas.
*
* On ajoute donc un padding vertical au body de TOUS les drawers via l'API de
* test stable de la lib (@malio/layer-ui), sans la modifier ni toucher chaque
* drawer un par un. Le sélecteur reste limité au panneau du drawer.
*/
[data-test="panel"] > [data-test="body"] {
padding-top: 1rem;
padding-bottom: 1rem;
}
@@ -102,7 +102,8 @@ const others = computed<AbsenceBalance[]>(() =>
) )
function formatNumber(n: number): string { function formatNumber(n: number): string {
return (Math.round(n * 2) / 2).toString() // Valeur réelle avec décimales (ex. 8,75) : pas d'arrondi qui gonflerait le solde.
return new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 2 }).format(n)
} }
// Total entitlement = acquired (N-1) + in-progress (N); falls back to the // Total entitlement = acquired (N-1) + in-progress (N); falls back to the
+18
View File
@@ -11,6 +11,16 @@
:error="touched.username && !form.username.trim() ? 'Le nom est requis' : ''" :error="touched.username && !form.username.trim() ? 'Le nom est requis' : ''"
@blur="touched.username = true" @blur="touched.username = true"
/> />
<MalioInputText
v-model="form.firstName"
label="Prénom"
input-class="w-full"
/>
<MalioInputText
v-model="form.lastName"
label="Nom"
input-class="w-full"
/>
<MalioInputPassword <MalioInputPassword
v-model="form.password" v-model="form.password"
label="Mot de passe" label="Mot de passe"
@@ -84,6 +94,8 @@ const isSubmitting = ref(false)
const form = reactive({ const form = reactive({
username: '', username: '',
firstName: '',
lastName: '',
password: '', password: '',
roles: [] as string[], roles: [] as string[],
isEmployee: false, isEmployee: false,
@@ -98,11 +110,15 @@ watch(() => props.modelValue, (open) => {
if (open) { if (open) {
if (props.item) { if (props.item) {
form.username = props.item.username ?? '' form.username = props.item.username ?? ''
form.firstName = props.item.firstName ?? ''
form.lastName = props.item.lastName ?? ''
form.password = '' form.password = ''
form.roles = [...props.item.roles] form.roles = [...props.item.roles]
form.isEmployee = props.item.isEmployee ?? false form.isEmployee = props.item.isEmployee ?? false
} else { } else {
form.username = '' form.username = ''
form.firstName = ''
form.lastName = ''
form.password = '' form.password = ''
form.roles = ['ROLE_USER'] form.roles = ['ROLE_USER']
form.isEmployee = false form.isEmployee = false
@@ -124,6 +140,8 @@ async function handleSubmit() {
try { try {
const payload: UserWrite = { const payload: UserWrite = {
username: form.username.trim(), username: form.username.trim(),
firstName: form.firstName.trim() || null,
lastName: form.lastName.trim() || null,
roles: form.roles, roles: form.roles,
isEmployee: form.isEmployee, isEmployee: form.isEmployee,
} }
+5 -3
View File
@@ -75,9 +75,11 @@ export function useAbsenceHelpers() {
} }
function formatDays(days: number): string { function formatDays(days: number): string {
const rounded = Math.round(days * 2) / 2 // Affiche la valeur réelle avec décimales (ex. 8,75) : un solde de CP se
const unit = rounded > 1 ? t('absences.daysPlural') : t('absences.daySingular') // gère en demi/quart de journée, arrondir masquerait des droits réels.
return `${rounded} ${unit}` const value = new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 2 }).format(days)
const unit = days >= 2 ? t('absences.daysPlural') : t('absences.daySingular')
return `${value} ${unit}`
} }
return { return {
+1 -1
View File
@@ -2,7 +2,7 @@ export default defineNuxtConfig({
compatibilityDate: '2025-07-15', compatibilityDate: '2025-07-15',
devtools: {enabled: false}, devtools: {enabled: false},
ssr: false, ssr: false,
css: ['~/assets/css/dark.css'], css: ['~/assets/css/app.css', '~/assets/css/dark.css'],
app: { app: {
baseURL: process.env.NODE_ENV === 'production' baseURL: process.env.NODE_ENV === 'production'
? (process.env.NUXT_PUBLIC_APP_BASE || '/') ? (process.env.NUXT_PUBLIC_APP_BASE || '/')
+4
View File
@@ -4,6 +4,8 @@ export type UserData = {
id: number id: number
'@id'?: string '@id'?: string
username: string username: string
firstName?: string | null
lastName?: string | null
roles: string[] roles: string[]
avatarUrl?: string | null avatarUrl?: string | null
apiToken?: string | null apiToken?: string | null
@@ -20,6 +22,8 @@ export type UserData = {
export type UserWrite = { export type UserWrite = {
username: string username: string
firstName?: string | null
lastName?: string | null
plainPassword?: string plainPassword?: string
roles: string[] roles: string[]
// HR / absence management // HR / absence management
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add optional first name / last name to users.
*/
final class Version20260526120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add user.first_name and user.last_name (nullable)';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" ADD first_name VARCHAR(100) DEFAULT NULL');
$this->addSql('ALTER TABLE "user" ADD last_name VARCHAR(100) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" DROP COLUMN IF EXISTS first_name');
$this->addSql('ALTER TABLE "user" DROP COLUMN IF EXISTS last_name');
}
}
+8
View File
@@ -43,6 +43,8 @@ class AppFixtures extends Fixture
// Users // Users
$admin = new User(); $admin = new User();
$admin->setUsername('admin'); $admin->setUsername('admin');
$admin->setFirstName('Alex');
$admin->setLastName('Martin');
$admin->setRoles(['ROLE_ADMIN']); $admin->setRoles(['ROLE_ADMIN']);
$admin->setPassword($this->passwordHasher->hashPassword($admin, 'admin')); $admin->setPassword($this->passwordHasher->hashPassword($admin, 'admin'));
$admin->setApiToken('dev-mcp-token-for-testing-only-do-not-use-in-production'); $admin->setApiToken('dev-mcp-token-for-testing-only-do-not-use-in-production');
@@ -50,18 +52,24 @@ class AppFixtures extends Fixture
$userAlice = new User(); $userAlice = new User();
$userAlice->setUsername('alice'); $userAlice->setUsername('alice');
$userAlice->setFirstName('Alice');
$userAlice->setLastName('Dupont');
$userAlice->setRoles(['ROLE_USER']); $userAlice->setRoles(['ROLE_USER']);
$userAlice->setPassword($this->passwordHasher->hashPassword($userAlice, 'alice')); $userAlice->setPassword($this->passwordHasher->hashPassword($userAlice, 'alice'));
$manager->persist($userAlice); $manager->persist($userAlice);
$userBob = new User(); $userBob = new User();
$userBob->setUsername('bob'); $userBob->setUsername('bob');
$userBob->setFirstName('Bob');
$userBob->setLastName('Leroy');
$userBob->setRoles(['ROLE_USER']); $userBob->setRoles(['ROLE_USER']);
$userBob->setPassword($this->passwordHasher->hashPassword($userBob, 'bob')); $userBob->setPassword($this->passwordHasher->hashPassword($userBob, 'bob'));
$manager->persist($userBob); $manager->persist($userBob);
$userCharlie = new User(); $userCharlie = new User();
$userCharlie->setUsername('charlie'); $userCharlie->setUsername('charlie');
$userCharlie->setFirstName('Charlie');
$userCharlie->setLastName('Moreau');
$userCharlie->setRoles(['ROLE_USER']); $userCharlie->setRoles(['ROLE_USER']);
$userCharlie->setPassword($this->passwordHasher->hashPassword($userCharlie, 'charlie')); $userCharlie->setPassword($this->passwordHasher->hashPassword($userCharlie, 'charlie'));
$manager->persist($userCharlie); $manager->persist($userCharlie);
+32
View File
@@ -55,6 +55,14 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[Groups(['me:read', 'task:read', 'user:list', 'user:write', 'time_entry:read', 'absence_request:read', 'absence_balance:read'])] #[Groups(['me:read', 'task:read', 'user:list', 'user:write', 'time_entry:read', 'absence_request:read', 'absence_balance:read'])]
private ?string $username = null; private ?string $username = null;
#[ORM\Column(length: 100, nullable: true)]
#[Groups(['me:read', 'user:list', 'user:write'])]
private ?string $firstName = null;
#[ORM\Column(length: 100, nullable: true)]
#[Groups(['me:read', 'user:list', 'user:write'])]
private ?string $lastName = null;
/** @var list<string> */ /** @var list<string> */
#[ORM\Column] #[ORM\Column]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")] #[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
@@ -147,6 +155,30 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(?string $firstName): static
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(?string $lastName): static
{
$this->lastName = $lastName;
return $this;
}
public function getUserIdentifier(): string public function getUserIdentifier(): string
{ {
return (string) $this->username; return (string) $this->username;