Saltar al contenido principal

Activity-Scoped Templates Feature Documentation

Overview​

This document describes the implementation of activity-scoped templates, allowing organizations to define custom Handlebars templates for individual processing activities or the entire organization.

Date: 2026-01-02 Version: 1.0


Table of Contents​

  1. Feature Summary
  2. Architecture Changes
  3. Database Schema Changes
  4. Component Documentation
  5. Service Layer Changes
  6. UI/UX Changes
  7. Usage Guide
  8. Migration Notes
  9. API Reference

Feature Summary​

What Changed​

Before:

  • Organizations could have only one template (stored as templateId field)
  • Template applied to all activities organization-wide

After:

  • Organizations can have multiple templates stored in a templates array
  • Each template is associated with an activityId:
    • activityId = 0: Full organization template (default)
    • activityId > 0: Specific activity template
  • Template Editor UI includes an ActivityPicker to select scope
  • Automatic fallback: activity template β†’ organization template β†’ default file template

Architecture Changes​

High-Level Flow​

User opens Template Editor
↓
Selects activity via ActivityPicker (or defaults to "Full organization")
↓
System loads template:
1. Check if template exists for specific activityId
2. If not, fallback to activityId=0 template (if available)
3. If not, load default .hbs file from filesystem
↓
User edits and saves template
↓
Template saved with associated activityId in organization.templates array

Template Loading Priority​

Templates are NOT locale-specific in the organization's templates array. Each Template document has its own locale field.

Loading hierarchy (by activityId only):

  1. Specific Activity Template (activityId = N): Database template for the specific activity
  2. Organization Template (activityId = 0): Database template for full organization (if no activity-specific template exists)
  3. Hardcoded Template File: Always exists for all supported locales in templates/activity-declaration-{locale}.hbs

Database Schema Changes​

Template Model​

File: src/models/template.js

Updated Schema (2026-01-10)​

A new description field has been added to the Template model to provide human-readable context for each template:

description: {
type: String,
required: true,
trim: true,
}

Description Values:

  • Organization-level template: "Organization generic activity template"
  • Activity-specific template: "Activity {activityId} template"

Note: The activityId field is retained in the Template model for backward compatibility and consistency with the organization's templates array lookup system.

Organization Model​

File: src/models/organization.js

Old Schema​

templateId: {
type: mongoose.Schema.Types.ObjectId,
required: false,
}

New Schema​

templates: {
type: [
{
activityId: {
type: Number,
required: true,
},
templateId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
},
},
],
default: [],
}

Schema Version Update​

Schema version incremented from 10 to 11 to track this migration.

Example Document Structure​

{
"_id": "507f1f77bcf86cd799439011",
"shortName": "acme-corp",
"schemaVersion": 11,
"templates": [
{
"activityId": 0,
"templateId": "507f1f77bcf86cd799439012"
},
{
"activityId": 5,
"templateId": "507f1f77bcf86cd799439013"
},
{
"activityId": 12,
"templateId": "507f1f77bcf86cd799439014"
}
],
// ... other fields
}

Component Documentation​

ActivityPicker Component​

File: src/lib/ui/pickers/ActivityPicker.jsx

Purpose​

Allows users to select between:

  • Full organization (activityId = 0)
  • Any specific processing activity from the ROPA

Props​

PropTypeRequiredDefaultDescription
activitiesArrayNo[]List of available activities with OU information
currentActivityIdNumberNo0Currently selected activity ID
onActivityChangeFunctionNo-Callback when selection changes: (activityId, activity) => void
triggerFunctionNo-Custom trigger component: (triggerProps, displayText, icon) => ReactNode
textColorStringNo"inherit"Color for trigger text/icon

Activity Object Structure​

Each activity in the activities array should have:

{
activityId: Number, // Unique activity ID
activityName: String, // Display name
activityIcon: String, // Icon name from RopaIcon set
ouId: Number, // Parent OU ID
ouName: String, // Parent OU name
ouColor: String, // OU color (hex code) - used as background
}

