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 onlyHR_MANAGER/DG: all employees, filtered byorgUnitIdif 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=nullHR_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¶
monthquery param must be a validYYYY-MMformat — 400"Format de mois invalide. Utilisez YYYY-MM."orgUnitIdis 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
tenantIdfromScopedValue<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 labelDepartmentFilter— 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 periodCalendarLegend— Legend: Absent, Present, Aujourd'huiCalendarSummary— Summary section: absence rate, absent today countPersonAbsenceSheet— BottomSheet (mobile) showing person's absences for the monthAbsenceTooltip— 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 optimizationdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §Read-only query optimizationdocs/standards/api-guidelines.md— §GET endpoint patterns, §ApiResponse envelope, §Query parameter validationdocs/standards/react-typescript-guidelines.md— §Component patterns, §CSS Modules, §i18n, §Responsive patternsdocs/standards/design-system.md— §Select, §BottomSheet, §EmptyState, §Toastdocs/standards/offline-sync-guidelines.md— §Cached read-only data, §Stale-data banner pattern
Testing Requirements¶
Unit Tests¶
TeamCalendarService.getTeamCalendar(): EMPLOYEE role → names=null, leaveTypes=nullTeamCalendarService.getTeamCalendar(): MANAGER role → names=abbreviated, leaveTypes=nullTeamCalendarService.getTeamCalendar(): HR_MANAGER role → names=abbreviated, leaveTypes=visibleTeamCalendarService.getTeamCalendar(): DG role → same as HR_MANAGERTeamCalendarService.getTeamCalendar(): EMPLOYEE attempts cross-unit query → 403TeamCalendarService.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