-
+
diff --git a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts
index 7e568019..75a5b064 100644
--- a/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts
+++ b/apps/lfx-one/src/app/modules/dashboards/maintainer/maintainer-dashboard.component.ts
@@ -1,8 +1,9 @@
// Copyright The Linux Foundation and each contributor to LFX.
// SPDX-License-Identifier: MIT
-import { Component, computed, inject } from '@angular/core';
+import { Component, computed, inject, signal } from '@angular/core';
import { ProjectContextService } from '@app/shared/services/project-context.service';
+import { MAINTAINER_ACTION_ITEMS } from '@lfx-one/shared/constants';
import { MyMeetingsComponent } from '../components/my-meetings/my-meetings.component';
import { MyProjectsComponent } from '../components/my-projects/my-projects.component';
@@ -20,4 +21,5 @@ export class MaintainerDashboardComponent {
private readonly projectContextService = inject(ProjectContextService);
public readonly selectedProject = computed(() => this.projectContextService.selectedFoundation() || this.projectContextService.selectedProject());
+ public readonly maintainerActions = signal(MAINTAINER_ACTION_ITEMS);
}
diff --git a/apps/lfx-one/src/app/shared/components/header/header.component.ts b/apps/lfx-one/src/app/shared/components/header/header.component.ts
index 94c68dc7..a552471d 100644
--- a/apps/lfx-one/src/app/shared/components/header/header.component.ts
+++ b/apps/lfx-one/src/app/shared/components/header/header.component.ts
@@ -9,7 +9,6 @@ import { Router, RouterModule } from '@angular/router';
import { AppService } from '@app/shared/services/app.service';
import { AvatarComponent } from '@components/avatar/avatar.component';
import { MenubarComponent } from '@components/menubar/menubar.component';
-import { PersonaSelectorComponent } from '@components/persona-selector/persona-selector.component';
import { CombinedProfile, Project } from '@lfx-one/shared/interfaces';
import { ProjectService } from '@services/project.service';
import { UserService } from '@services/user.service';
@@ -18,23 +17,12 @@ import { AutoCompleteCompleteEvent, AutoCompleteSelectEvent } from 'primeng/auto
import { RippleModule } from 'primeng/ripple';
import { catchError, debounceTime, distinctUntilChanged, of, startWith, switchMap } from 'rxjs';
-import { AutocompleteComponent } from '../autocomplete/autocomplete.component';
import { MenuComponent } from '../menu/menu.component';
@Component({
selector: 'lfx-header',
standalone: true,
- imports: [
- CommonModule,
- ReactiveFormsModule,
- MenubarComponent,
- RippleModule,
- RouterModule,
- AvatarComponent,
- MenuComponent,
- AutocompleteComponent,
- PersonaSelectorComponent,
- ],
+ imports: [CommonModule, ReactiveFormsModule, MenubarComponent, RippleModule, RouterModule, AvatarComponent, MenuComponent],
templateUrl: './header.component.html',
styleUrl: './header.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
diff --git a/apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts b/apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
index fefe5cdb..2b70aa82 100644
--- a/apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
+++ b/apps/lfx-one/src/app/shared/components/meeting-card/meeting-card.component.ts
@@ -252,9 +252,9 @@ export class MeetingCardComponent implements OnInit {
const ref = this.dialogService.open(SummaryModalComponent, {
header: 'Meeting Summary',
width: '800px',
- modal: true,
- closable: true,
- dismissableMask: true,
+ modal: false,
+ closable: false,
+ dismissableMask: false,
data: {
summaryContent: this.summaryContent(),
summaryUid: this.summaryUid(),
diff --git a/apps/lfx-one/src/app/shared/components/meeting-rsvp-details/meeting-rsvp-details.component.html b/apps/lfx-one/src/app/shared/components/meeting-rsvp-details/meeting-rsvp-details.component.html
index d8ac3b78..40cbdaef 100644
--- a/apps/lfx-one/src/app/shared/components/meeting-rsvp-details/meeting-rsvp-details.component.html
+++ b/apps/lfx-one/src/app/shared/components/meeting-rsvp-details/meeting-rsvp-details.component.html
@@ -45,14 +45,27 @@
- @if (acceptedCount() > 0) {
-
- }
- @if (!pastMeeting() && maybeCount() > 0) {
-
- }
- @if (!pastMeeting() && declinedCount() > 0) {
-
+ @if (pastMeeting()) {
+
+ @if (attendedCount() > 0) {
+
+ }
+ @if (meetingRegistrantCount() - attendedCount() > 0) {
+
+ }
+ } @else {
+
+ @if (acceptedCount() > 0) {
+
+ }
+ @if (maybeCount() > 0) {
+
+ }
+ @if (declinedCount() > 0) {
+
+ }
}
diff --git a/apps/lfx-one/src/app/shared/services/project.service.ts b/apps/lfx-one/src/app/shared/services/project.service.ts
index a064add8..81092afc 100644
--- a/apps/lfx-one/src/app/shared/services/project.service.ts
+++ b/apps/lfx-one/src/app/shared/services/project.service.ts
@@ -3,7 +3,7 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { inject, Injectable, signal, WritableSignal } from '@angular/core';
-import { Project } from '@lfx-one/shared/interfaces';
+import { PendingActionItem, Project } from '@lfx-one/shared/interfaces';
import { BehaviorSubject, catchError, Observable, of, tap } from 'rxjs';
@Injectable({
@@ -47,4 +47,20 @@ export class ProjectService {
})
);
}
+
+ /**
+ * Get pending action surveys for the current user
+ * @param projectSlug - Project slug to filter surveys
+ * @returns Observable of pending action items with survey links
+ */
+ public getPendingActionSurveys(projectSlug: string): Observable
{
+ const params = new HttpParams().set('projectSlug', projectSlug);
+
+ return this.http.get('/api/projects/pending-action-surveys', { params }).pipe(
+ catchError((error) => {
+ console.error('Failed to fetch pending action surveys:', error);
+ return of([]);
+ })
+ );
+ }
}
diff --git a/apps/lfx-one/src/server/controllers/project.controller.ts b/apps/lfx-one/src/server/controllers/project.controller.ts
index 6dfcae43..ec9ba367 100644
--- a/apps/lfx-one/src/server/controllers/project.controller.ts
+++ b/apps/lfx-one/src/server/controllers/project.controller.ts
@@ -437,6 +437,59 @@ export class ProjectController {
}
}
+ /**
+ * GET /projects/pending-action-surveys - Get pending survey actions for the authenticated user
+ */
+ public async getPendingActionSurveys(req: Request, res: Response, next: NextFunction): Promise {
+ const startTime = Logger.start(req, 'get_pending_action_surveys');
+
+ try {
+ // Extract user email from OIDC
+ const userEmail = req.oidc?.user?.['email'];
+ if (!userEmail) {
+ Logger.error(req, 'get_pending_action_surveys', startTime, new Error('User email not found in OIDC context'));
+
+ const validationError = ServiceValidationError.forField('email', 'User email not found in authentication context', {
+ operation: 'get_pending_action_surveys',
+ service: 'project_controller',
+ path: req.path,
+ });
+
+ next(validationError);
+ return;
+ }
+
+ // Extract projectSlug from query parameters
+ const projectSlug = req.query['projectSlug'] as string | undefined;
+ if (!projectSlug) {
+ Logger.error(req, 'get_pending_action_surveys', startTime, new Error('Missing projectSlug parameter'));
+
+ const validationError = ServiceValidationError.forField('projectSlug', 'projectSlug query parameter is required', {
+ operation: 'get_pending_action_surveys',
+ service: 'project_controller',
+ path: req.path,
+ });
+
+ next(validationError);
+ return;
+ }
+
+ // Get pending surveys from service
+ const pendingActions = await this.projectService.getPendingActionSurveys(userEmail, projectSlug);
+
+ Logger.success(req, 'get_pending_action_surveys', startTime, {
+ email: userEmail,
+ project_slug: projectSlug,
+ survey_count: pendingActions.length,
+ });
+
+ res.json(pendingActions);
+ } catch (error) {
+ Logger.error(req, 'get_pending_action_surveys', startTime, error);
+ next(error);
+ }
+ }
+
private isUuid(slug: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(slug);
}
diff --git a/apps/lfx-one/src/server/routes/projects.route.ts b/apps/lfx-one/src/server/routes/projects.route.ts
index bf9054a7..bcf218a9 100644
--- a/apps/lfx-one/src/server/routes/projects.route.ts
+++ b/apps/lfx-one/src/server/routes/projects.route.ts
@@ -18,6 +18,8 @@ router.get('/', (req, res, next) => projectController.getProjects(req, res, next
router.get('/search', (req, res, next) => projectController.searchProjects(req, res, next));
+router.get('/pending-action-surveys', (req, res, next) => projectController.getPendingActionSurveys(req, res, next));
+
router.get('/:slug', (req, res, next) => projectController.getProjectBySlug(req, res, next));
router.get('/:uid/permissions', (req, res, next) => projectController.getProjectPermissions(req, res, next));
diff --git a/apps/lfx-one/src/server/services/project.service.ts b/apps/lfx-one/src/server/services/project.service.ts
index b84e293a..42bad124 100644
--- a/apps/lfx-one/src/server/services/project.service.ts
+++ b/apps/lfx-one/src/server/services/project.service.ts
@@ -4,6 +4,8 @@
import { NATS_CONFIG } from '@lfx-one/shared/constants';
import { NatsSubjects } from '@lfx-one/shared/enums';
import {
+ PendingActionItem,
+ PendingSurveyRow,
Project,
ProjectIssuesResolutionAggregatedRow,
ProjectIssuesResolutionResponse,
@@ -723,6 +725,53 @@ export class ProjectService {
};
}
+ /**
+ * Get pending survey actions for a user
+ * Queries for non-responded surveys and transforms them into PendingActionItem format
+ * @param email - User's email from OIDC authentication
+ * @param projectSlug - Project slug to filter surveys
+ * @returns Array of pending action items with survey links
+ */
+ public async getPendingActionSurveys(email: string, projectSlug: string): Promise {
+ const query = `
+ SELECT
+ SURVEY_TITLE,
+ SURVEY_CUTOFF_DATE,
+ PROJECT_NAME,
+ SURVEY_LINK
+ FROM ANALYTICS_DEV.DEV_ADESILVA_PLATINUM_LFX_ONE.MEMBER_DASHBOARD_PENDING_ACTION_SURVEYS
+ WHERE EMAIL = ?
+ AND PROJECT_SLUG = ?
+ AND SURVEY_CUTOFF_DATE > CURRENT_DATE()
+ AND RESPONSE_TYPE = 'non_response'
+ AND COMMITTEE_CATEGORY = 'Board'
+ ORDER BY SURVEY_CUTOFF_DATE ASC
+ `;
+
+ const result = await this.snowflakeService.execute(query, [email, projectSlug]);
+
+ // Transform database rows to PendingActionItem format
+ return result.rows.map((row) => {
+ // Format the cutoff date as a readable string
+ const cutoffDate = new Date(row.SURVEY_CUTOFF_DATE);
+ const formattedDate = cutoffDate.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ });
+
+ return {
+ type: 'Submit Feedback',
+ badge: row.PROJECT_NAME,
+ text: `${row.SURVEY_TITLE} is due ${formattedDate}`,
+ icon: 'fa-regular fa-clipboard-list',
+ color: 'amber' as const,
+ buttonText: 'Submit Survey',
+ buttonLink: row.SURVEY_LINK,
+ };
+ });
+ }
+
/**
* Get project ID by slug using NATS request-reply pattern
* @private
diff --git a/packages/shared/src/interfaces/components.interface.ts b/packages/shared/src/interfaces/components.interface.ts
index 135651c3..9bc64389 100644
--- a/packages/shared/src/interfaces/components.interface.ts
+++ b/packages/shared/src/interfaces/components.interface.ts
@@ -354,6 +354,8 @@ export interface PendingActionItem {
color: 'amber' | 'blue' | 'green' | 'purple';
/** Button text */
buttonText: string;
+ /** Button link */
+ buttonLink?: string;
}
/**
diff --git a/packages/shared/src/interfaces/project.interface.ts b/packages/shared/src/interfaces/project.interface.ts
index fb7cc57d..3c47bcb4 100644
--- a/packages/shared/src/interfaces/project.interface.ts
+++ b/packages/shared/src/interfaces/project.interface.ts
@@ -172,3 +172,54 @@ export interface ProjectContext {
/** URL-friendly project identifier */
slug: string;
}
+
+/**
+ * Pending survey response from analytics database
+ * @description Survey data for member dashboard pending actions
+ */
+export interface PendingSurveyRow {
+ /** Unique survey identifier */
+ SURVEY_ID: string;
+ /** Survey title/name */
+ SURVEY_TITLE: string;
+ /** Survey status (e.g., sent) */
+ SURVEY_STATUS: string;
+ /** Survey cohort date */
+ SURVEY_COHORT_DATE: string;
+ /** Survey cutoff/due date */
+ SURVEY_CUTOFF_DATE: string;
+ /** Committee identifier */
+ COMMITTEE_ID: string;
+ /** Committee name */
+ COMMITTEE_NAME: string;
+ /** Committee category (e.g., Board) */
+ COMMITTEE_CATEGORY: string;
+ /** Project identifier */
+ PROJECT_ID: string;
+ /** Project slug */
+ PROJECT_SLUG: string;
+ /** Project name */
+ PROJECT_NAME: string;
+ /** Response identifier */
+ RESPONSE_ID: string;
+ /** Response date */
+ RESPONSE_DATE: string;
+ /** Respondent first name */
+ FIRST_NAME: string;
+ /** Respondent last name */
+ LAST_NAME: string;
+ /** Respondent email */
+ EMAIL: string;
+ /** Account identifier */
+ ACCOUNT_ID: string;
+ /** Account name */
+ ACCOUNT_NAME: string;
+ /** Organization identifier */
+ ORGANIZATION_ID: string;
+ /** Organization name */
+ ORGANIZATION_NAME: string;
+ /** Response type (e.g., non_response) */
+ RESPONSE_TYPE: string;
+ /** Survey link URL */
+ SURVEY_LINK: string;
+}