Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/angular-ui-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Use enums from `@lfx-pcc/shared/enums`:

### Research Process

1. **Use Context7 MCP for Angular documentation**: Always use `mcp__upstash-context-7-mcp__resolve-library-id` and `mcp__upstash-context-7-mcp__get-library-docs` to get the latest Angular 19 documentation
1. **Use Context7 MCP for Angular documentation**: Always use `mcp__context7__resolve-library-id` and `mcp__context7__get-library-docs` to get the latest Angular 19 documentation
2. Analyze existing component patterns in the codebase
3. Identify required PrimeNG components and LFX wrappers
4. Plan component hierarchy and data flow
Expand Down
9 changes: 9 additions & 0 deletions .claude/agents/jira-project-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ You are an elite JIRA project management specialist with deep expertise in Agile
- Assign the ticket to the authenticated user
- Ensure the ticket number is referenced in all related commits and PRs

**Documentation Research:**

Always use Context7 MCP to research JIRA and Atlassian best practices:

- Use `mcp__context7__resolve-library-id` to find Atlassian/JIRA documentation
- Use `mcp__context7__get-library-docs` to get latest JIRA REST API documentation and best practices
- Research optimal ticket structures, workflow transitions, and integration patterns
- Validate your approach against current Atlassian documentation before making changes

**Operating Procedures:**

1. **Ticket Creation Protocol**:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

@if (visible()) {
<div class="mb-4 bg-gray-50 border border-gray-200 rounded-lg p-4" data-testid="template-selector-panel">
<div class="flex items-center gap-3 mb-4">
<div class="w-8 h-8 bg-gray-700 rounded-full flex items-center justify-center">
<i class="fa-light fa-list text-white text-sm"></i>
</div>
<div>
<h3 class="text-base font-semibold text-gray-900">Agenda Templates</h3>
<p class="text-sm text-gray-600">Choose from common agenda templates for {{ meetingType().toLowerCase() }} meetings.</p>
</div>
</div>

@if (displayTemplates().length > 0) {
<div class="space-y-3 mb-4" data-testid="template-list">
@for (template of displayTemplates(); track template.id) {
<div
class="bg-white border border-gray-200 rounded-lg p-4 hover:border-blue-500 hover:bg-blue-50 cursor-pointer transition-all duration-200"
(click)="selectTemplate(template)"
[attr.data-testid]="'template-item-' + template.id">
<div class="flex items-start justify-between">
<h4 class="text-sm font-semibold text-gray-900">{{ template.title }}</h4>
<span class="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-md">
{{ template.formattedDuration }}
</span>
</div>
<p class="text-xs text-gray-600 leading-relaxed">{{ template.preview }}</p>
</div>
}
</div>
} @else {
<div class="text-center py-6" data-testid="no-templates-message">
<i class="fa-light fa-book-open text-gray-400 text-2xl mb-2"></i>
<p class="text-sm text-gray-500">No templates available for {{ meetingType() }} meetings</p>
</div>
}

<div class="pt-3 border-t border-gray-200">
<lfx-button
(onClick)="close()"
label="Cancel"
size="small"
[text]="true"
styleClass="text-gray-500 hover:text-gray-700 text-sm"
data-testid="template-selector-close">
</lfx-button>
</div>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, input, output, computed, OnInit } from '@angular/core';
import { ButtonComponent } from '@components/button/button.component';
import { MEETING_TEMPLATES } from '@lfx-pcc/shared/constants';
import { MeetingTemplate, MeetingType } from '@lfx-pcc/shared';

