Aller au contenu

Story AB-018: Team Calendar (Backend + Frontend)

Module: absence-management Slice: VS-AB-013 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 18 Depends on: AB-002 Can develop concurrently with: AB-019, AB-020 Merge order: After AB-002 Estimated complexity: M PRD User Stories: US-ABS-003


Objective

Provide a team calendar view showing employee absences by day in a timeline/Gantt format, with privacy-respecting visibility (employees see "Absent" only, managers see names, HR/DG see leave types) and department filtering for HR.


Backend Scope

Entities & Records

Response records:

public record TeamCalendarResponse(
    YearMonth month,
    List<TeamCalendarEmployee> employees,
    TeamCalendarSummary summary
) {}

public record TeamCalendarEmployee(
    UUID employeeId,
    String name,                        // null for EMPLOYEE role (privacy)
    List<TeamCalendarAbsence> absences
) {}

public record TeamCalendarAbsence(
    LocalDate startDate,
    LocalDate endDate,
    String leaveType                    // null unless viewer is HR_MANAGER or DG
) {}

public record TeamCalendarSummary(
    BigDecimal absenceRate,             // percentage, 1 decimal
    int absentToday,
    int totalEmployees
) {}

Repository Layer

Uses existing repositories from AB-002: - LeaveRequestRepository — query APPROVED, IN_PROGRESS, COMPLETED leaves for a given month - Custom query: findByTenantIdAndStatusInAndDateRange(UUID tenantId, List<LeaveStatus> statuses, LocalDate monthStart, LocalDate monthEnd) — returns all leaves overlapping with the month

Service Layer

TeamCalendarService.java:

  • getTeamCalendar(UUID tenantId, YearMonth month, String scope, UUID orgUnitId, AuthenticatedUser currentUser): TeamCalendarResponse
  • Determine employee set based on role:
    • EMPLOYEE: team members in same org unit (from employee service)
    • MANAGER: direct reports only
    • HR_MANAGER / DG: all employees, filtered by orgUnitId if provided
  • Query APPROVED + IN_PROGRESS + COMPLETED leaves for the month
  • Apply RBAC-based detail level:
    • EMPLOYEE: name=null, leaveType=null (presence/absence only)
    • MANAGER: name=abbreviated (e.g., "DIALLO F."), leaveType=null
    • HR_MANAGER / DG: name=abbreviated, leaveType=visible
  • Calculate summary: absenceRate, absentToday, totalEmployees
  • Performance target: < 1 sec for 50-person team

  • getMyTeamCalendar(UUID tenantId, YearMonth month, AuthenticatedUser currentUser): TeamCalendarResponse

  • Convenience method for managers — scopes to direct reports automatically

API Endpoints

Method Path Query Params Response Auth
GET /api/v1/absence/team-calendar month (required, YYYY-MM), scope (optional: ORG_UNIT), orgUnitId (optional) ApiResponse<TeamCalendarResponse> EMPLOYEE (presence-only), MANAGER (direct reports), HR_MANAGER/DG (all)
GET /api/v1/absence/team-calendar/my-team month (required, YYYY-MM) ApiResponse<TeamCalendarResponse> MANAGER, HR_MANAGER, DG

Validation Rules

  • month query param must be a valid YYYY-MM format — 400 "Format de mois invalide. Utilisez YYYY-MM."
  • orgUnitId is only accepted for HR_MANAGER/DG — 403 for other roles
  • EMPLOYEE can only see their own org unit — 403 if attempting cross-unit query
  • MANAGER can only see direct reports — 403 if attempting to view other teams

Multi-Tenant Considerations

  • All queries include tenantId from ScopedValue<TenantContext>
  • Employee org unit membership resolved via employee service (cross-module event or cached data)
  • Calendar data is read-only — no mutation, no offline sync needed
  • Works across all 4 DB profiles (designed for BYO-DB first)

Frontend Scope

OFFLINE SYNC REMINDER (Papillon — CRITICAL): Team calendar is a read-only view. Cached calendar data is served when offline with a stale-data banner showing last sync time. See docs/standards/offline-sync-guidelines.md.

Pages & Routes

Route Page Description
/absences/calendrier TeamCalendarPage Team calendar timeline view

Components

  • MonthNavigator — Month navigation: left/right arrows + month/year label
  • DepartmentFilter — Select for department filtering (HR/DG only, hidden for others)
  • CalendarTimeline — Horizontally scrollable timeline grid (rows=people, columns=days)
  • AbsenceBar — Amber bar representing an absence period
  • CalendarLegend — Legend: Absent, Present, Aujourd'hui
  • CalendarSummary — Summary section: absence rate, absent today count
  • PersonAbsenceSheet — BottomSheet (mobile) showing person's absences for the month
  • AbsenceTooltip — Tooltip (desktop) showing absence details on hover

UI States (ALL REQUIRED)

State Behavior French Copy
Loading Grid shimmer cells (rows x columns)
Empty (no absences) EmptyState with people illustration "Toute l'equipe est presente ce mois-ci" / CTA: "Mois precedent"
Error Toast + retry button "Impossible de charger le calendrier."
Offline Cached calendar data + stale-data banner "Hors connexion — derniere synchro : {time}"
Success N/A (read-only view)

Responsive Behavior

Mobile (360px — Papillon primary): - Month navigation at top: "< Mars 2026 >" - Department filter (Select, HR/DG only) below navigation - Horizontally scrollable timeline: - Rows = people (3-letter abbreviation, e.g., "DIA"), fixed left column - Columns = days of month (1-31), scrollable - Today highlighted with primary-light background - Absent = amber bar, Present = light fill, Weekend = dimmed - Legend below timeline: "Absent / Present / Aujourd'hui" - Tap on name → BottomSheet with person's absences for visible month - Summary section at bottom: "Taux d'absence: X% / Absents aujourd'hui: N/total" - All touch targets >= 48px

