@ieeesa-npm/groups 0.15.17 → 0.15.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/esm2022/lib/assets/constants.json +27 -1
  2. package/esm2022/lib/components/add-user-roster/add-user-roster.component.mjs +8 -10
  3. package/esm2022/lib/components/create-group/create-group.component.mjs +2 -2
  4. package/esm2022/lib/components/edit-user-roles/edit-user-roles.component.mjs +7 -55
  5. package/esm2022/lib/components/interest-form-management/interest-form-management.component.mjs +3 -3
  6. package/esm2022/lib/components/interest-responses/interest-responses.component.mjs +119 -0
  7. package/esm2022/lib/components/interested-rosters/interested-rosters.component.mjs +370 -15
  8. package/esm2022/lib/components/roster-import-history/roster-import-history.component.mjs +106 -8
  9. package/esm2022/lib/components/view-roster/view-roster.component.mjs +2 -2
  10. package/esm2022/lib/groups.component.mjs +58 -9
  11. package/esm2022/lib/models/interest-response.model.mjs +18 -0
  12. package/esm2022/lib/models/roster-import.model.mjs +1 -1
  13. package/esm2022/lib/services/groups.service.mjs +91 -7
  14. package/esm2022/public-api.mjs +3 -1
  15. package/fesm2022/ieeesa-npm-groups.mjs +819 -210
  16. package/fesm2022/ieeesa-npm-groups.mjs.map +1 -1
  17. package/lib/components/edit-user-roles/edit-user-roles.component.d.ts +0 -10
  18. package/lib/components/interest-responses/interest-responses.component.d.ts +80 -0
  19. package/lib/components/interested-rosters/interested-rosters.component.d.ts +143 -9
  20. package/lib/components/roster-import-history/roster-import-history.component.d.ts +42 -4
  21. package/lib/groups.component.d.ts +9 -4
  22. package/lib/models/interest-response.model.d.ts +209 -0
  23. package/lib/models/roster-import.model.d.ts +12 -0
  24. package/lib/services/groups.service.d.ts +66 -10
  25. package/package.json +1 -1
  26. package/public-api.d.ts +2 -0
  27. package/esm2022/lib/assets/country-calling-codes.mjs +0 -83
  28. package/lib/assets/country-calling-codes.d.ts +0 -14
