Rep Portal CPD Log Page¶
Overview¶
The Rep CPD Log page (/rep/cpd) allows representatives to view and self-log Continuing Professional Development (CPD) activities for the current calendar year. The page is implemented in client/src/pages/rep/RepCpdLogPage.tsx.
Access control: The repProfileByUser query resolves the current user's rep profile. The createCpdRecord mutation is role-gated to @Roles("FI_USER", "REP_USER", "COMP", "HR_ADMIN", "ORG_ADMIN", "SYS_ADMIN") — representatives can create their own CPD records.
Page context: The page does not wrap in a PageEntityProvider. The chatbot receives no entity context when opened from this page.
Page Layout¶
Header¶
PageHeader component:
- Title: "CPD Log"
- Subtitle: "Self-log continuing professional development activities and hours."
- Back link: "/rep" (navigates to Rep Dashboard)
YTD Summary Card (Top)¶
The YtdSummaryCard component displays the year-to-date CPD hours for the current calendar year (new Date().getFullYear()). The card has two sub-sections:
Ethics Warning Banner (conditional): Shown when totals !== null AND ethicsHours > 0 AND ethicsHours < 6.0. Amber warning card with:
- AlertTriangle icon
- "Core CPD hours below MAS minimum" heading
- "MAS requires minimum 6.0 Core CPD hours (ethics/rules/regulations, IBF/SCI-accredited) per year per FAA-N26. Current: X.X hours." message
Hours Grid (4-column responsive): Displays four hour totals from the cpdHoursTotalForRep query:
| Category | Display Label | Styling | Source |
|---|---|---|---|
| Ethics | "Ethics" (uppercase label) | Gray background | totals.ethics.toFixed(1) |
| Product | "Product" (uppercase label) | Gray background | totals.product.toFixed(1) |
| Other | "Other" (uppercase label) | Gray background | totals.other.toFixed(1) |
| Total | "Total" (primary-colored) | Primary-50 blue background, primary-700 text | totals.total.toFixed(1) |
The card heading is "YTD CPD Hours (YYYY)" with subtitle "Total hours logged across all categories for the current CPD cycle."
Category model note: The legacy ethics/product/other display buckets are being superseded by the statutory categories. The regnIFY
CpdRecordmodel was migrated to track Core CPD (ethics or rules-&-regulations, IBF/SCI-accredited — subject to an accreditation gate) vs Supplementary CPD hours, matching FAA-N26 §5.4 / SFA 04-N22 §8.4. The "Ethics" total maps to Core; "Product"/"Other" map to Supplementary.
Add CPD Entry Toggle¶
When showAddForm is false, an "Add CPD Entry" button (Plus icon, primary variant) is shown aligned to the right above the records table. Clicking it sets showAddForm = true and reveals the AddCpdEntryForm component.
Add CPD Entry Form¶
The AddCpdEntryForm component renders as a full card with form fields:
Form Header: - "Log CPD Activity" heading - "Record a continuing professional development activity and its hours." subtitle - Cancel button (outline) aligned right
Form Fields (2-column grid):
| Field | Type | Required | Notes |
|---|---|---|---|
| Cycle Year | number input | Yes | Default: current year. Range: 2000-2100. |
| Course Name | text input | Yes | Placeholder: "e.g., Financial Products & Services" |
| Provider Name | text input | No | Placeholder: "e.g., IBF, MAS, training provider" |
| IBF Course Code | text input | No | Help text: "Optional: Include if this is an IBF-accredited course"; Placeholder: "e.g., IBF-CPD-12345" |
| Completion Date | date input | Yes | CalendarIcon adornment; default: today's date in YYYY-MM-DD |
| Ethics Hours | number input | No | Min: 0, step: 0.5; Help text: "Hours related to ethics, compliance, or regulation" |
| Product Hours | number input | No | Min: 0, step: 0.5; Help text: "Hours on financial products, services, or markets" |
| Other Hours | number input | No | Min: 0, step: 0.5; Help text: "Professional development hours not in the above categories" |
Validation (via isFormValid()):
- Course name must be non-empty after trim
- Completion date must be non-empty
- At least one of hoursEthics, hoursProduct, or hoursOther must be > 0
- Error toast: "Please fill in all required fields (course name, completion date, and at least one hour type)."
Footer: Cancel (outline) + "Log CPD Entry" (primary, Plus icon, disabled when form invalid, shows loading spinner while saving)
On successful submission: toast "CPD entry logged successfully.", form closes, records refresh.
CPD Records Table¶
The CpdRecordsTable component renders all CPD records for the current year:
Empty state: When no records exist, shows EmptyState component with:
- BookOpen icon
- "No CPD entries yet" title
- "You haven't logged any CPD activities for the current cycle. Click 'Add CPD Entry' to get started." description
With records: Card with "CPD Log History" heading and horizontally scrollable table:
| Column | Content |
|---|---|
| Course | Course name (bold) + IBF course code badge (info variant, small) if present |
| Provider | Provider name or "—" |
| Date Completed | SG-locale date |
| Ethics | Hours formatted to 1 decimal place, or "—" if 0 |
| Product | Hours formatted to 1 decimal place, or "—" if 0 |
| Other | Hours formatted to 1 decimal place, or "—" if 0 |
Records are sorted by completedAt descending (newest first).
No-Profile State¶
If the user has no rep profile (!profileLoading && !repProfileId), the page renders:
- PageHeader (same as normal)
- EmptyState: "No Rep Profile Found" with BookOpen icon + description "Your representative profile has not been created yet. Please contact your administrator or HR to set up your profile before logging CPD activities."
FAA-N26 CPD Thresholds¶
The page displays one threshold warning:
- Core CPD minimum: 6.0 hours per year (ethics/rules/regulations, IBF/SCI-accredited), shown via conditional warning banner when ethicsHours < 6.0 and ethicsHours > 0
The page does not display the full FAA-N26 thresholds (30h/year total: 6h Core CPD + 24h Supplementary CPD) as visual progress bars. Only the Core CPD minimum check is implemented in the UI. The draft_cpd_reminder chatbot tool (in FastAPI) can generate reminder emails that reference all FAA-N26 thresholds.
GraphQL Data Sources¶
| Query | Purpose | Parameters | Role Guard |
|---|---|---|---|
repProfileByUser(userId) |
Get rep profile for current user | userId: String (Keycloak sub) |
@Roles("FI_USER", "REP_USER", "COMP", "HR_ADMIN", "ORG_ADMIN", "SYS_ADMIN") |
cpdRecordsForRep(repProfileId, cycleYear) |
Fetch CPD entries | repProfileId: Int, cycleYear: Int (optional) |
Frontend note: resolver has @ServiceOnly() — the frontend GraphQL call to this query may fail without SERVICE_INTERNAL_TOKEN |
cpdHoursTotalForRep(repProfileId, cycleYear) |
Get aggregated hours | repProfileId: Int, cycleYear: Int |
@Roles("FI_USER", "REP_USER", "COMP", "HR_ADMIN", "ORG_ADMIN", "SYS_ADMIN") |
createCpdRecord(input) |
Log a new CPD entry | CreateCpdRecordInput |
@Roles("FI_USER", "REP_USER", "COMP", "HR_ADMIN", "ORG_ADMIN", "SYS_ADMIN") |
CreateCpdRecordInput Shape¶
input CreateCpdRecordInput {
repProfileId: Int!
cycleYear: Int!
courseName: String!
providerName: String
ibfCourseCode: String
hoursEthics: Float # defaults to 0 if not provided
hoursProduct: Float # defaults to 0 if not provided
hoursOther: Float # defaults to 0 if not provided
completedAt: DateTime!
}
CpdHoursTotal Response Shape¶
type CpdHoursTotal {
ethics: Float!
product: Float!
other: Float!
total: Float!
}
Self-Service Capabilities¶
Representatives can: - View their own CPD records and YTD totals for the current year - Create new CPD entries (course name, provider, IBF code, hours by category, completion date) - See a warning when Core CPD hours fall below the 6.0 minimum (FAA-N26 §5.4(a)(i))
Representatives cannot: - Edit or delete existing CPD records (no update/delete mutation exposed on the frontend) - View CPD records for other representatives - Change the cycle year to a past or future year without admin assistance (frontend defaults to current year but allows manual override in the form input)
Related Pages¶
/rep— Rep Dashboard: overview with shortcuts to CPD Log, Passport, Attestations, Incident Disclosure/rep/passport— Rep Passport: includes CPD compliance status as part of the exported regulatory passport/admin/rep-register— Rep Register: admin view of all reps with R/A/G health badges computed from CMFAS + CPD status
Chatbot Interaction¶
The draft_cpd_reminder tool in FastAPI's chatbot_tools.py can draft reminder emails for CPD compliance. When a user on the CPD Log page asks about their CPD status, the chatbot should:
- Read the YTD totals from the page data if available via page context (Note: the page does not currently provide page context, so the chatbot may need to call get_cpd_status tool)
- Reference the FAA-N26 thresholds: 30h/year total: ≥6h Core CPD (ethics/rules, IBF/SCI-accredited) + ≥24h Supplementary CPD (FAA-N26 §5.4)
- Point out which category is below threshold and suggest specific courses/types
- The chatbot can call the draft_cpd_reminder tool which triggers a reminder email workflow (requires COMP or HR_ADMIN approval before sending)