Desktop (1024px+ — Enterprise primary): - Full month visible (31 days), all columns without horizontal scroll - Sidebar navigation visible - Hover on absence bar → tooltip: "DIALLO F. — Absent 11-13/03" - Click name (HR only) → navigate to employee balance detail (AB-021) - Department filter + CSV export button (top-right area) - More compact row height, denser information display

Interactions

  • Month navigation: tap arrows to go prev/next month, animation slide left/right
  • Department filter (HR/DG): Select component, onChange triggers re-fetch
  • Mobile tap on person row → BottomSheet opens with absence list for that person
  • Desktop hover on absence bar → AbsenceTooltip with details
  • Desktop click on name (HR only) → navigate to /absences/employes/{id}/solde
  • Offline: cached data displayed, banner shown, navigation still works within cached months

Privacy Rules

Viewer Sees
EMPLOYEE "Absent" or "Present" only, no names, no types
MANAGER Direct reports names + "Absent" bars, no leave type
HR_MANAGER/DG All employees, leave type on hover/tap, department filter

French Micro-Copy

  • calendar.title: "Calendrier d'equipe"
  • calendar.month_nav: "{month} {year}" (e.g., "Mars 2026")
  • calendar.department_filter: "Departement"
  • calendar.all_departments: "Tous les departements"
  • calendar.legend_absent: "Absent"
  • calendar.legend_present: "Present"
  • calendar.legend_today: "Aujourd'hui"
  • calendar.absence_rate: "Taux d'absence : {rate}%"
  • calendar.absent_today: "Absents aujourd'hui : {count}/{total}"
  • calendar.empty: "Toute l'equipe est presente ce mois-ci"
  • calendar.empty_cta: "Mois precedent"
  • calendar.error: "Impossible de charger le calendrier."
  • calendar.offline: "Hors connexion — derniere synchro : {time}"
  • calendar.tooltip: "{name} — Absent {startDay}-{endDay}/{month}"
  • calendar.sheet_title: "Absences de {name}"

Acceptance Criteria

AC-057: Manager sees direct reports' absences
Given I am a manager
When I view the team calendar
Then I see all my direct reports' absences for the current month

AC-058: Privacy-respecting leave type hiding
Given a team member is on leave
When I view the calendar as a non-HR employee
Then I see "Absent" (not the leave type) to protect privacy

AC-059: HR can filter by department
Given I am HR_MANAGER or DG
When I view the calendar
Then I can filter by department and see all employees

OHADA & Regulatory Rules

N/A — calendar is a display feature with no regulatory requirements. Privacy rules are application-level design decisions, not OHADA-mandated.


Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §REST controller patterns, §RBAC authorization, §Query optimization
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Read-only query optimization
  • docs/standards/api-guidelines.md — §GET endpoint patterns, §ApiResponse envelope, §Query parameter validation
  • docs/standards/react-typescript-guidelines.md — §Component patterns, §CSS Modules, §i18n, §Responsive patterns
  • docs/standards/design-system.md — §Select, §BottomSheet, §EmptyState, §Toast
  • docs/standards/offline-sync-guidelines.md — §Cached read-only data, §Stale-data banner pattern

Testing Requirements

Unit Tests

  • TeamCalendarService.getTeamCalendar(): EMPLOYEE role → names=null, leaveTypes=null
  • TeamCalendarService.getTeamCalendar(): MANAGER role → names=abbreviated, leaveTypes=null
  • TeamCalendarService.getTeamCalendar(): HR_MANAGER role → names=abbreviated, leaveTypes=visible
  • TeamCalendarService.getTeamCalendar(): DG role → same as HR_MANAGER
  • TeamCalendarService.getTeamCalendar(): EMPLOYEE attempts cross-unit query → 403
  • TeamCalendarService.getTeamCalendar(): MANAGER attempts to view other team → 403
  • Summary calculation: 2 absent out of 10 employees → absenceRate=20.0
  • Summary calculation: no absences → absenceRate=0.0, absentToday=0

Integration Tests

  • GET /team-calendar?month=2026-03 as MANAGER → 200, direct reports only
  • GET /team-calendar?month=2026-03 as EMPLOYEE → 200, presence/absence only
  • GET /team-calendar?month=2026-03&orgUnitId=... as HR_MANAGER → 200, filtered by department
  • GET /team-calendar?month=2026-03&orgUnitId=... as EMPLOYEE → 403
  • GET /team-calendar?month=invalid → 400
  • Multi-tenant: tenant A employees not visible to tenant B

What QA Will Validate

  • Calendar renders at 360px with horizontal scroll, today highlighted
  • Month navigation works (prev/next)
  • Tap on person name → BottomSheet with absences (mobile)
  • Hover on absence bar → tooltip with details (desktop)
  • EMPLOYEE sees no names, no leave types
  • MANAGER sees direct reports with names, no leave types
  • HR/DG sees all employees, leave types on hover, department filter works
  • Empty state renders when no absences in month
  • Offline: cached data shown with stale banner

Out of Scope

  • Leave planning at enterprise scale (V2)
  • Coverage rules / minimum staffing levels (V2)
  • CSV/PDF export (Enterprise V2)
  • Multi-month view / yearly overview (V2)
  • Drag-and-drop leave creation from calendar (V2)

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success)
  • [ ] French micro-copy matches design spec exactly
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified — employees and leaves scoped to tenant
  • [ ] Offline read works (Papillon) — cached data displayed with stale banner
  • [ ] No TypeScript any
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Privacy rules enforced — RBAC-based detail level verified for all 4 roles