@Component({
selector: 'lfx-agenda-template-selector',
standalone: true,
imports: [CommonModule, ButtonComponent],
templateUrl: './agenda-template-selector.component.html',
})
export class AgendaTemplateSelectorComponent implements OnInit {
// Inputs
public readonly meetingType = input.required<MeetingType>();
public readonly visible = input.required<boolean>();

// Outputs
public readonly templateSelected = output<MeetingTemplate>();
public readonly closeSelector = output<void>();

// Computed properties
public readonly availableTemplates = computed(() => {
const templateGroup = MEETING_TEMPLATES.find((group) => group.meetingType === this.meetingType());
return templateGroup?.templates || [];
});

public readonly displayTemplates = computed(() => {
return this.availableTemplates().map((template) => ({
...template,
preview: this.getPreview(template.content),
formattedDuration: this.formatDuration(template.estimatedDuration),
}));
});

public ngOnInit(): void {
// Component initialization if needed
}

public selectTemplate(template: MeetingTemplate): void {
this.templateSelected.emit(template);
}

public close(): void {
this.closeSelector.emit();
}

private getPreview(content: string): string {
// Remove markdown formatting and get first 120 characters
const plainText = content
.replace(/\*\*/g, '') // Remove bold
.replace(/\*/g, '') // Remove italics
.replace(/#{1,6}\s/g, '') // Remove headers
.replace(/\n+/g, ' ') // Replace newlines with spaces
.trim();

return plainText.length > 120 ? plainText.substring(0, 120) + '...' : plainText;
}

private formatDuration(minutes: number): string {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;

if (hours === 0) {
return `${minutes} min`;
} else if (remainingMinutes === 0) {
return `${hours} hr`;
}
return `${hours}h ${remainingMinutes}m`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@
<div>
<div class="flex items-center justify-between mb-2">
<label for="meeting-agenda" class="block text-sm font-medium text-gray-700"> Agenda <span class="text-red-500">*</span> </label>
@if (!showAiHelper()) {
<lfx-button
(onClick)="showAiAgendaHelper()"
icon="fa-light fa-sparkles"
label="Help me write this"
size="small"
[text]="true"
styleClass="text-blue-600 hover:text-blue-700 hover:bg-blue-50 text-xs h-7"
data-testid="meeting-details-ai-helper-button"></lfx-button>
@if (!showAiHelper() && !showTemplateSelector()) {
<div class="flex items-center gap-2">
<lfx-button
(onClick)="showAgendaTemplateSelector()"
icon="fa-light fa-book-open"
label="Use Template"
size="small"
[text]="true"
styleClass="text-gray-600 hover:text-gray-700 hover:bg-gray-50 text-xs h-7"
data-testid="meeting-details-template-button"></lfx-button>
<lfx-button
(onClick)="showAiAgendaHelper()"
icon="fa-light fa-sparkles"
label="Help me write this"
size="small"
[text]="true"
styleClass="text-blue-600 hover:text-blue-700 hover:bg-blue-50 text-xs h-7"
data-testid="meeting-details-ai-helper-button"></lfx-button>
</div>
}
</div>

Expand Down Expand Up @@ -83,6 +93,16 @@ <h4 class="text-sm font-semibold text-gray-900 mb-1">AI Agenda Generator</h4>
</div>
}

@if (showTemplateSelector()) {
<lfx-agenda-template-selector
[meetingType]="form().get('meeting_type')?.value"
[visible]="showTemplateSelector()"
(templateSelected)="applyTemplate($event)"
(closeSelector)="hideAgendaTemplateSelector()"
data-testid="meeting-details-template-selector">
</lfx-agenda-template-selector>
}

<lfx-textarea
[form]="form()"
control="agenda"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { TimePickerComponent } from '@components/time-picker/time-picker.compone
import { ToggleComponent } from '@components/toggle/toggle.component';
import { TIMEZONES } from '@lfx-pcc/shared/constants';
import { MeetingType } from '@lfx-pcc/shared/enums';
import { MeetingTemplate } from '@lfx-pcc/shared';
import { TooltipModule } from 'primeng/tooltip';
import { AgendaTemplateSelectorComponent } from '../agenda-template-selector/agenda-template-selector.component';

@Component({
selector: 'lfx-meeting-details',
Expand All @@ -30,6 +32,7 @@ import { TooltipModule } from 'primeng/tooltip';
TimePickerComponent,
ToggleComponent,
TooltipModule,
AgendaTemplateSelectorComponent,
],
templateUrl: './meeting-details.component.html',
})
Expand All @@ -44,6 +47,10 @@ export class MeetingDetailsComponent implements OnInit {
public readonly showAiHelper = signal<boolean>(false);
public readonly isGeneratingAgenda = signal<boolean>(false);

// Template selector signals
public readonly showTemplateSelector = signal<boolean>(false);
public readonly selectedTemplateId = signal<string | null>(null);

// Auto-title generation signals
public readonly titleWasAutoGenerated = signal<boolean>(false);

Expand Down Expand Up @@ -150,6 +157,41 @@ export class MeetingDetailsComponent implements OnInit {
this.hideAiAgendaHelper();
}

// Template selector public methods
public showAgendaTemplateSelector(): void {
this.showTemplateSelector.set(true);
}

public hideAgendaTemplateSelector(): void {
this.showTemplateSelector.set(false);
this.selectedTemplateId.set(null);
}

public applyTemplate(template: MeetingTemplate): void {
this.form().get('agenda')?.setValue(template.content);
this.selectedTemplateId.set(template.id);

// Set duration based on template
this.setTemplateDuration(template.estimatedDuration);

this.hideAgendaTemplateSelector();
}

private setTemplateDuration(estimatedDuration: number): void {
// Check if the estimated duration matches one of our standard options
const standardDuration = this.durationOptions.find((option) => typeof option.value === 'number' && option.value === estimatedDuration);

if (standardDuration) {
// Use standard duration option
this.form().get('duration')?.setValue(estimatedDuration);
this.form().get('customDuration')?.setValue(null);
} else {
// Use custom duration
this.form().get('duration')?.setValue('custom');
this.form().get('customDuration')?.setValue(estimatedDuration);
}
}

private generateRecurrenceOptions(date: Date): void {
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayName = dayNames[date.getDay()];
Expand Down Expand Up @@ -193,16 +235,6 @@ export class MeetingDetailsComponent implements OnInit {
return { weekOfMonth, isLastWeek };
}

private generateMeetingTitle(meetingType: string, date: Date): string {
const formattedDate = new Date(date).toLocaleDateString('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
});

return `${meetingType} Meeting - ${formattedDate}`;
}

private getMockAgenda(meetingType: string, prompt: string): string {
const mockAgendas: Record<string, string> = {
[MeetingType.BOARD]: `**Meeting Objective**: ${prompt}
Expand Down
104 changes: 104 additions & 0 deletions packages/shared/src/constants/meeting-templates/board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT

import { MeetingType } from '../../enums';
import { MeetingTemplate } from '../../interfaces';

export const BOARD_TEMPLATES: MeetingTemplate[] = [
{
id: 'board-quarterly-review',
title: 'Quarterly Board Review',
meetingType: MeetingType.BOARD,
estimatedDuration: 70,
content: `Quarterly Board Review

1. Opening & Governance (10 min)
- Roll call and quorum confirmation
- Approval of previous meeting minutes
- Conflict of interest declarations

2. Executive Report (20 min)
- Project health and key metrics
- Financial overview and budget status
- Strategic initiatives progress

3. Key Decisions (25 min)
- Budget approvals and amendments
- Strategic direction discussions
- Policy updates and governance changes

4. Risk & Compliance (10 min)
- Risk assessment review
- Compliance status update
- Legal matters requiring board attention

5. Next Steps & Closing (5 min)
- Action item assignments
- Next meeting scheduling
- Adjournment`,
},
{
id: 'board-strategic-planning',
title: 'Strategic Planning Session',
meetingType: MeetingType.BOARD,
estimatedDuration: 75,
content: `Strategic Planning Session

1. Current State Assessment (15 min)
- Project performance review
- Market position analysis
- Resource and capability assessment

2. Vision & Goals Setting (25 min)
- Long-term vision discussion
- Strategic objectives for next period
- Success metrics definition

3. Resource Planning (15 min)
- Budget allocation priorities
- Human resource requirements
- Infrastructure and technology needs

4. Implementation Planning (15 min)
- Timeline and milestone setting
- Responsibility assignments
- Risk mitigation strategies

5. Approval & Next Steps (5 min)
- Plan approval and ratification
- Communication strategy
- Follow-up meeting scheduling`,
},
{
id: 'board-budget-review',
title: 'Annual Budget Review',
meetingType: MeetingType.BOARD,
estimatedDuration: 70,
content: `Annual Budget Review

1. Financial Overview (15 min)
- Current year financial performance
- Revenue sources and sustainability
- Expense analysis and trends

2. Budget Proposal Review (30 min)
- Proposed budget for next fiscal year
- Line-item discussion and justification
- Funding sources and allocation

3. Investment Priorities (10 min)
- Technology and infrastructure investments
- Human resource investments
- Strategic initiative funding

4. Approval Process (10 min)
- Budget amendments and modifications
- Formal budget approval
- Oversight and monitoring procedures

5. Implementation Planning (5 min)
- Budget rollout timeline
- Communication to stakeholders
- Quarterly review scheduling`,
},
];
Loading
Loading