Compare commits

...

8 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
gitea-actions b13ba41674 chore: bump version to v0.4.11
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 44s
2026-05-26 09:02:57 +00:00
Matthieu ac615875f3 fix(absences) : dates du drawer employé en pleine largeur (1 par ligne)
Auto Tag Develop / tag (push) Successful in 7s
Dans le drawer « Informations employé », les champs Date d'embauche et
Date de sortie étaient sur une grille 2 colonnes : le popover du
calendrier MalioDate, coincé en demi-largeur, chevauchait le champ
voisin. Passage en pleine largeur (sm:col-span-2) pour laisser le
calendrier s'afficher correctement.
2026-05-26 11:02:50 +02:00
11 changed files with 139 additions and 16 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
parameters:
app.version: '0.4.10'
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 {
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
+16 -10
View File
@@ -7,16 +7,22 @@
</div>
</template>
<form v-if="user" class="grid grid-cols-1 gap-4 sm:grid-cols-2" @submit.prevent="save">
<MalioDate
v-model="form.hireDate"
:label="$t('absences.admin.employees.fields.hireDate')"
group-class="w-full"
/>
<MalioDate
v-model="form.endDate"
:label="$t('absences.admin.employees.fields.endDate')"
group-class="w-full"
/>
<!-- Dates en pleine largeur (1 par ligne) : le popover du calendrier
a besoin de toute la largeur pour s'afficher correctement. -->
<div class="sm:col-span-2">
<MalioDate
v-model="form.hireDate"
:label="$t('absences.admin.employees.fields.hireDate')"
group-class="w-full"
/>
</div>
<div class="sm:col-span-2">
<MalioDate
v-model="form.endDate"
:label="$t('absences.admin.employees.fields.endDate')"
group-class="w-full"
/>
</div>
<MalioSelect
v-model="form.contractType"
:label="$t('absences.admin.employees.fields.contractType')"
+18
View File
@@ -11,6 +11,16 @@
:error="touched.username && !form.username.trim() ? 'Le nom est requis' : ''"
@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
v-model="form.password"
label="Mot de passe"
@@ -84,6 +94,8 @@ const isSubmitting = ref(false)
const form = reactive({
username: '',
firstName: '',
lastName: '',
password: '',
roles: [] as string[],
isEmployee: false,
@@ -98,11 +110,15 @@ watch(() => props.modelValue, (open) => {
if (open) {
if (props.item) {
form.username = props.item.username ?? ''
form.firstName = props.item.firstName ?? ''
form.lastName = props.item.lastName ?? ''
form.password = ''
form.roles = [...props.item.roles]
form.isEmployee = props.item.isEmployee ?? false
} else {
form.username = ''
form.firstName = ''
form.lastName = ''
form.password = ''
form.roles = ['ROLE_USER']
form.isEmployee = false
@@ -124,6 +140,8 @@ async function handleSubmit() {
try {
const payload: UserWrite = {
username: form.username.trim(),
firstName: form.firstName.trim() || null,
lastName: form.lastName.trim() || null,
roles: form.roles,
isEmployee: form.isEmployee,
}
+5 -3
View File
@@ -75,9 +75,11 @@ export function useAbsenceHelpers() {
}
function formatDays(days: number): string {
const rounded = Math.round(days * 2) / 2
const unit = rounded > 1 ? t('absences.daysPlural') : t('absences.daySingular')
return `${rounded} ${unit}`
// Affiche la valeur réelle avec décimales (ex. 8,75) : un solde de CP se
// gère en demi/quart de journée, arrondir masquerait des droits réels.
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 {
+1 -1
View File
@@ -2,7 +2,7 @@ export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: {enabled: false},
ssr: false,
css: ['~/assets/css/dark.css'],
css: ['~/assets/css/app.css', '~/assets/css/dark.css'],
app: {
baseURL: process.env.NODE_ENV === 'production'
? (process.env.NUXT_PUBLIC_APP_BASE || '/')
+4
View File
@@ -4,6 +4,8 @@ export type UserData = {
id: number
'@id'?: string
username: string
firstName?: string | null
lastName?: string | null
roles: string[]
avatarUrl?: string | null
apiToken?: string | null
@@ -20,6 +22,8 @@ export type UserData = {
export type UserWrite = {
username: string
firstName?: string | null
lastName?: string | null
plainPassword?: string
roles: string[]
// 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
$admin = new User();
$admin->setUsername('admin');
$admin->setFirstName('Alex');
$admin->setLastName('Martin');
$admin->setRoles(['ROLE_ADMIN']);
$admin->setPassword($this->passwordHasher->hashPassword($admin, 'admin'));
$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->setUsername('alice');
$userAlice->setFirstName('Alice');
$userAlice->setLastName('Dupont');
$userAlice->setRoles(['ROLE_USER']);
$userAlice->setPassword($this->passwordHasher->hashPassword($userAlice, 'alice'));
$manager->persist($userAlice);
$userBob = new User();
$userBob->setUsername('bob');
$userBob->setFirstName('Bob');
$userBob->setLastName('Leroy');
$userBob->setRoles(['ROLE_USER']);
$userBob->setPassword($this->passwordHasher->hashPassword($userBob, 'bob'));
$manager->persist($userBob);
$userCharlie = new User();
$userCharlie->setUsername('charlie');
$userCharlie->setFirstName('Charlie');
$userCharlie->setLastName('Moreau');
$userCharlie->setRoles(['ROLE_USER']);
$userCharlie->setPassword($this->passwordHasher->hashPassword($userCharlie, 'charlie'));
$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'])]
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> */
#[ORM\Column]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
@@ -147,6 +155,30 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
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
{
return (string) $this->username;