Features​

  • Color-Coded Menu Items: Each activity displays with its OU's background color
  • Icon Support: Shows activity icon (or fallback to BusinessIcon)
  • Grouped Sorting: Activities sorted by OU name, then activity name
  • Full Organization Option: Special menu item at the top with activityId = 0
  • Localized Labels: Uses next-intl for internationalization

Usage Example​

import { ActivityPicker } from "@/lib/ui/pickers";

function MyComponent({ activities }) {
const [currentActivityId, setCurrentActivityId] = useState(0);

return (
<ActivityPicker
activities={activities}
currentActivityId={currentActivityId}
onActivityChange={(activityId, activity) => {
console.log("Selected:", activityId, activity);
setCurrentActivityId(activityId);
}}
/>
);
}

Service Layer Changes​

Template Service​

File: src/services/templateService.js

createTemplate() - Updated (2026-01-10)​

Now requires a description field:

createTemplate({
orgId,
ouId?,
activityId?,
locale,
handlebars,
description // REQUIRED: Human-readable template description
})

Behavior:

  • For organization-level templates (no ouId):
    1. Removes existing template with same activityId from templates array
    2. Pushes new template entry { activityId, templateId }
    3. Description is auto-generated: "Organization generic activity template" (activityId=0) or "Activity N template" (activityId>0)
    4. Defaults to activityId = 0 if not specified

updateTemplate() - Updated (2026-01-10)​

Now accepts description as an updatable field:

updateTemplate(templateId, {
locale?,
handlebars?,
description? // NEW: Can update template description
})

deleteTemplate() - Updated​

Behavior:

  • For organization-level templates:
    • Uses $pull to remove matching entry from templates array
    • Searches by templateId

Template Actions (Helper Functions)​

File: src/lib/mongoose/templateActions.js

New Functions​

loadTemplateByActivityId(activityId, locale)​

Loads template with fallback logic:

loadTemplateByActivityId(5, 'en')

Returns:

{
isError: false,
data: {
template: "...", // Handlebars template content
source: "database", // "database" | "database-fallback" | "default" | "default-fallback"
templateId: ObjectId, // Template ID (or null if using fallback/default)
activityId: 5 // The requested activityId
}
}

Loading Priority:

  1. Template with exact activityId
  2. Template with activityId = 0 (if requested activityId !== 0)
  3. Default file template for locale
  4. Default English template
saveTemplateByActivityId(templateContent, locale, activityId)​

Saves template for a specific activity scope:

saveTemplateByActivityId(templateContent, 'en', 5)

Behavior:

  • If template exists for activityId: Updates it
  • Otherwise: Creates new template with orgTemplateActivityId = activityId

Deprecated Functions​

  • loadOrganizationTemplate(locale) β†’ Use loadTemplateByActivityId(0, locale)
  • saveOrganizationTemplate(templateContent, locale) β†’ Use saveTemplateByActivityId(templateContent, locale, 0)

UI/UX Changes​

Template Editor Modal​

File: src/lib/ui/modals/TemplateEditorModal.jsx

Changes​

  1. Replaced EditNoteIcon with ActivityPicker in dialog title
  2. New Props:
    • activities: Array of available activities
    • currentActivityId: Selected activity scope
  3. Updated Handlers:
    • handleSave(): Now passes activityId to save action
    • handleRefresh(): Now passes activityId to refresh action
    • handleActivityChange(): Navigates to new URL with ?activityId=N parameter

Activity Change Flow​

When user selects a different activity:

User clicks ActivityPicker
↓
Selects new activity (e.g., activityId=5)
↓
handleActivityChange(5) called
↓
Router navigates to /e/org/templates?activityId=5
↓
Page reloads with new template data

Template Page​

File: src/app/[locale]/(app)/e/org/templates/page.jsx

