This document provides a complete implementation plan for Angular frontend components to support the 5 new API controllers:
- BreakPolicy - Break policy management
- PayRuleSet - Pay rule set configuration
- PayDayTypeRule - Day type rules
- PayTierRule - Tier-based pay rules
- PayTimeBandRule - Time band rules
Estimated Effort: 80-100 hours total
Prerequisites: All backend APIs implemented and tested ✅
Status: Ready for implementation
After analyzing the eform-client folder, the following patterns are established:
src/app/plugins/modules/time-planning-pn/
├── components/ # Shared components
├── modules/ # Feature modules
│ ├── {entity}/
│ │ ├── components/
│ │ │ ├── {entity}-container/
│ │ │ ├── {entity}-table/
│ │ │ └── {entity}-actions/
│ │ ├── {entity}.module.ts
│ │ └── {entity}.routing.ts
├── models/ # TypeScript models
├── services/ # API services
├── i18n/ # Translations
├── enums/ # Enums
└── consts/ # Constants
Location: src/app/plugins/modules/time-planning-pn/services/
Pattern:
@Injectable({ providedIn: 'root' })
export class TimePlanningPn{Entity}Service {
constructor(private apiBaseService: ApiBaseService) {}
// CRUD methods returning Observable<OperationDataResult<T>>
getAll(model: RequestModel): Observable<OperationDataResult<Model[]>>
getById(id: number): Observable<OperationDataResult<Model>>
create(model: CreateModel): Observable<OperationResult>
update(model: UpdateModel): Observable<OperationResult>
delete(id: number): Observable<OperationResult>
}API Method Constants:
export let TimePlanningPn{Entity}Methods = {
Index: 'api/time-planning-pn/{entity}/index',
Get: 'api/time-planning-pn/{entity}',
// etc.
};Container Component (Smart):
- Service injection
- Data fetching
- State management
- Event handling
Table Component (Presentational):
- Uses
mtx-gridfrom@ng-matero/extensions - Input: data array
- Output: events (edit, delete, etc.)
- Column definitions with templates
Modal Components:
- Material Dialog (
MatDialog) - Form validation
- Reactive forms
- Data binding
Location: src/app/plugins/modules/time-planning-pn/models/{entity}/
Files per entity:
{entity}.model.ts- Full model{entity}-create.model.ts- Create DTO{entity}-update.model.ts- Update DTO{entity}-request.model.ts- List request with paging{entity}-list.model.ts- List response with paginationindex.ts- Barrel export
Create 6 model files:
break-policy.model.ts:
export interface BreakPolicyModel {
id: number;
name: string;
planningWorkdayCode: string;
rules: BreakPolicyRuleModel[];
createdAt: Date;
updatedAt: Date;
}
export interface BreakPolicyRuleModel {
id: number;
breakPolicyId: number;
dayOfWeek: number; // 0-6
paidBreakSeconds: number;
unpaidBreakSeconds: number;
}break-policy-create.model.ts:
export interface BreakPolicyCreateModel {
name: string;
planningWorkdayCode: string;
rules: BreakPolicyRuleCreateModel[];
}break-policy-update.model.ts:
export interface BreakPolicyUpdateModel {
id: number;
name: string;
planningWorkdayCode: string;
rules: BreakPolicyRuleUpdateModel[];
}break-policy-request.model.ts:
export interface BreakPolicyRequestModel {
offset: number;
pageSize: number;
sort?: string;
isSortDsc?: boolean;
}break-policy-list.model.ts:
export interface BreakPolicyListModel {
total: number;
breakPolicies: BreakPolicySimpleModel[];
}
export interface BreakPolicySimpleModel {
id: number;
name: string;
}index.ts: Export all models
export let TimePlanningPnBreakPoliciesMethods = {
Index: 'api/time-planning-pn/break-policies',
BreakPolicies: 'api/time-planning-pn/break-policies',
};
@Injectable({ providedIn: 'root' })
export class TimePlanningPnBreakPoliciesService {
constructor(private apiBaseService: ApiBaseService) {}
getAll(model: BreakPolicyRequestModel): Observable<OperationDataResult<BreakPolicyListModel>> {
return this.apiBaseService.get(TimePlanningPnBreakPoliciesMethods.Index, model);
}
getById(id: number): Observable<OperationDataResult<BreakPolicyModel>> {
return this.apiBaseService.get(`${TimePlanningPnBreakPoliciesMethods.BreakPolicies}/${id}`);
}
create(model: BreakPolicyCreateModel): Observable<OperationResult> {
return this.apiBaseService.post(TimePlanningPnBreakPoliciesMethods.BreakPolicies, model);
}
update(model: BreakPolicyUpdateModel): Observable<OperationResult> {
return this.apiBaseService.put(`${TimePlanningPnBreakPoliciesMethods.BreakPolicies}/${model.id}`, model);
}
delete(id: number): Observable<OperationResult> {
return this.apiBaseService.delete(`${TimePlanningPnBreakPoliciesMethods.BreakPolicies}/${id}`);
}
}Container Component (break-policies-container.component.ts):
- Manages state
- Fetches data
- Handles CRUD operations
- Pagination logic
Table Component (break-policies-table.component.ts):
- Displays data with mtx-grid
- Column definitions
- Action buttons (edit, delete)
- Event emitters
Modal Components:
break-policy-create-modal.component- Create dialogbreak-policy-edit-modal.component- Edit dialogbreak-policy-delete-modal.component- Delete confirmationbreak-policy-rules-edit.component- Weekday rules editor (nested)
break-policies.module.ts:
@NgModule({
imports: [
CommonModule,
TranslateModule,
FormsModule,
ReactiveFormsModule,
EformSharedModule,
RouterModule,
BreakPoliciesRouting,
MtxGridModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatDialogModule,
MatSelectModule,
MatTooltipModule,
],
declarations: [
BreakPoliciesContainerComponent,
BreakPoliciesTableComponent,
BreakPolicyCreateModalComponent,
BreakPolicyEditModalComponent,
BreakPolicyDeleteModalComponent,
BreakPolicyRulesEditComponent,
],
})
export class BreakPoliciesModule {}break-policies.routing.ts:
export const routes: Routes = [
{
path: '',
canActivate: [PermissionGuard],
component: BreakPoliciesContainerComponent,
data: {
requiredPermission: TimePlanningPnClaims.accessTimePlanningPlugin,
},
},
];time-planning-pn.routing.ts:
{
path: 'break-policies',
canActivate: [AuthGuard],
loadChildren: () =>
import('./modules/break-policies/break-policies.module').then(
(m) => m.BreakPoliciesModule
),
},pay-rule-set.model.ts:
export interface PayRuleSetModel {
id: number;
name: string;
payDayRules: PayDayRuleModel[];
createdAt: Date;
updatedAt: Date;
}
export interface PayDayRuleModel {
id: number;
payRuleSetId: number;
dayTypeCode: string; // SUNDAY, SATURDAY, WEEKDAY, HOLIDAY, etc.
payTiers: PayTierRuleModel[];
}
export interface PayTierRuleModel {
id: number;
payDayRuleId: number;
order: number;
upToSeconds: number | null;
payCode: string;
}pay-rule-set-create.model.ts, pay-rule-set-update.model.ts, etc.
Similar to BreakPolicy service pattern.
Key complexity: Nested editing of PayDayRules with PayTierRules
Components needed:
pay-rule-sets-container.componentpay-rule-sets-table.componentpay-rule-set-create-modal.componentpay-rule-set-edit-modal.componentpay-rule-set-delete-modal.componentpay-day-rules-editor.component- Nested editor for day rulespay-tier-rules-editor.component- Nested editor for tier rules
pay-day-type-rule.model.ts:
export interface PayDayTypeRuleModel {
id: number;
payRuleSetId: number;
dayType: string; // "Weekday", "Weekend", "Holiday"
timeBands: PayTimeBandRuleModel[];
}
export interface PayTimeBandRuleModel {
id: number;
payDayTypeRuleId: number;
startSecondOfDay: number;
endSecondOfDay: number;
payCode: string;
}Follow standard pattern.
pay-day-type-rules-container.componentpay-day-type-rules-table.componentpay-day-type-rule-create-modal.componentpay-day-type-rule-edit-modal.componentpay-day-type-rule-delete-modal.componentpay-time-bands-editor.component- Time band editor (with time pickers)
Special UI consideration: Time band editor needs time-of-day pickers
Note: May be integrated into PayRuleSet module as nested component rather than standalone module.
Already defined in PayRuleSet models.
If separate CRUD UI needed:
pay-tier-rules-container.componentpay-tier-rules-table.component(filtered by PayDayRuleId)- CRUD modals
Note: May be integrated into PayDayTypeRule module as nested component.
Already defined in PayDayTypeRule models.
If separate CRUD UI needed:
pay-time-band-rules-container.componentpay-time-band-rules-table.component(filtered by PayDayTypeRuleId)- CRUD modals with time pickers
Add to all language files (27 total):
// Break Policies
'Break Policies': 'Break Policies',
'Break Policy': 'Break Policy',
'Create Break Policy': 'Create Break Policy',
'Edit Break Policy': 'Edit Break Policy',
'Delete Break Policy': 'Delete Break Policy',
'Break Policy Name': 'Break Policy Name',
'Planning Workday Code': 'Planning Workday Code',
'Break Rules': 'Break Rules',
'Day of Week': 'Day of Week',
'Paid Break (seconds)': 'Paid Break (seconds)',
'Unpaid Break (seconds)': 'Unpaid Break (seconds)',
// Pay Rule Sets
'Pay Rule Sets': 'Pay Rule Sets',
'Pay Rule Set': 'Pay Rule Set',
'Create Pay Rule Set': 'Create Pay Rule Set',
'Edit Pay Rule Set': 'Edit Pay Rule Set',
'Delete Pay Rule Set': 'Delete Pay Rule Set',
'Rule Set Name': 'Rule Set Name',
'Day Rules': 'Day Rules',
'Day Type Code': 'Day Type Code',
'Pay Tiers': 'Pay Tiers',
'Tier Order': 'Tier Order',
'Up To (seconds)': 'Up To (seconds)',
'Pay Code': 'Pay Code',
// Pay Day Type Rules
'Pay Day Type Rules': 'Pay Day Type Rules',
'Day Type': 'Day Type',
'Weekday': 'Weekday',
'Weekend': 'Weekend',
'Holiday': 'Holiday',
'Time Bands': 'Time Bands',
'Start Time': 'Start Time',
'End Time': 'End Time',
// Pay Tier Rules
'Pay Tier Rules': 'Pay Tier Rules',
'Tier': 'Tier',
'Time Boundary': 'Time Boundary',
// Pay Time Band Rules
'Pay Time Band Rules': 'Pay Time Band Rules',
'Time Band': 'Time Band',
'Time Range': 'Time Range',
// Common
'Name': 'Name',
'Description': 'Description',
'Actions': 'Actions',
'Create': 'Create',
'Edit': 'Edit',
'Delete': 'Delete',
'Save': 'Save',
'Cancel': 'Cancel',
'Confirm': 'Confirm',
'Yes': 'Yes',
'No': 'No',
'Are you sure?': 'Are you sure?',
'Delete confirmation': 'Are you sure you want to delete this item?',enUS.tsda.tsdeDE.ts- ... (all 27 language files)
e2e/
├── Page objects/
│ └── TimePlanning/
│ ├── BreakPoliciesPage.ts
│ ├── PayRuleSetsPage.ts
│ ├── PayDayTypeRulesPage.ts
│ ├── PayTierRulesPage.ts
│ └── PayTimeBandRulesPage.ts
└── Tests/
└── time-planning-rules/
├── break-policies.spec.ts
├── pay-rule-sets.spec.ts
├── pay-day-type-rules.spec.ts
├── pay-tier-rules.spec.ts
└── pay-time-band-rules.spec.ts
Example: BreakPoliciesPage.ts
class BreakPoliciesPage {
// Selectors
get createButton() { return $('#create-break-policy-btn'); }
get tableRows() { return $$('.break-policy-row'); }
get nameInput() { return $('#break-policy-name'); }
get saveButton() { return $('#save-break-policy-btn'); }
get cancelButton() { return $('#cancel-break-policy-btn'); }
// Actions
async navigateTo() {
await browser.url('/time-planning/break-policies');
await this.createButton.waitForDisplayed({ timeout: 10000 });
}
async openCreateModal() {
await this.createButton.click();
await this.nameInput.waitForDisplayed({ timeout: 5000 });
}
async createBreakPolicy(name: string, workdayCode: string) {
await this.openCreateModal();
await this.nameInput.setValue(name);
// ... set other fields
await this.saveButton.click();
}
async getBreakPolicyByName(name: string) {
const rows = await this.tableRows;
for (const row of rows) {
const rowName = await row.$('.name-cell').getText();
if (rowName === name) return row;
}
return null;
}
async editBreakPolicy(name: string, newName: string) {
const row = await this.getBreakPolicyByName(name);
const editBtn = await row.$('.edit-btn');
await editBtn.click();
await this.nameInput.setValue(newName);
await this.saveButton.click();
}
async deleteBreakPolicy(name: string) {
const row = await this.getBreakPolicyByName(name);
const deleteBtn = await row.$('.delete-btn');
await deleteBtn.click();
const confirmBtn = await $('#confirm-delete-btn');
await confirmBtn.click();
}
}
export default new BreakPoliciesPage();describe('Break Policies Management', () => {
before(async () => {
await loginPage.open('/auth');
await loginPage.login();
});
it('should navigate to break policies page', async () => {
await breakPoliciesPage.navigateTo();
expect(await breakPoliciesPage.createButton.isDisplayed()).toBe(true);
});
it('should create a new break policy', async () => {
const name = `Test Policy ${Date.now()}`;
await breakPoliciesPage.createBreakPolicy(name, 'WORKDAY');
const row = await breakPoliciesPage.getBreakPolicyByName(name);
expect(row).not.toBeNull();
});
it('should create break policy with rules for all weekdays', async () => {
const name = `Policy With Rules ${Date.now()}`;
await breakPoliciesPage.openCreateModal();
await breakPoliciesPage.nameInput.setValue(name);
// Add rules for each day
for (let day = 0; day < 7; day++) {
await breakPoliciesPage.addRule(day, 1800, 1800); // 30min paid, 30min unpaid
}
await breakPoliciesPage.saveButton.click();
const row = await breakPoliciesPage.getBreakPolicyByName(name);
expect(row).not.toBeNull();
});
it('should edit an existing break policy', async () => {
const oldName = `Edit Test ${Date.now()}`;
const newName = `${oldName} - Updated`;
await breakPoliciesPage.createBreakPolicy(oldName, 'WORKDAY');
await breakPoliciesPage.editBreakPolicy(oldName, newName);
const row = await breakPoliciesPage.getBreakPolicyByName(newName);
expect(row).not.toBeNull();
});
it('should delete a break policy', async () => {
const name = `Delete Test ${Date.now()}`;
await breakPoliciesPage.createBreakPolicy(name, 'WORKDAY');
await breakPoliciesPage.deleteBreakPolicy(name);
const row = await breakPoliciesPage.getBreakPolicyByName(name);
expect(row).toBeNull();
});
it('should validate required fields', async () => {
await breakPoliciesPage.openCreateModal();
await breakPoliciesPage.saveButton.click();
const errorMessage = await $('#name-error');
expect(await errorMessage.isDisplayed()).toBe(true);
expect(await errorMessage.getText()).toContain('required');
});
it('should paginate results', async () => {
// Create 50+ policies
for (let i = 0; i < 55; i++) {
await breakPoliciesPage.createBreakPolicy(`Pagination Test ${i}`, 'WORKDAY');
}
await breakPoliciesPage.navigateTo();
const rows = await breakPoliciesPage.tableRows;
expect(rows.length).toBeLessThanOrEqual(50); // Default page size
const nextPageBtn = await $('#next-page-btn');
expect(await nextPageBtn.isDisplayed()).toBe(true);
await nextPageBtn.click();
await browser.pause(500);
const rowsPage2 = await breakPoliciesPage.tableRows;
expect(rowsPage2.length).toBeGreaterThan(0);
});
it('should sort by name', async () => {
await breakPoliciesPage.navigateTo();
const nameHeader = await $('#name-header');
await nameHeader.click();
await browser.pause(500);
const rows = await breakPoliciesPage.tableRows;
const names = await Promise.all(rows.map(r => r.$('.name-cell').getText()));
const sorted = [...names].sort();
expect(names).toEqual(sorted);
});
});describe('Pay Rule Sets Management', () => {
it('should create pay rule set with day rules', async () => {
const name = `Rule Set ${Date.now()}`;
await payRuleSetsPage.openCreateModal();
await payRuleSetsPage.nameInput.setValue(name);
// Add SUNDAY rule
await payRuleSetsPage.addDayRule('SUNDAY');
await payRuleSetsPage.addTierToDay('SUNDAY', 1, 39600, 'SUN_80'); // First 11h at 80%
await payRuleSetsPage.addTierToDay('SUNDAY', 2, null, 'SUN_100'); // Rest at 100%
// Add SATURDAY rule
await payRuleSetsPage.addDayRule('SATURDAY');
await payRuleSetsPage.addTierToDay('SATURDAY', 1, null, 'SAT_50');
await payRuleSetsPage.saveButton.click();
const row = await payRuleSetsPage.getByName(name);
expect(row).not.toBeNull();
});
it('should edit day rules in existing rule set', async () => {
// Test editing nested day rules
});
it('should validate tier order', async () => {
// Test tier order validation
});
it('should validate tier boundaries', async () => {
// Test upToSeconds validation (order, non-overlapping)
});
});describe('Pay Day Type Rules Management', () => {
it('should create day type rule with time bands', async () => {
const name = `Day Type Rule ${Date.now()}`;
await payDayTypeRulesPage.openCreateModal();
await payDayTypeRulesPage.selectDayType('Weekend');
// Add time band: 00:00-18:00
await payDayTypeRulesPage.addTimeBand(0, 64800, 'WEEKEND_DAY'); // 0 to 18:00
// Add time band: 18:00-23:59
await payDayTypeRulesPage.addTimeBand(64800, 86399, 'WEEKEND_EVENING'); // 18:00 to 23:59
await payDayTypeRulesPage.saveButton.click();
const row = await payDayTypeRulesPage.getByDayType('Weekend');
expect(row).not.toBeNull();
});
it('should validate time band overlaps', async () => {
// Test that overlapping time bands are rejected
});
it('should use time pickers for band boundaries', async () => {
// Test time picker UI
});
});Cross-entity relationships:
describe('Rule Engine Integration', () => {
it('should link break policy to workday planning', async () => {
// Create break policy
// Use it in planning
// Verify application
});
it('should link pay rule set to pay calculation', async () => {
// Create pay rule set with tiers
// Apply to work registration
// Verify pay lines generated correctly
});
it('should apply day type rules correctly', async () => {
// Create day type rule for Sunday
// Register Sunday work
// Verify correct pay code applied
});
});Each entity needs validation tests for:
- Required fields
- Unique constraints (e.g., unique names)
- Range validations (e.g., seconds must be positive)
- Relationship validations (e.g., can't delete referenced entity)
- Business rule validations (e.g., tier order, time band overlaps)
MatFormFieldModule- Form fieldsMatInputModule- Input controlsMatButtonModule- ButtonsMatIconModule- IconsMatDialogModule- Dialogs/modalsMatSelectModule- DropdownsMatTooltipModule- TooltipsMatDatepickerModule- Date pickers (if needed)MatCheckboxModule- CheckboxesMatTableModule- Tables (alternative to mtx-grid)
@ng-matero/extensions- mtx-grid for data tablesngx-auto-unsubscribe- Memory leak prevention@ngx-translate/core- i18n
EformSharedModule- Shared components/directivesCommonModule- Angular commonFormsModule- Template formsReactiveFormsModule- Reactive formsRouterModule- Routing
- Models - All model files for all 5 entities
- Services - All API services
- i18n - Translation keys (at least enUS)
- BreakPolicy - Complete module (simplest, no deep nesting)
- PayDayTypeRule - Medium complexity (time bands)
- PayRuleSet - Most complex (nested day rules + tier rules)
- PayTierRule - If standalone (otherwise part of PayRuleSet)
- PayTimeBandRule - If standalone (otherwise part of PayDayTypeRule)
- Routing - Integrate all modules into main routing
- Cypress Tests - Complete E2E test suite
- Manual Testing - End-to-end workflows
- Bug Fixes - Address issues found
- UI/UX - Refine user interface
- Translations - Complete all 27 languages
- Documentation - User guide
- Performance - Optimize loading/rendering
- Models: 6 files
- Service: 1 file
- Components: 5-8 files (.ts, .html, .scss)
- Tests: 3-5 files
- Module/Routing: 2 files
Total per entity: ~20-25 files
- Total: 100-125 files
- i18n: 27 files (updated)
- Cypress tests: 10+ files
- Shared components: 5-10 files
Grand Total: ~150 files
- ✅ All CRUD operations work for all 5 entities
- ✅ Pagination works correctly
- ✅ Sorting works on all columns
- ✅ Search/filter works where applicable
- ✅ Nested editing works (day rules, tier rules, time bands)
- ✅ Validation prevents invalid data entry
- ✅ Error messages are user-friendly
- ✅ Success messages confirm actions
- ✅ All pages load in < 2 seconds
- ✅ No memory leaks (Auto-unsubscribe working)
- ✅ Responsive design (mobile-friendly)
- ✅ Accessible (WCAG 2.1 AA)
- ✅ All translations complete
- ✅ Consistent UI/UX with existing modules
- ✅ All Cypress tests pass
- ✅ E2E coverage for all workflows
- ✅ Validation tests pass
- ✅ Integration tests pass
- ✅ Manual testing complete
Risk: PayRuleSet with nested day rules and tier rules is complex
Mitigation:
- Build BreakPolicy first (simpler nested structure)
- Use learnings for PayRuleSet
- Consider wizard-style UI for complex creation
Risk: Time-of-day editing (seconds since midnight) is not user-friendly
Mitigation:
- Use Material time pickers
- Convert seconds to HH:MM format in UI
- Provide helper text and validation
Risk: 27 languages × 100+ strings = 2700+ translation entries
Mitigation:
- Start with enUS (English) only
- Use translation service for others
- Crowdsource from community
- Phase translations (core languages first)
Risk: Large rule sets may have performance issues
Mitigation:
- Server-side pagination
- Virtual scrolling for large lists
- Lazy loading of nested data
- Caching strategies
- Functionality: 100% of API endpoints have UI coverage
- Quality: 0 critical bugs at launch
- Performance: < 2s page load time
- Test Coverage: > 80% Cypress test coverage
- User Satisfaction: Positive feedback from QA/users
- Review this plan with development team
- Estimate resources (1-2 developers × 5-6 weeks)
- Prioritize entities (if phased rollout desired)
- Set up project tracking (Jira/GitHub issues)
- Begin Phase 1 (Foundation - models, services, i18n)
- Should PayTierRule and PayTimeBandRule be standalone modules or integrated into parent modules?
- What is the priority order of the 5 entities?
- Is there existing UI design/mockups?
- What languages are priority for translations?
- Are there performance requirements (users, data volume)?
This plan provides a complete roadmap for implementing Angular frontend components for the rule engine API. The estimated effort is 80-100 hours (2-2.5 months for 1 developer, or 1-1.5 months for 2 developers working in parallel).
The implementation follows established patterns in the codebase, minimizing architectural risk. The phased approach allows for incremental delivery and testing.
Status: ✅ Ready for implementation