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 + 72hagainst current time. - Re-invite action: Tapping "Renvoyer →" calls
POST /api/v1/users/{id}/resend-inviteand 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 bannerdocs/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