Changes​

  1. Accepts searchParams: Reads activityId from URL query parameter
  2. Loads All Activities: Calls getAllActivities() to fetch from ROPA
  3. Uses New Functions:
    • loadTemplateByActivityId(activityId, locale)
    • saveTemplateByActivityId(template, locale, activityId)
    • refreshTemplateAction(locale, activityId)

Server Actions Updated​

All server actions now include activityId:

const saveTemplateAction = async (template, locale, activityId) => {
"use server";
const result = await saveTemplateByActivityId(template, locale, activityId);
return toPlain(result);
};

Usage Guide​

For End Users​

Editing Organization-Wide Template​

  1. Navigate to Organization Settings β†’ Templates
  2. ActivityPicker defaults to "Full organization" (activityId = 0)
  3. Edit template and click Save
  4. Template applies to all activities without specific templates

Creating Activity-Specific Template​

  1. Navigate to Organization Settings β†’ Templates
  2. Click ActivityPicker (shows current activity/organization name with icon)
  3. Select a specific activity from the dropdown
  4. Template Editor loads:
    • Existing template for that activity (if exists)
    • OR copy of organization template (if exists)
    • OR default template file
  5. Edit and save
  6. Template now applies only to that specific activity

Template Source Indicators​

The template editor shows the source of the loaded template:

SourceMessage
database"Loaded from custom template"
database-fallback"Loaded from organization template (copied for this activity)"
default"Loaded from hardcoded template ({locale})"

For Developers​

Adding Activity Support to New Features​

  1. Import helper functions:
import {
loadTemplateByActivityId,
saveTemplateByActivityId
} from "@/lib/mongoose";
  1. Load template:
const activityId = 5; // or 0 for organization
const result = await loadTemplateByActivityId(activityId, locale);

if (!result.isError) {
const { template, source, templateId } = result.data;
// Use template
}
  1. Save template:
const result = await saveTemplateByActivityId(
templateContent,
locale,
activityId
);

Migration Notes​

Migrating Existing Organizations​

Organizations with the old templateId field will continue to work but won't appear in the new ActivityPicker until migrated.

async function migrateOrganizationTemplates() {
const orgs = await OrganizationModel.find({
templateId: { $exists: true, $ne: null }
});

for (const org of orgs) {
await OrganizationModel.updateOne(
{ _id: org._id },
{
$set: {
templates: [
{
activityId: 0,
templateId: org.templateId
}
],
schemaVersion: 11
},
$unset: { templateId: "" }
}
);
}
}

Backward Compatibility​

  • Old loadOrganizationTemplate() still works (marked as deprecated)
  • Old saveOrganizationTemplate() still works (marked as deprecated)
  • Template Editor gracefully handles missing templates array

API Reference​

Localization Keys​

File: messages/en.json

{
"activityPicker": {
"changeActivity": "Change activity or organization scope",
"fullOrganization": "Full organization",
"noActivities": "No activities available",
"selectActivity": "Select activity",
"title": "Select Template Scope",
"unknownOu": "Unknown OU",
"unnamedActivity": "Unnamed activity"
}
}

File Changes Summary​

FileChangesLines Changed
src/models/organization.jsSchema update: templateId β†’ templates array~20
src/services/templateService.jsUpdated createTemplate() and deleteTemplate()~40
src/lib/mongoose/templateActions.jsAdded loadTemplateByActivityId(), saveTemplateByActivityId()~180
src/app/[locale]/(app)/e/org/templates/page.jsxAdded activities loading, activityId support~90
src/lib/ui/modals/TemplateEditorModal.jsxIntegrated ActivityPicker, updated handlers~30
src/lib/ui/pickers/ActivityPicker.jsxNew component~250
src/lib/ui/pickers/index.jsAdded export1
messages/en.jsonAdded activityPicker section7

Total: ~618 lines changed/added


Testing Checklist​