@@ -113,16 +113,6 @@ export declare class EditUserRolesComponent implements OnInit, OnDestroy {
113
113
  * @memberof EditUserRolesComponent
114
114
  */
115
115
  getEditUserRolePayload(formData: any): AddOrEditUserPayload | AddOrEditUserIndividualRosterPayload;
116
- /**
117
- * Appends the optional contact fields (phone country code, phone number and
118
- * address) to the payload. Reads from the combined PHONE control value
119
- * ({ countryCode, phoneNumber }) and the address control. Sends the country
120
- * code only when a number is present; omits empty values.
121
- * @param {(AddOrEditUserPayload | AddOrEditUserIndividualRosterPayload)} payload
122
- * @param {*} formData
123
- * @memberof EditUserRolesComponent
124
- */
125
- appendContactDetailsToPayload(payload: AddOrEditUserPayload | AddOrEditUserIndividualRosterPayload, formData: any): void;
126
116
  /**
127
117
  * Emits form cancel event.
128
118
  * @memberof EditUserRolesComponent
@@ -0,0 +1,80 @@
1
+ import { ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core';
2
+ import { GroupsService } from '../../services/groups.service';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * A single question/answer pair rendered in the interest responses list. Built
6
+ * from a backend {@link InterestFormResponse}; {@code value} is normalized to a
7
+ * display string (CHECKBOX arrays are joined).
8
+ */
9
+ interface SubmissionAnswer {
10
+ label: string;
11
+ value: string;
12
+ }
13
+ /**
14
+ * Payload passed into {@link InterestResponsesComponent} by the host
15
+ * {@link ModalComponent} via the dynamic-input pattern.
16
+ */
17
+ export interface InterestResponsesData {
18
+ /** The interest record whose stored responses are shown. */
19
+ interestId: string;
20
+ }
21
+ /**
22
+ * "View Responses" content of the Roster Management "Interested Rosters" tab
23
+ * (SP3M-428). Rendered inside the shared {@link ModalComponent} (which supplies
24
+ * the title row and X close), it lists a requester's submitted interest-form
25
+ * answers as label/value pairs. Responses are loaded on init from
26
+ * {@link GroupsService.getInterestResponses} using the interest id supplied by
27
+ * the host modal. All display text is sourced from constants.json.
28
+ * @export
29
+ * @class InterestResponsesComponent
30
+ */
31
+ export declare class InterestResponsesComponent implements OnInit, OnDestroy {
32
+ private groupsService;
33
+ private cdr;
34
+ /**
35
+ * Data supplied by the host {@link ModalComponent} via the dynamic-input
36
+ * pattern (`dataInputName: 'componentData'`): carries the interest id whose
37
+ * responses are loaded.
38
+ */
39
+ componentData: InterestResponsesData | null;
40
+ constants: any;
41
+ texts: any;
42
+ submissionAnswers: SubmissionAnswer[];
43
+ isLoading: boolean;
44
+ errorMessage: string;
45
+ private readonly destroy$;
46
+ constructor(groupsService: GroupsService, cdr: ChangeDetectorRef);
47
+ /**
48
+ * Loads the interest-form responses for the supplied interest id on init.
49
+ * @memberof InterestResponsesComponent
50
+ */
51
+ ngOnInit(): void;
52
+ ngOnDestroy(): void;
53
+ /**
54
+ * Fetches and maps the interest-form responses into displayable
55
+ * question/answer pairs. Failures surface a user-facing error message and are
56
+ * delegated to GroupsService for alerting, mirroring the platform pattern.
57
+ * @memberof InterestResponsesComponent
58
+ */
59
+ private loadResponses;
60
+ /**
61
+ * Maps backend responses to label/value pairs, joining multi-select
62
+ * (CHECKBOX) answers and normalizing missing values to an empty string.
63
+ * @param {InterestFormResponse[]} responses
64
+ * @return {SubmissionAnswer[]}
65
+ * @memberof InterestResponsesComponent
66
+ */
67
+ private mapResponses;
68
+ /**
69
+ * Normalizes an answer value for display. CHECKBOX answers arrive as an array
70
+ * and are joined with commas; all other field types render their string
71
+ * value as-is.
72
+ * @param {InterestFormResponse} response
73
+ * @return {string}
74
+ * @memberof InterestResponsesComponent
75
+ */
76
+ private formatAnswer;
77
+ static ɵfac: i0.ɵɵFactoryDeclaration<InterestResponsesComponent, never>;
78
+ static ɵcmp: i0.ɵɵComponentDeclaration<InterestResponsesComponent, "ieeesa-interest-responses", never, { "componentData": { "alias": "componentData"; "required": false; }; }, {}, never, never, true, never>;
79
+ }
80
+ export {};
@@ -1,17 +1,151 @@
1
+ import { ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core';
2
+ import { MatDialog } from '@angular/material/dialog';
3
+ import { PageEvent } from '@angular/material/paginator';
4
+ import { Sort } from '@angular/material/sort';
5
+ import { SelectOption } from '@ieeesa-npm/models';
6
+ import { PaginatorConfig, TableActions, TableColumnSchema } from '@ieeesa-npm/presentation';
7
+ import { GroupInfo } from '../../models/group-info.model';
8
+ import { InterestedRoster } from '../../models/interest-response.model';
9
+ import { GroupsService } from '../../services/groups.service';
1
10
  import * as i0 from "@angular/core";
11
+ type InterestedRosterColumnSchema = TableColumnSchema & {
12
+ ariaLabel?: (row: InterestedRoster) => string;
13
+ };
2
14
  /**
3
- * "Interested Rosters" tab content of the Roster Interests dialog (SP3M-317).
4
- * Displays the list of users who expressed interest in the group. This is a
5
- * placeholder shell for now; the submissions table will be implemented in a
6
- * later increment once the backend is available (data will be mocked in the
7
- * service layer meanwhile).
15
+ * "Interested Rosters" tab of the Roster Interests dialog (SP3M-428). Lists the
16
+ * users who expressed interest in the group and lets a Group Admin review each
17
+ * requester's submitted responses and set/override their membership level from
18
+ * the Roster Management screen.
19
+ *
20
+ * The list is rendered through the shared {@link DataTableComponent}: the
21
+ * Name/Email/Affiliations/Submitted On columns are plain TEXT, Member Level is
22
+ * the reusable table's DROPDOWN column (an inline {@code ieeesa-select}) and
23
+ * Form is a LINK that opens the "View Responses" dialog. The Member Level
24
+ * options are sourced from the roles API
25
+ * ({@link GroupsService.getGroupTypeRoles}) filtered to the "Member Level" role
26
+ * type. Interested-roster rows are loaded from
27
+ * {@link GroupsService.getInterestResponsesByGroup} and each requester's stored
28
+ * form responses are loaded on demand from
29
+ * {@link GroupsService.getInterestResponses} using the row's `interestId`. All
30
+ * display text is sourced from constants.json.
8
31
  * @export
9
32
  * @class InterestedRostersComponent
10
33
  */
11
- export declare class InterestedRostersComponent {
34
+ export declare class InterestedRostersComponent implements OnInit, OnDestroy {
35
+ private dialog;
36
+ private groupsService;
37
+ private cdr;
38
+ group: GroupInfo | null;
12
39
  constants: any;
13
- interestedRostersTexts: any;
14
- placeholderMessage: any;
15
- static ɵfac: i0.ɵɵFactoryDeclaration<InterestedRostersComponent, never>;
40
+ texts: any;
41
+ private destroy$;
42
+ /**
43
+ * Member-level dropdown options, sourced from the roles API
44
+ * ({@code GET /groups/config/rolesByGroupType/{groupType}}) filtered to the "Member Level" role type.
45
+ * Each option is {@code { id: roleId, name: roleName }}.
46
+ */
47
+ memberLevelOptions: SelectOption[];
48
+ rosters: InterestedRoster[];
49
+ tableData: InterestedRoster[];
50
+ columnSchema: InterestedRosterColumnSchema[];
51
+ /** Total interested-roster rows reported by the backend (for paging/counts). */
52
+ totalRecords: number;
53
+ paginatorConfig: PaginatorConfig;
54
+ pageIndex: number;
55
+ defaultSortedColumn: string;
56
+ defaultColumnSortDirection: Sort['direction'];
57
+ private sortedBy;
58
+ private sortedOrderBy;
59
+ /** Read-only table — no search / filter / add / edit / delete actions. */
60
+ tableActions: TableActions;
61
+ isLoading: boolean;
62
+ constructor(dialog: MatDialog, groupsService: GroupsService, cdr: ChangeDetectorRef, group: GroupInfo | null);
63
+ /**
64
+ * Builds the reusable-table column schema and loads the interested-roster
65
+ * list for the current group from the backend.
66
+ * @memberof InterestedRostersComponent
67
+ */
68
+ ngOnInit(): void;
69
+ /**
70
+ * Cleans up in-flight subscriptions when the component is destroyed.
71
+ * @memberof InterestedRostersComponent
72
+ */
73
+ ngOnDestroy(): void;
74
+ /**
75
+ * Resolves the current group id from the dialog data (falling back to the
76
+ * service's selected group) and loads the interested-roster rows for it. Maps
77
+ * each backend {@link GroupInterestResponseView} onto the flattened
78
+ * {@link InterestedRoster} shape the reusable table binds to. On failure the
79
+ * table is cleared and the error is delegated to GroupsService for alerting.
80
+ * @memberof InterestedRostersComponent
81
+ */
82
+ private loadInterestedRosters;
83
+ onPaginationChange(event: PageEvent): void;
84
+ onSortChange(event: Sort): void;
85
+ /**
86
+ * Maps a backend {@link GroupInterestResponseView} to the flattened
87
+ * {@link InterestedRoster} row the table renders: affiliation org names are
88
+ * joined into the affiliations list, the assigned membership level (if any)
89
+ * becomes the role name — which the Member Level dropdown binds as its
90
+ * pre-selected value — the submitted timestamp is formatted for display, and
91
+ * the interest id is carried through for the responses dialog and the level
92
+ * override.
93
+ * @param {GroupInterestResponseView} view
94
+ * @return {InterestedRoster}
95
+ * @memberof InterestedRostersComponent
96
+ */
97
+ private mapToRoster;
98
+ /**
99
+ * Formats the backend submitted timestamp (ISO date string) for display,
100
+ * falling back to the raw value when it is not a parseable date.
101
+ * @param {string} submittedOn
102
+ * @return {string}
103
+ * @memberof InterestedRostersComponent
104
+ */
105
+ private formatSubmittedOn;
106
+ /**
107
+ * Loads the Member Level dropdown options from the roles API
108
+ * ({@code GET /groups/config/rolesByGroupType/{groupType}}), keeping only roles whose
109
+ * {@code roleTypeName} is "Member Level" and mapping each to a
110
+ * {@link SelectOption} ({@code { id: roleId, name: roleName }}).
111
+ * @memberof InterestedRostersComponent
112
+ */
113
+ private loadMemberLevelOptions;
114
+ /**
115
+ * Persists the membership-level override when the admin picks a value in a
116
+ * row's Member Level dropdown (bound to the reusable table's
117
+ * `rowDropdownChange` output). Builds the request from the dialog's current
118
+ * group id, the row's user id and the selected option's id (the role id), and
119
+ * posts it to the backend. The row's displayed level is updated optimistically
120
+ * and reverted if the save fails.
121
+ * @param {{ row: InterestedRoster; option: SelectOption }} event
122
+ * @memberof InterestedRostersComponent
123
+ */
124
+ onMemberLevelChange(event: {
125
+ row: InterestedRoster;
126
+ option: SelectOption;
127
+ }): void;
128
+ /**
129
+ * Opens the "View Responses" dialog for a roster row (bound to the reusable
130
+ * table's LINK output) using the shared {@link ModalComponent}. The modal
131
+ * supplies the title row and X close; the requester's stored interest-form
132
+ * responses are rendered by {@link InterestResponsesComponent}, which loads
133
+ * them from the backend using the row's {@code interestId} passed via the
134
+ * ModalComponent dynamic-input pattern.
135
+ * @param {InterestedRoster} row
136
+ * @memberof InterestedRostersComponent
137
+ */
138
+ onFormLinkClick(row: InterestedRoster): void;
139
+ /**
140
+ * Defines the reusable-table columns for the Interested Rosters list. Every
141
+ * `cellData`/`selectedOption` is a `(row) => ...` accessor, matching the
142
+ * shared table's contract. Member Level is the DROPDOWN column seeded with the
143
+ * configured levels plus "None"; Form is a LINK that opens the responses
144
+ * dialog.
145
+ * @memberof InterestedRostersComponent
146
+ */
147
+ private buildColumnSchema;
148
+ static ɵfac: i0.ɵɵFactoryDeclaration<InterestedRostersComponent, [null, null, null, { optional: true; }]>;
16
149
  static ɵcmp: i0.ɵɵComponentDeclaration<InterestedRostersComponent, "ieeesa-interested-rosters", never, {}, {}, never, never, true, never>;
17
150
  }
151
+ export {};
@@ -24,10 +24,11 @@ interface RosterImportHistoryRow {
24
24
  * Data comes from the real BE endpoint via {@link RosterImportApiService}:
25
25
  * `GET /v1/groups/roster/import/history/{groupId}`. The response is a list of
26
26
  * `RosterImportJob` whose JSON view exposes `fileName`, `successRows`,
27
- * `failedRows`, `status` and ids — but NOT `createdOn` / `createdBy` (both are
28
- * `@JsonIgnore` on the BE entity). So the "Timestamp" and "Person Imported"
29
- * columns render a placeholder until the BE exposes those fields. "Target
30
- * Group" uses the current group's name (history is scoped to one group).
27
+ * `failedRows`, `status` and ids. The "Timestamp" and "Person Imported" columns
28
+ * map to `createdOn` / `createdBy`; these are currently `@JsonIgnore` on the BE
29
+ * entity, so until the BE exposes them the columns show a placeholder, and they
30
+ * populate automatically once the response carries the fields. "Target Group"
31
+ * uses the current group's name (history is scoped to one group).
31
32
  *
32
33
  * @export
33
34
  * @class RosterImportHistoryComponent
@@ -83,6 +84,43 @@ export declare class RosterImportHistoryComponent implements OnInit, OnDestroy {
83
84
  * @memberof RosterImportHistoryComponent
84
85
  */
85
86
  private mapJobToRow;
87
+ /**
88
+ * Formats the BE `createdOn` timestamp for display, converting from UTC to the
89
+ * browser's local timezone.
90
+ *
91
+ * The API sends a UTC wall-clock string in `dd/MM/yyyy HH:mm:ss` with NO zone
92
+ * marker, so it must be parsed EXPLICITLY as UTC (otherwise luxon/JS would
93
+ * assume local time and shift it incorrectly), then rendered in the system
94
+ * (browser) zone via toLocal(). Uses luxon per the project's date standards
95
+ * (moment is legacy — not used for new code).
96
+ * @param {string} value - The raw UTC createdOn value from the BE.
97
+ * @return {string} the localized display string, or the placeholder when
98
+ * blank/invalid.
99
+ * @memberof RosterImportHistoryComponent
100
+ */
101
+ private formatTimestamp;
102
+ /**
103
+ * Parses a BE timestamp string into a luxon {@link DateTime}, tolerant of the
104
+ * several shapes the backend may send so the UI renders correctly regardless
105
+ * of which format is used.
106
+ *
107
+ * The agreed/expected BE format is a UTC wall-clock string `dd/MM/yyyy HH:mm:ss`
108
+ * with NO zone marker; it is tried FIRST and parsed explicitly in UTC. If the
109
+ * BE instead sends a different shape, these fallbacks are tried in order:
110
+ * 1. `dd/MM/yyyy HH:mm:ss` (UTC) — the expected/agreed format
111
+ * 2. ISO-8601 (e.g. 2026-10-09T14:23:05Z or with offset) — zone from the string,
112
+ * or assumed UTC when the string carries none
113
+ * 3. SQL datetime `yyyy-MM-dd HH:mm:ss` (UTC)
114
+ * 4. `dd/MM/yyyy HH:mm` (UTC, no seconds)
115
+ * 5. RFC-2822 / HTTP date
116
+ * 6. native JS Date (last resort) — assumed UTC
117
+ * The first format that yields a valid DateTime wins. Returns null when none
118
+ * parse.
119
+ * @param {string} value - The raw timestamp string from the BE.
120
+ * @return {(DateTime | null)}
121
+ * @memberof RosterImportHistoryComponent
122
+ */
123
+ private parseUtcTimestamp;
86
124
  /**
87
125
  * Maps a failed history request to a user-facing inline error message.
88
126
  * A 403 gets the permission-specific message; other errors get the generic
@@ -6,7 +6,7 @@ import { Subject } from 'rxjs';
6
6
  import { GroupInfo } from './models/group-info.model';
7
7
  import { SelectOption, IconButton, AlignType, SearchType, BreakpointObserverService } from '@ieeesa-npm/presentation';
8
8
  import { GroupType, GroupTypeConfigValue } from './models/group-types.model';
9
- import { AlertsService, GlobalErrorHandlerService } from '@ieeesa-npm/utility';
9
+ import { AlertsService, GlobalErrorHandlerService, StorageService } from '@ieeesa-npm/utility';
10
10
  import { AlertType } from '@ieeesa-npm/models';
11
11
  import { GroupFunctionType } from './models/group-function-type.model';
12
12
  import { GetGroupsPayload } from './models/view-group.model';
@@ -29,6 +29,7 @@ export declare class GroupsComponent implements OnInit, OnDestroy {
29
29
  globalErrorHandlerService: GlobalErrorHandlerService;
30
30
  private userPermissionsService;
31
31
  private groupPermissionsService;
32
+ private storageService;
32
33
  destroy$: Subject<boolean>;
33
34
  dialogRef: any;
34
35
  constants: any;
@@ -84,7 +85,8 @@ export declare class GroupsComponent implements OnInit, OnDestroy {
84
85
  trimmedSearchTerm: string;
85
86
  userGroupPermissions: GroupPermission[];
86
87
  supportMessage: string;
87
- constructor(dialog: MatDialog, groupsService: GroupsService, alertService: AlertsService, breakpointObserverService: BreakpointObserverService, globalErrorHandlerService: GlobalErrorHandlerService, userPermissionsService: UserPermissionsService, groupPermissionsService: GroupPermissionsMapService);
88
+ private hasAttemptedHistoryRestore;
89
+ constructor(dialog: MatDialog, groupsService: GroupsService, alertService: AlertsService, breakpointObserverService: BreakpointObserverService, globalErrorHandlerService: GlobalErrorHandlerService, userPermissionsService: UserPermissionsService, groupPermissionsService: GroupPermissionsMapService, storageService: StorageService);
88
90
  /**
89
91
  * Initialize the groups component.
90
92
  * @return -returns nothing
@@ -138,7 +140,7 @@ export declare class GroupsComponent implements OnInit, OnDestroy {
138
140
  * @param {GroupInfo} selectedGroupInfo
139
141
  * @memberof GroupsComponent
140
142
  */
141
- openRosterImportHistoryModal(selectedGroupInfo: GroupInfo): void;
143
+ openRosterImportHistoryModal(selectedGroupInfo: GroupInfo, isRestored?: boolean): void;
142
144
  /**
143
145
  * Sets the dialog options for users modal and calls openModal method.
144
146
  * @param {GroupInfo} group
@@ -160,7 +162,7 @@ export declare class GroupsComponent implements OnInit, OnDestroy {
160
162
  * @return -returns nothing
161
163
  * @memberof GroupsComponent
162
164
  */
163
- openModal(dialogOptions: any, className?: string, groupInfo?: GroupInfo | EditGroupAndParentInfo): void;
165
+ openModal(dialogOptions: any, className?: string, groupInfo?: GroupInfo | EditGroupAndParentInfo, isRestoredReadOnlyDialog?: boolean): void;
164
166
  /**
165
167
  * Opens the confirmation modal to get user confirmation to delete the selected group.
166
168
  * @memberof GroupComponent
@@ -222,6 +224,9 @@ export declare class GroupsComponent implements OnInit, OnDestroy {
222
224
  * @memberof GroupsComponent
223
225
  */
224
226
  getGroupTypes(): void;
227
+ private get rosterHistoryStorageKey();
228
+ private persistRosterHistory;
229
+ private restoreRosterImportHistoryModal;
225
230
  /**
226
231
  * Handles alerts for success, error and warning.
227
232
  * @param {(string | string[])} message -success or error or warning message.
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Interest-form field types frozen on each stored response via the backend's
3
+ * {@code field_type_snapshot} column. Mirrors the BE `FieldType` enum
4
+ * (sa-saapps-groups-api: org.ieee.standards.groups.model.FieldType) and drives
5
+ * how {@link InterestFormResponse.answerValue} is rendered in the admin
6
+ * "View Responses" dialog (single value vs. a list for CHECKBOX).
7
+ */
8
+ export declare enum InterestFieldType {
9
+ TEXT = "TEXT",
10
+ TEXTAREA = "TEXTAREA",
11
+ SELECT = "SELECT",
12
+ CHECKBOX = "CHECKBOX",
13
+ RADIO = "RADIO",
14
+ DATE = "DATE"
15
+ }
16
+ /**
17
+ * A single submitted answer to an interest-form question, as returned by the
18
+ * backend from
19
+ * {@code GET {apiBaseUrl}/groups/interest/{interestId}/responses}.
20
+ *
21
+ * Mirrors the UI-view projection of the BE `InterestFormResponse` entity
22
+ * (sa-saapps-groups-api: org.ieee.standards.groups.model.InterestFormResponse);
23
+ * only the fields annotated with the BE `@JsonView(UiView)` are serialized —
24
+ * the audit/app_id columns are `@JsonIgnore` and never arrive on the client.
25
+ *
26
+ * {@link questionLabelSnapshot} and {@link fieldTypeSnapshot} freeze the
27
+ * question label and field type as configured at submit time, so later form
28
+ * configuration changes apply only to new submissions (R8.6, R8.8).
29
+ * {@link answerValue} is a JSONB passthrough: it arrives as a plain string for
30
+ * single-value fields (TEXT/TEXTAREA/SELECT/RADIO/DATE) and as a string[] for
31
+ * multi-select (CHECKBOX).
32
+ */
33
+ export interface InterestFormResponse {
34
+ /** Response row identifier (e.g. "IFR_SAAPPS_..."). */
35
+ responseId: string;
36
+ /** The interest record this answer belongs to (e.g. "GINT_SAAPPS_..."). */
37
+ interestId: string;
38
+ /** The question this answer is for (e.g. "IFQ_SAAPPS_..."), when available. */
39
+ questionId?: string;
40
+ /** Question label frozen at submit time (R8.6, R8.8). */
41
+ questionLabelSnapshot?: string;
42
+ /** Field type frozen at submit time; see {@link InterestFieldType}. */
43
+ fieldTypeSnapshot?: string;
44
+ /**
45
+ * Submitted answer. A string for single-value fields; a string[] for
46
+ * CHECKBOX. `null`/undefined when the question was left unanswered.
47
+ */
48
+ answerValue?: string | string[] | null;
49
+ }
50
+ /**
51
+ * A single organization affiliation on an interested-roster row, as returned
52
+ * inside {@link GroupInterestResponseView.affiliation}. Mirrors the UI-view
53
+ * projection of the BE `Organization` entity (only `orgId`/`orgName` are
54
+ * `@JsonView(UiView)`; the rest are `@JsonIgnore`).
55
+ */
56
+ export interface InterestAffiliation {
57
+ /** Organization id (e.g. "ORG_SAAPPS_..."). */
58
+ orgId: string;
59
+ /** Organization display name. */
60
+ orgName: string;
61
+ }
62
+ /**
63
+ * A single membership level (role) on an interested-roster row, as returned
64
+ * inside {@link GroupInterestResponseView.memberLevel}. Mirrors the UI-view
65
+ * projection of the BE role object: the level is a group role, so the fields
66
+ * are role-shaped ({@link roleId}/{@link roleName}). The list carries at most
67
+ * one element — the interest's current/assigned membership level (empty when
68
+ * none is assigned).
69
+ */
70
+ export interface InterestMembershipLevel {
71
+ /** Role id of the membership level (e.g. "ROLE_SAAPPS_..."). */
72
+ roleId: string;
73
+ /** Role type id, when categorized; `null` otherwise. */
74
+ roleType?: string | null;
75
+ /** Display name of the level (e.g. "Member"). Bound as the dropdown value. */
76
+ roleName: string;
77
+ /** Human-readable role type name; `null` otherwise. */
78
+ roleTypeName?: string | null;
79
+ /** Display order within the group type's role catalog. */
80
+ viewOrder?: number;
81
+ }
82
+ /**
83
+ * One interested-users roster row for a group, as returned by the backend from
84
+ * {@code GET {apiBaseUrl}/groups/interest/group/{groupId}/responses}.
85
+ *
86
+ * Mirrors the UI-view projection of the BE `GroupInterestResponseView` read
87
+ * object (sa-saapps-groups-api:
88
+ * org.ieee.standards.groups.model.GroupInterestResponseView). It combines the
89
+ * interest record with the interested user's name/email, their organization
90
+ * affiliations for the group, the submitted timestamp, the group's default
91
+ * membership level (at most one element), and the group's type id, so the whole
92
+ * "Interested" roster renders in a single call (R8.7). Only `@JsonView(UiView)`
93
+ * fields are serialized; tenant app_id and audit columns stay hidden.
94
+ */
95
+ export interface GroupInterestResponseView {
96
+ /** The interest record id (e.g. "GINT_SAAPPS_..."). */
97
+ interestId: string;
98
+ /** The group the interest belongs to (e.g. "GRP_SAAPPS_..."). */
99
+ groupId?: string;
100
+ /** The interested user's id (e.g. "User_NESC_..."). */
101
+ userId: string;
102
+ /** Interested user's display name. */
103
+ name: string;
104
+ /** Interested user's email address. */
105
+ email: string;
106
+ /** Organization affiliations for the group (id + name). */
107
+ affiliation: InterestAffiliation[];
108
+ /** Submitted timestamp (ISO date string; BE `Date`). */
109
+ submittedOn: string;
110
+ /** Group's default membership level (0 or 1 element). */
111
+ memberLevel: InterestMembershipLevel[];
112
+ /** The group's group type id. */
113
+ groupTypeId?: string;
114
+ }
115
+ /**
116
+ * Request payload for the interested-users roster list endpoint
117
+ * ({@code POST {apiBaseUrl}/groups/interest/group/{groupId}/responses}).
118
+ *
119
+ * Follows the repo payload convention of wrapping the request body in a
120
+ * {@code payload} key (matching FilterUsersPayload, SearchUsersPayload, etc.).
121
+ * Supports server-side search ({@link InterestedRostersRequestBody.searchByNameOrEmail})
122
+ * and pagination/sorting so the "Interested Rosters" table can page and sort
123
+ * large rosters without loading everything client-side.
124
+ */
125
+ export interface InterestedRostersRequestPayload {
126
+ payload: InterestedRostersRequestBody;
127
+ }
128
+ /**
129
+ * The body of {@link InterestedRostersRequestPayload}. All fields are sent to
130
+ * the backend to filter, page and sort the interested-users roster.
131
+ */
132
+ export interface InterestedRostersRequestBody {
133
+ /** Free-text search term matched against the requester's name or email. */
134
+ searchByNameOrEmail: string;
135
+ /** Zero-based row offset for the requested page. */
136
+ offset: number;
137
+ /** Number of rows per page. */
138
+ pageSize: number;
139
+ /** Column key to sort by (e.g. "name", "submittedOn"). */
140
+ sortedBy: string;
141
+ /** Sort direction (e.g. "asc" / "desc"). */
142
+ sortedOrderBy: string;
143
+ }
144
+ /**
145
+ * The `body` of the interested-users roster list response, as returned by
146
+ * {@code POST {apiBaseUrl}/groups/interest/group/{groupId}/responses}.
147
+ *
148
+ * The rows live under {@link data}; the remaining fields echo the paging state
149
+ * ({@link totalRecords}, {@link offset}, {@link pageSize}) so the table can
150
+ * drive pagination. The outer envelope ({@code status}/{@code message}/
151
+ * {@code body}) is unwrapped by the HTTP layer, so this models the `body` only.
152
+ */
153
+ export interface InterestedRostersResponse {
154
+ /** The interested-roster rows for the requested page. */
155
+ data: GroupInterestResponseView[];
156
+ /** Total number of interested-roster rows across all pages. */
157
+ totalRecords: number;
158
+ /** Zero-based row offset of the returned page. */
159
+ offset: number;
160
+ /** Number of rows per page used for this response. */
161
+ pageSize: number;
162
+ }
163
+ /**
164
+ * A single interested-roster row shown in the Roster Management "Interested
165
+ * Rosters" table. Carries the {@link interestId} needed to fetch that
166
+ * requester's stored responses via the interest-responses endpoint and to
167
+ * override their membership level. Rows are produced by mapping the backend
168
+ * {@link GroupInterestResponseView} list onto this flattened display shape.
169
+ */
170
+ export interface InterestedRoster {
171
+ /** The interest record id (e.g. "GINT_SAAPPS_..."), used for responses + override. */
172
+ interestId: string;
173
+ /** The interested user's id (e.g. "User_NESC_..."), used for the level override. */
174
+ userId: string;
175
+ /** Requester display name. */
176
+ name: string;
177
+ /** Requester email address. */
178
+ email: string;
179
+ /** Requester affiliations (organizations). */
180
+ affiliations: string[];
181
+ /** Submission date, preformatted for display. */
182
+ submittedOn: string;
183
+ /** Current/assigned membership level name, or null when none is assigned. */
184
+ memberLevel: string | null;
185
+ }
186
+ /**
187
+ * Request payload for overriding/setting an interested user's membership level
188
+ * ({@code POST {apiBaseUrl}/groups/interest/member-level}).
189
+ *
190
+ * Follows the repo payload convention of wrapping the request body in a
191
+ * {@code payload} key. Identifies the roster row by {@link UpdateMemberLevelBody.groupId}
192
+ * + {@link UpdateMemberLevelBody.userId} and carries the newly selected role in
193
+ * {@link UpdateMemberLevelBody.roleId}.
194
+ */
195
+ export interface UpdateMemberLevelPayload {
196
+ payload: UpdateMemberLevelBody;
197
+ }
198
+ /**
199
+ * The body of {@link UpdateMemberLevelPayload}: the group and user being
200
+ * updated plus the role id of the chosen membership level.
201
+ */
202
+ export interface UpdateMemberLevelBody {
203
+ /** The group the interested user belongs to (e.g. "GRP_SAAPPS_..."). */
204
+ groupId: string;
205
+ /** The interested user whose membership level is being set (e.g. "User_NESC_..."). */
206
+ userId: string;
207
+ /** The selected membership-level role id (e.g. "ROLE_SAAPPS_..."). */
208
+ roleId: string;
209
+ }
@@ -54,6 +54,18 @@ export interface RosterImportJob {
54
54
  successRows?: number;
55
55
  /** Rows skipped due to validation errors. */
56
56
  failedRows?: number;
57
+ /**
58
+ * When the import was created (ISO timestamp). Drives the "Timestamp" column
59
+ * in View History. Present only once the BE exposes it (currently @JsonIgnore);
60
+ * undefined otherwise, in which case the UI shows a placeholder.
61
+ */
62
+ createdOn?: string;
63
+ /**
64
+ * Who ran the import (user email/name). Drives the "Person Imported" column in
65
+ * View History. Present only once the BE exposes it (currently @JsonIgnore);
66
+ * undefined otherwise, in which case the UI shows a placeholder.
67
+ */
68
+ createdBy?: string;
57
69
  /**
58
70
  * Row-level errors from the BE `error_report` JSONB column. Observed to arrive
59
71
  * EITHER as a parsed object `{ rows: RosterImportErrorReport[] }` OR as a raw