Skip to content

Commit 136702c

Browse files
asithadeclaude
andauthored
feat: implement complete meeting creation flow with recurring meetings support (#12)
* feat: implement complete meeting creation flow with backend support - Add MeetingFormComponent with comprehensive form validation and tooltips - Create backend endpoints for meeting CRUD operations (POST/PUT/GET) - Add TypeScript interfaces for CreateMeetingRequest and UpdateMeetingRequest - Implement proper datetime conversion from 12-hour format to ISO - Integrate meeting form into modal component with create/edit modes - Add meeting creation functionality to dashboard with form modal - Update SupabaseService with meeting management methods - Fix time picker integration and validation Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * feat: enhance meeting form with comprehensive features - Add comprehensive timezone list with 80+ timezones covering all regions - Implement form validation to ensure meetings are scheduled in the future - Add calendar date restriction to only allow dates from yesterday onwards - Default start date/time to current time + 1 hour (rounded to 15 min) - Show all validation errors when submit is clicked - Add minDate/maxDate support to calendar component wrapper - Create timezone constants in shared package with helper functions - Fix nested ternary expressions per linting rules - Update meeting type validation and default values Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix: update meeting form field initialization - Fix meeting_type default value to use 'None' instead of empty string - Ensure consistent default values across create and edit modes - Update form patchValue to handle meeting data properly Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> * feat: add recurring meeting options to meeting form Implement comprehensive recurring meeting functionality with 6 predefined recurrence patterns: - Does not repeat (default) - Daily - Weekly on [current day] - Monthly on the [nth] [day] - Monthly on the last [day] - Every weekday Key features: - Dynamic options based on selected meeting date - Smart UI that only shows relevant options (no duplicate monthly options) - Proper memory management with takeUntilDestroyed() - Full TypeScript support with new MeetingRecurrence interface - Generates correct Zoom API recurrence objects - Edit support for existing recurring meetings Technical implementation: - Added RecurrenceType enum to shared enums package - Enhanced MeetingRecurrence interface with proper typing - Updated CreateMeetingRequest and UpdateMeetingRequest interfaces - Intelligent date calculation for monthly recurrence patterns - Subscription cleanup to prevent memory leaks Addresses LFXV2-138: Add Recurring Meeting Options to Meeting Form Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Asitha de Silva <asithade@gmail.com> * refactor: migrate MeetingVisibility and MeetingType enums to shared enums folder Move MeetingVisibility and MeetingType enums from interfaces to the proper shared enums folder structure for better organization and consistency. 🤖 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa940ac commit 136702c

File tree

18 files changed

+1223
-15
lines changed

18 files changed

+1223
-15
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,5 @@ See `apps/lfx-pcc/src/app/modules/project/components/committee-dashboard/committ
179179
- **Pre-commit hooks enforce license headers** - commits will fail without proper headers
180180
- Always run yarn format from the root of the project to ensure formatting is done after you have made all your changes
181181
- Always preprend "Generated with [Claude Code](https://claude.ai/code)" if you assisted with the code
182+
- Do not nest ternary expressions
183+
- Always run yarn lint before yarn build to validate your linting

apps/lfx-pcc/src/app/modules/project/components/meeting-dashboard/meeting-dashboard.component.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// SPDX-License-Identifier: MIT
33

44
import { CommonModule } from '@angular/common';
5-
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
5+
import { Component, computed, inject, Injector, runInInjectionContext, signal, Signal, WritableSignal } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
77
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
8+
import { Router } from '@angular/router';
89
import { ButtonComponent } from '@app/shared/components/button/button.component';
910
import { CardComponent } from '@app/shared/components/card/card.component';
1011
import { FullCalendarComponent } from '@app/shared/components/fullcalendar/fullcalendar.component';
@@ -49,6 +50,8 @@ export class MeetingDashboardComponent {
4950
private readonly meetingService = inject(MeetingService);
5051
private readonly confirmationService = inject(ConfirmationService);
5152
private readonly dialogService = inject(DialogService);
53+
private readonly router = inject(Router);
54+
private readonly injector = inject(Injector);
5255

5356
// Class variables with types
5457
public project: typeof this.projectService.project;
@@ -103,7 +106,7 @@ export class MeetingDashboardComponent {
103106
}
104107

105108
public scheduleNewMeeting(): void {
106-
// TODO: Open create dialog when form is available
109+
this.onCreateMeeting();
107110
}
108111

109112
public onVisibilityChange(value: string | null): void {
@@ -131,6 +134,25 @@ export class MeetingDashboardComponent {
131134
this.currentView.set(value);
132135
}
133136

137+
public onCreateMeeting(): void {
138+
this.dialogService
139+
.open(MeetingModalComponent, {
140+
header: 'Create Meeting',
141+
width: '600px',
142+
modal: true,
143+
closable: true,
144+
dismissableMask: true,
145+
data: {
146+
isEditing: false, // This triggers form mode
147+
},
148+
})
149+
.onClose.subscribe((meeting) => {
150+
if (meeting) {
151+
this.refreshMeetings();
152+
}
153+
});
154+
}
155+
134156
public onCalendarEventClick(eventInfo: any): void {
135157
const meetingId = eventInfo.event.extendedProps?.meetingId;
136158
if (meetingId) {
@@ -391,4 +413,16 @@ export class MeetingDashboardComponent {
391413
});
392414
});
393415
}
416+
417+
private refreshMeetings(): void {
418+
this.meetingsLoading.set(true);
419+
this.pastMeetingsLoading.set(true);
420+
runInInjectionContext(this.injector, () => {
421+
this.meetings = this.initializeMeetings();
422+
this.pastMeetings = this.initializePastMeetings();
423+
this.filteredMeetings = this.initializeFilteredMeetings();
424+
this.publicMeetingsCount = this.initializePublicMeetingsCount();
425+
this.privateMeetingsCount = this.initializePrivateMeetingsCount();
426+
});
427+
}
394428
}

apps/lfx-pcc/src/app/shared/components/calendar/calendar.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
[showIcon]="showIcon()"
1818
[showButtonBar]="showButtonBar()"
1919
[dateFormat]="dateFormat()"
20+
[minDate]="minDate()"
21+
[maxDate]="maxDate()"
2022
(onSelect)="handleSelect($event)"
2123
styleClass="w-full"
2224
[size]="size()">

apps/lfx-pcc/src/app/shared/components/calendar/calendar.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export class CalendarComponent {
2424
public readonly showButtonBar = input<boolean>(false);
2525
public readonly dateFormat = input<string>('mm/dd/yy');
2626
public readonly size = input<'small' | 'large'>('small');
27+
public readonly minDate = input<Date | null>(null);
28+
public readonly maxDate = input<Date | null>(null);
2729

2830
// Events
2931
public readonly onSelect = output<any>();
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
2+
<!-- SPDX-License-Identifier: MIT -->
3+
4+
<form [formGroup]="form()" (ngSubmit)="onSubmit()" class="space-y-6">
5+
<!-- Basic Information Section -->
6+
<div class="flex flex-col gap-3">
7+
<!-- Topic (Required) -->
8+
<div>
9+
<label for="meeting-topic" class="block text-sm font-medium text-gray-700 mb-1"> Title <span class="text-red-500">*</span> </label>
10+
<lfx-input-text size="small" [form]="form()" control="topic" id="meeting-topic" placeholder="Enter meeting title" styleClass="w-full"></lfx-input-text>
11+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('topic')?.errors?.['required'] && form().get('topic')?.touched">Meeting topic is required</p>
12+
</div>
13+
14+
<!-- Agenda -->
15+
<div>
16+
<label for="meeting-agenda" class="block text-sm font-medium text-gray-700 mb-1"> Agenda </label>
17+
<lfx-textarea [form]="form()" control="agenda" id="meeting-agenda" placeholder="Enter meeting agenda" [rows]="4" styleClass="w-full"></lfx-textarea>
18+
</div>
19+
20+
<!-- Meeting Type -->
21+
<div>
22+
<label for="meeting-type" class="block text-sm font-medium text-gray-700 mb-1"> Meeting Type </label>
23+
<lfx-select
24+
size="small"
25+
[form]="form()"
26+
control="meeting_type"
27+
[options]="meetingTypeOptions"
28+
placeholder="Select meeting type"
29+
styleClass="w-full"
30+
id="meeting-type"></lfx-select>
31+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('meeting_type')?.errors?.['required'] && form().get('meeting_type')?.touched">
32+
Meeting type is required
33+
</p>
34+
</div>
35+
</div>
36+
37+
<!-- Date & Time Section -->
38+
<div class="flex flex-col gap-3">
39+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
40+
<!-- Start Date -->
41+
<div>
42+
<label for="start-date" class="block text-sm font-medium text-gray-700 mb-1"> Start Date <span class="text-red-500">*</span> </label>
43+
<lfx-calendar
44+
[form]="form()"
45+
control="startDate"
46+
id="start-date"
47+
placeholder="Select date"
48+
[showIcon]="true"
49+
[minDate]="minDate()"
50+
styleClass="w-full"></lfx-calendar>
51+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('startDate')?.errors?.['required'] && form().get('startDate')?.touched">
52+
Start date is required
53+
</p>
54+
</div>
55+
56+
<!-- Start Time -->
57+
<div>
58+
<label for="start-time" class="block text-sm font-medium text-gray-700 mb-1"> Start Time <span class="text-red-500">*</span> </label>
59+
<lfx-time-picker [form]="form()" control="startTime" id="start-time" placeholder="Select time" styleClass="w-full" width="w-48"></lfx-time-picker>
60+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('startTime')?.errors?.['required'] && form().get('startTime')?.touched">
61+
Start time is required
62+
</p>
63+
</div>
64+
65+
<!-- Duration -->
66+
<div>
67+
<label for="duration" class="block text-sm font-medium text-gray-700 mb-1"> Duration <span class="text-red-500">*</span> </label>
68+
<lfx-select
69+
size="small"
70+
[form]="form()"
71+
control="duration"
72+
[options]="durationOptions"
73+
placeholder="Select duration"
74+
styleClass="w-full"
75+
id="duration"></lfx-select>
76+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('duration')?.errors?.['required'] && form().get('duration')?.touched">Duration is required</p>
77+
</div>
78+
</div>
79+
80+
<!-- Future Date/Time validation error -->
81+
<p class="mt-1 text-sm text-red-600" *ngIf="form().errors?.['futureDateTime'] && (form().get('startDate')?.touched || form().get('startTime')?.touched)">
82+
Meeting must be scheduled in the future
83+
</p>
84+
85+
<!-- Custom Duration (shown when 'custom' is selected) -->
86+
@if (this.form().get('duration')?.value === 'custom') {
87+
<div>
88+
<label for="custom-duration" class="block text-sm font-medium text-gray-700 mb-1">
89+
Custom Duration (minutes) <span class="text-red-500">*</span>
90+
</label>
91+
<lfx-input-text
92+
size="small"
93+
[form]="form()"
94+
control="customDuration"
95+
id="custom-duration"
96+
placeholder="Enter minutes"
97+
type="number"
98+
styleClass="w-full"></lfx-input-text>
99+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('customDuration')?.errors?.['required'] && form().get('customDuration')?.touched">
100+
Custom duration is required
101+
</p>
102+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('customDuration')?.errors?.['min'] && form().get('customDuration')?.touched">
103+
Custom duration must be greater than 5 minutes
104+
</p>
105+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('customDuration')?.errors?.['max'] && form().get('customDuration')?.touched">
106+
Custom duration must be less than 480 minutes
107+
</p>
108+
</div>
109+
}
110+
111+
<!-- Timezone -->
112+
<div>
113+
<label for="timezone" class="block text-sm font-medium text-gray-700 mb-1"> Timezone <span class="text-red-500">*</span> </label>
114+
<lfx-select
115+
size="small"
116+
[filter]="true"
117+
[form]="form()"
118+
control="timezone"
119+
[options]="timezoneOptions"
120+
placeholder="Select timezone"
121+
styleClass="w-full"
122+
id="timezone"></lfx-select>
123+
<p class="mt-1 text-sm text-red-600" *ngIf="form().get('timezone')?.errors?.['required'] && form().get('timezone')?.touched">Timezone is required</p>
124+
</div>
125+
126+
<!-- Recurrence -->
127+
<div>
128+
<div class="flex items-center gap-2 mb-1">
129+
<label for="recurrence" class="block text-sm font-medium text-gray-700"> Repeat </label>
130+
<i
131+
class="fa-light fa-info-circle text-gray-400 cursor-help"
132+
pTooltip="Set up recurring meetings that repeat daily, weekly, monthly, or on weekdays"
133+
tooltipPosition="right"
134+
tooltipStyleClass="text-xs max-w-xs"></i>
135+
</div>
136+
<lfx-select
137+
size="small"
138+
[form]="form()"
139+
control="recurrence"
140+
[options]="recurrenceOptions()"
141+
placeholder="Select recurrence"
142+
styleClass="w-full"
143+
id="recurrence"></lfx-select>
144+
</div>
145+
146+
<!-- Early Join Time -->
147+
<div>
148+
<div class="flex items-center gap-2 mb-1">
149+
<label for="early-join-time" class="block text-sm font-medium text-gray-700"> Early Join Time (minutes before start time) </label>
150+
<i
151+
class="fa-light fa-info-circle text-gray-400 cursor-help"
152+
pTooltip="Allow participants to join the meeting early. Useful for informal networking before the official start time"
153+
tooltipPosition="right"
154+
tooltipStyleClass="text-xs max-w-xs"></i>
155+
</div>
156+
<lfx-input-text
157+
size="small"
158+
[form]="form()"
159+
control="early_join_time"
160+
id="early-join-time"
161+
placeholder="Enter minutes"
162+
type="number"
163+
styleClass="w-full"></lfx-input-text>
164+
</div>
165+
</div>
166+
167+
<!-- Meeting Settings Section -->
168+
<div class="flex flex-col gap-3">
169+
<h3 class="text-lg font-medium text-gray-900">Meeting Settings</h3>
170+
171+
<!-- Public Calendar Visibility -->
172+
<div class="flex flex-col gap-2">
173+
<div class="flex items-center gap-2">
174+
<lfx-toggle size="small" [form]="form()" control="show_in_public_calendar" label="Show in Public Calendar" id="public-calendar-toggle"></lfx-toggle>
175+
<i
176+
class="fa-light fa-info-circle text-gray-400 cursor-help"
177+
pTooltip="When enabled, this meeting will be visible on the project's public calendar"
178+
tooltipPosition="right"
179+
tooltipStyleClass="text-xs max-w-xs"></i>
180+
</div>
181+
</div>
182+
183+
<!-- Recording Settings -->
184+
<div class="flex flex-col gap-2">
185+
<div class="flex items-center gap-2">
186+
<lfx-toggle size="small" [form]="form()" control="recording_enabled" label="Enable Recording" id="recording-toggle"></lfx-toggle>
187+
<i
188+
class="fa-light fa-info-circle text-gray-400 cursor-help"
189+
pTooltip="Record the meeting for later viewing. Required for transcripts and automatic YouTube uploads"
190+
tooltipPosition="right"
191+
tooltipStyleClass="text-xs max-w-xs"></i>
192+
</div>
193+
194+
<!-- Recording-dependent features -->
195+
@if (form().get('recording_enabled')?.value === true) {
196+
<div class="flex items-center gap-2">
197+
<lfx-toggle size="small" [form]="form()" control="transcripts_enabled" label="Enable Transcripts" id="transcripts-toggle"></lfx-toggle>
198+
<i
199+
class="fa-light fa-info-circle text-gray-400 cursor-help"
200+
pTooltip="Generate automated transcripts from the meeting recording"
201+
tooltipPosition="right"
202+
tooltipStyleClass="text-xs max-w-xs"></i>
203+
</div>
204+
205+
<div class="flex items-center gap-2">
206+
<lfx-toggle size="small" [form]="form()" control="youtube_enabled" label="Enable YouTube Upload" id="youtube-toggle"></lfx-toggle>
207+
<i
208+
class="fa-light fa-info-circle text-gray-400 cursor-help"
209+
pTooltip="Automatically upload the meeting recording to YouTube after the meeting ends"
210+
tooltipPosition="right"
211+
tooltipStyleClass="text-xs max-w-xs"></i>
212+
</div>
213+
}
214+
</div>
215+
216+
<!-- Zoom AI Settings -->
217+
<div class="flex flex-col gap-2">
218+
<div class="flex items-center gap-2">
219+
<lfx-toggle size="small" [form]="form()" control="zoom_ai_enabled" label="Enable Zoom AI" id="zoom-ai-toggle"></lfx-toggle>
220+
<i
221+
class="fa-light fa-info-circle text-gray-400 cursor-help"
222+
pTooltip="Use Zoom's AI features to generate meeting summaries and insights"
223+
tooltipPosition="right"
224+
tooltipStyleClass="text-xs max-w-xs"></i>
225+
</div>
226+
227+
<!-- Zoom AI dependent features -->
228+
@if (form().get('zoom_ai_enabled')?.value === true) {
229+
<div class="flex items-center gap-2">
230+
<lfx-toggle
231+
size="small"
232+
[form]="form()"
233+
control="require_ai_summary_approval"
234+
label="Require AI Summary Approval"
235+
id="ai-approval-toggle"></lfx-toggle>
236+
<i
237+
class="fa-light fa-info-circle text-gray-400 cursor-help"
238+
pTooltip="Require manual review and approval before publishing AI-generated summaries"
239+
tooltipPosition="right"
240+
tooltipStyleClass="text-xs max-w-xs"></i>
241+
</div>
242+
243+
<div>
244+
<div class="flex items-center gap-2 mb-1">
245+
<label for="ai-summary-access" class="block text-sm font-medium text-gray-700"> AI Summary Access </label>
246+
<i
247+
class="fa-light fa-info-circle text-gray-400 cursor-help"
248+
pTooltip="Control who can access the AI-generated meeting summaries"
249+
tooltipPosition="right"
250+
tooltipStyleClass="text-xs max-w-xs"></i>
251+
</div>
252+
<lfx-select
253+
size="small"
254+
[form]="form()"
255+
control="ai_summary_access"
256+
[options]="aiSummaryAccessOptions"
257+
placeholder="Select access level"
258+
styleClass="w-full"
259+
id="ai-summary-access"></lfx-select>
260+
</div>
261+
}
262+
</div>
263+
</div>
264+
265+
<!-- Form Actions -->
266+
<div class="flex justify-end gap-3 pt-6 border-t">
267+
<lfx-button label="Cancel" severity="secondary" [outlined]="true" (click)="onCancel()" size="small" type="button"></lfx-button>
268+
<lfx-button
269+
[label]="isEditing() ? 'Update Meeting' : 'Create Meeting'"
270+
[loading]="submitting()"
271+
[disabled]="submitting()"
272+
type="submit"
273+
size="small"
274+
(onClick)="onSubmit()"></lfx-button>
275+
</div>
276+
</form>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
// Meeting form specific styles
5+
.meeting-form {
6+
// Component-specific styles can be added here if needed
7+
}

0 commit comments

Comments
 (0)