Functional Testing​

  • Load template for organization (activityId=0)
  • Load template for specific activity
  • Save template for organization
  • Save template for specific activity
  • Switch between activities using ActivityPicker
  • Verify fallback to organization template when activity template doesn't exist
  • Verify fallback to default file when no database templates exist
  • Delete activity-specific template
  • Delete organization template
  • Verify template preview works for all scopes

UI/UX Testing​

  • ActivityPicker displays "Full organization" by default
  • ActivityPicker shows all activities with correct names and icons
  • Activity items display with correct OU colors
  • Clicking activity navigates to correct URL with ?activityId parameter
  • Template source label displays correctly
  • Modal title shows selected activity name

Edge Cases​

  • Organization with no templates (uses default file)
  • Organization with only activity-specific templates (no activityId=0)
  • Activity with no activities in ROPA
  • Invalid activityId in URL parameter
  • Missing locale template file (fallback to English)

Bug Fixes and Enhancements (2026-01-03)​

Critical Bug Fix: Template Save from Validation Modal​

Issue: When saving a template through the validation modal (after sanitization), the currentActivityId was not being passed to the save function, causing templates to be saved with activityId=0 regardless of the selected activity.

Root Cause: The handleSaveFromModal function in TemplateEditorModal.jsx was only passing two parameters (template, locale) instead of three (template, locale, currentActivityId).

Fix: Updated handleSaveFromModal to pass currentActivityId as the third parameter:

// Before
const result = await onSave(template, locale);

// After
const result = await onSave(template, locale, currentActivityId);

Files Changed:

  • src/lib/ui/modals/TemplateEditorModal.jsx (line 408)

ActivityPicker Enhancements​

1. Organization Icon Position​

Change: BusinessIcon now appears on the left side of "Full organization" text in the trigger button, while activity icons remain on the right.

Implementation:

  • Added iconPosition property to currentActivityData state
  • Updated defaultTrigger to conditionally render icon based on position
  • Full organization: iconPosition: "left", mr: 1 (margin-right)
  • Activities: iconPosition: "right", ml: 1 (margin-left)

2. Alphabetical Sorting​

Change: Activities are now sorted alphabetically by activity name only (previously sorted by OU name first, then activity name).

Implementation:

// Before
.sort((a, b) => {
const ouCompare = (a.ouName || "").localeCompare(b.ouName || "");
if (ouCompare !== 0) return ouCompare;
return (a.activityName || "").localeCompare(b.activityName || "");
})

// After
.sort((a, b) => {
return (a.activityName || "").localeCompare(b.activityName || "");
})

3. Organization Name Display​

Change: "Full organization" menu item now displays the organization name as a caption (where OU name appears for regular activities).

Implementation:

  • Optimized data fetching: Combined getAllActivities and organization fetch into single getAllActivitiesAndOrg function
  • Returns both activities array and organization name
  • Passes organizationName prop through TemplateEditorModal to ActivityPicker
  • Full organization menu item shows organization name in Typography caption

Structure:

{/* Full Organization */}
<MenuItem>
<BusinessIcon />
<Box>
<Typography variant="body2">Full organization</Typography>
<Typography variant="caption">{organizationName}</Typography>
</Box>
</MenuItem>

{/* Activities */}
<MenuItem>
<ActivityIcon />
<Box>
<Typography variant="body2">{activityName}</Typography>
<Typography variant="caption">{ouName}</Typography>
</Box>
</MenuItem>

4. Icon Display Rules​

Change: BusinessIcon is now shown ONLY for the "Full organization" option. Activities without an icon show no icon (previously showed BusinessIcon as fallback).

Implementation:

  • Full organization: Always shows BusinessIcon
  • Activities with icon: Shows their specific icon via ShowIcon component
  • Activities without icon: Shows null (no icon)

