Aller au contenu

Story CP-015: User Management UI Enhancements

Module: control-plane Slice: Slice 3 from architecture (split from CP-003) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 3 Depends on: CP-003 Estimated complexity: S


Objective

Enhance the User Management page with invite status display (active/expired with re-invite) and multi-tenant detection UX during the invitation flow. These are frontend-focused enhancements that build on the user list and invite flow established in CP-003.


Backend Scope

Entities & Records

No new entities. This story consumes the User, UserTenantMembership, and related entities from CP-003.

Service Layer

UserService.java (additions): - resendInvitation(UUID userId, UUID tenantId) → void — Re-sends invitation email for expired invitations: 1. Verify user has status=INVITED and membership in tenant 2. Call KeycloakAdminService.sendPasswordSetupEmail(email) 3. Publish UserInvitationResent event - checkExistingUser(String email) → Optional<UserExistsResponse> — Returns user existence info for multi-tenant detection: - If email exists on platform: returns {exists: true, firstName, lastName} (no tenant details — privacy) - If not: returns {exists: false}

DTOs (additions):

public record UserExistsResponse(boolean exists, @Nullable String firstName, @Nullable String lastName) {}

API Endpoints

Method Path Request Body Response Auth
POST /api/v1/users/{id}/resend-invite 204 ROLE_DG, ROLE_RH_MANAGER
GET /api/v1/users/check-email?email= UserExistsResponse ROLE_DG, ROLE_RH_MANAGER

Validation Rules

  • Resend invite: user must have status=INVITED and be a member of the current tenant
  • Check email: valid email format, max 254 chars

Frontend Scope

Components

Uses UserManagementPage.tsx and InviteUserSheet.tsx from CP-003. This story adds behavior to those components.

UI Enhancements

User list — invite status display (AC-024):

Each user card/row shows status:

User Status Visual (Mobile Card) Visual (Desktop Row)
ACTIVE Green dot + "Actif" Green dot + "Actif"
INVITED (valid) Blue dot + "Invitation en cours" Blue dot + "Invitation en cours"
INVITED (expired) Orange dot + "Expiré — Renvoyer →" link Orange dot + "Expiré — Renvoyer →" link
DISABLED Grey dot + "Désactivé" Grey dot + "Désactivé"
  • Invitation expiry: Invitation links expire after 72 hours (configurable). Expiry is determined by comparing created_date + 72h against current time.
  • Re-invite action: Tapping "Renvoyer →" calls POST /api/v1/users/{id}/resend-invite and shows toast: "Invitation renvoyée à {email}"

Invite flow — multi-tenant detection (AC-025):

In InviteUserSheet.tsx, after the user types an email and the field loses focus (onBlur): 1. Call GET /api/v1/users/check-email?email={email} 2. If exists: true, show info banner below the email field: - "Cet utilisateur existe déjà sur la plateforme. Il sera ajouté à votre entreprise avec le rôle sélectionné." - Style: info blue background, info icon, French text 3. If exists: false, no banner shown (normal flow)

UI States

State Behavior French Copy
Re-invite loading Spinner on "Renvoyer" link
Re-invite success Green toast "Invitation renvoyée à {email}"
Re-invite error Red toast "Une erreur est survenue. Veuillez réessayer."
Multi-tenant detected Info banner below email field "Cet utilisateur existe déjà sur la plateforme. Il sera ajouté à votre entreprise avec le rôle sélectionné."

Responsive Behavior

Same breakpoints as CP-003 User Management page. Status badges and re-invite links adapt to card (mobile) vs. table row (desktop) layout.


Acceptance Criteria

AC-024: User list shows invite status
Given I am on the User Management page with 3 users (2 active, 1 invited with expired link)
When the page loads
Then I see 2 users with green "Actif" dots
And 1 user with "Expiré — Renvoyer →" link

AC-025: Invite flow multi-tenant detection
Given I tap "+" to invite "[email protected]" who already exists on the platform
When I enter the email and the field loses focus
Then I see the info message: "Cet utilisateur existe déjà sur la plateforme. Il sera ajouté à votre entreprise avec le rôle sélectionné."

OHADA & Regulatory Rules

  • Loi 2013-450 (Art. 36): The check-email endpoint returns minimal information (exists + first/last name only, no tenant details) to avoid leaking cross-tenant information.

Standards & Conventions

  • docs/standards/react-typescript-guidelines.md — §List components, §Toast pattern, §Info banner
  • docs/standards/api-guidelines.md — §Standard envelope

Testing Requirements

Unit Tests

  • UserService.resendInvitation: sends email for expired invitation
  • UserService.resendInvitation: rejects if user is not INVITED
  • UserService.checkExistingUser: returns exists=true with name for existing user
  • UserService.checkExistingUser: returns exists=false for unknown email

Integration Tests

  • POST /api/v1/users/{id}/resend-invite — success for expired invitation
  • POST /api/v1/users/{id}/resend-invite — 400 for non-INVITED user
  • GET /api/v1/users/check-email — existing user returns minimal info
  • GET /api/v1/users/check-email — unknown email returns exists=false

What QA Will Validate

  • User list shows correct status dots and labels for each user state
  • Expired invitation shows "Expiré — Renvoyer →" link
  • Re-invite sends email and shows success toast
  • Multi-tenant detection message appears after entering existing email
  • No cross-tenant information leaks in check-email response

Out of Scope

  • User deactivation/removal UI — V2
  • Invitation expiry configuration UI — V2 (72h default is hardcoded)
  • Tenant switching UI — CP-011

Definition of Done

  • [ ] User list shows correct status for ACTIVE, INVITED (valid), INVITED (expired), DISABLED
  • [ ] Re-invite flow works end-to-end
  • [ ] Multi-tenant detection banner displays correctly
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] French micro-copy matches design spec exactly
  • [ ] All UI states render correctly at 360px, 768px, 1024px
  • [ ] No TypeScript any — all types explicit
  • [ ] API responses follow standard envelope format