Files Changed:

  • src/lib/ui/pickers/ActivityPicker.jsx (lines 63-80, 175-187, 214-216)
  • src/app/[locale]/(app)/e/org/templates/page.jsx (lines 70-118, 133)
  • src/lib/ui/modals/TemplateEditorModal.jsx (lines 70-71, 635)

Performance Optimization​

Change: Eliminated duplicate organization data fetching in page.jsx.

Before:

  • getAllActivities() fetched organization internally
  • Page component fetched organization again for organization name
  • Result: 2 database queries for same data

After:

  • getAllActivitiesAndOrg() returns both activities and organization name
  • Single database query
  • Improved performance and reduced database load

Schema Enhancement (2026-01-10)​

Template Model: Adding description field​

Motivation: Improve template management by adding human-readable descriptions alongside the existing activityId field for better clarity and logging.

Changes Made​

  1. Template Model Schema (src/models/template.js):

    • Added description field (required, trimmed string)
    • Kept activityId field for backward compatibility
    • Description values: "Organization generic activity template" or "Activity {N} template"
  2. Service Layer Updates (src/services/templateService.js):

    • createTemplate(): Now requires description parameter
    • updateTemplate(): Added description to allowed update fields
    • saveTemplate(): Auto-generates description based on activityId parameter
  3. Template Actions Updates (src/lib/mongoose/templateActions.js):

    • saveTemplateByActivityId(): Auto-generates description before creating/updating templates
    • saveOrganizationTemplate(): Auto-generates organization template description

Backward Compatibility​

  • The activityId field is retained in the Template model
  • The organization model's templates array still uses activityId as the reference key
  • No changes required to UI components or API routes
  • Description is automatically generated from activityId during save operations

Migration Notes​

Existing templates in the database will need to be migrated to add the description field. A migration script should:

// Example migration for existing templates
const templates = await TemplateModel.find({});

for (const template of templates) {
const description = template.activityId === 0
? "Organization generic activity template"
: `Activity ${template.activityId} template`;

await TemplateModel.updateOne(
{ _id: template._id },
{ $set: { description } }
);
}

Future Enhancements​

Potential Features​

  1. OU-Level Templates: Extend to support templates at OU level

    • Already supported in Template model schema
    • Would require similar picker for OU selection
  2. Template Inheritance Visualization: Show template hierarchy

    • Visual indicator of which template is being used (inherited vs. custom)
    • Tree view showing template relationships
  3. Bulk Template Operations:

    • Apply organization template to multiple activities
    • Reset activity templates to organization default
    • Export/import templates
  4. Template Versioning:

    • Keep history of template changes
    • Rollback to previous versions
    • Compare versions
  5. Template Preview Per Activity:

    • When editing organization template, preview how it renders for different activities
    • Side-by-side comparison

Troubleshooting​

Common Issues​

ActivityPicker shows no activities​

Cause: No activities in ROPA for current locale Solution: Ensure ROPA has activities created; check locale matches

Template doesn't save​

Cause: Missing activityId in save action Solution: Verify onSave callback receives all three parameters: (template, locale, activityId)

URL parameter not working​

Cause: searchParams not passed to Page component Solution: Ensure Next.js page component receives searchParams prop:

const Page = async ({ params, searchParams }) => {
// ...
}

Template loads default instead of custom​

Cause: Template not properly saved to database Solution: Check MongoDB templates array in organization document; verify templateId references exist


Support​

For questions or issues, please refer to:

  • Code Repository: Check recent commits for implementation details
  • Schema Documentation: Review src/models/organization.js comments
  • Component Examples: See src/lib/ui/pickers/OuPicker.jsx for similar patterns

Document Version: 1.2 Last Updated: 2026-01-10 Author: Claude Code Implementation Changelog:

  • v1.2 (2026-01-10): Added description field to Template model, updated all services to generate and pass descriptions
  • v1.1 (2026-01-03): Added bug fixes and enhancements section, documented ActivityPicker improvements
  • v1.0 (2026-01-02): Initial documentation