@ieeesa-npm/groups 0.15.14 → 0.15.15

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.
@@ -0,0 +1,75 @@
1
+ import { EventEmitter, OnInit } from '@angular/core';
2
+ import { FormBuilder, FormGroup } from '@angular/forms';
3
+ import { CustomQuestion, QuestionFieldType } from '../../models/custom-question.model';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Editable card for a single custom interest-form question (SP3M-317). Renders
7
+ * the Question Label, Type, Required toggle, Placeholder, Help Text and (for
8
+ * choice types) Options fields. Emits save/cancel so the parent manages the
9
+ * questions list. The Options field is only shown for choice field types
10
+ * (SELECT / CHECKBOX / RADIO), matching the backend's choice-type validation.
11
+ * @export
12
+ * @class CustomQuestionEditorComponent
13
+ */
14
+ export declare class CustomQuestionEditorComponent implements OnInit {
15
+ private formBuilder;
16
+ constants: any;
17
+ editorTexts: any;
18
+ fieldType: typeof QuestionFieldType;
19
+ typeOptions: {
20
+ value: QuestionFieldType;
21
+ label: any;
22
+ }[];
23
+ question: CustomQuestion;
24
+ save: EventEmitter<CustomQuestion>;
25
+ cancel: EventEmitter<void>;
26
+ form: FormGroup;
27
+ constructor(formBuilder: FormBuilder);
28
+ /**
29
+ * Builds the reactive form, seeding values from the question being edited.
30
+ * The question's options are stored as a raw JSON string, so they are parsed
31
+ * into one-per-line text for editing.
32
+ * @memberof CustomQuestionEditorComponent
33
+ */
34
+ ngOnInit(): void;
35
+ /**
36
+ * Parses a stored options JSON string into one-per-line text for the textarea.
37
+ * @param {(string | undefined)} optionsJson
38
+ * @return {string}
39
+ * @memberof CustomQuestionEditorComponent
40
+ */
41
+ parseOptionsString(optionsJson: string | undefined): string;
42
+ /**
43
+ * Whether the currently selected type requires an options list.
44
+ * @return {boolean}
45
+ * @memberof CustomQuestionEditorComponent
46
+ */
47
+ get isChoiceType(): boolean;
48
+ /**
49
+ * Parses the options textarea into a trimmed, non-empty string array,
50
+ * stripping trailing commas.
51
+ * @return {string[]}
52
+ * @memberof CustomQuestionEditorComponent
53
+ */
54
+ parseOptions(): string[];
55
+ /**
56
+ * Whether the editor is valid: label and type are required, and choice types
57
+ * require at least one option.
58
+ * @return {boolean}
59
+ * @memberof CustomQuestionEditorComponent
60
+ */
61
+ get isSaveDisabled(): boolean;
62
+ /**
63
+ * Emits the assembled question on save. Options are serialized to a JSON
64
+ * string to match the backend's JSONB column.
65
+ * @memberof CustomQuestionEditorComponent
66
+ */
67
+ onSave(): void;
68
+ /**
69
+ * Emits the cancel event.
70
+ * @memberof CustomQuestionEditorComponent
71
+ */
72
+ onCancel(): void;
73
+ static ɵfac: i0.ɵɵFactoryDeclaration<CustomQuestionEditorComponent, never>;
74
+ static ɵcmp: i0.ɵɵComponentDeclaration<CustomQuestionEditorComponent, "ieeesa-custom-question-editor", never, { "question": { "alias": "question"; "required": false; }; }, { "save": "save"; "cancel": "cancel"; }, never, never, true, never>;
75
+ }
@@ -1,6 +1,12 @@
1
1
  import { OnInit } from '@angular/core';
2
- import { MatDialogRef } from '@angular/material/dialog';
2
+ import { MatDialog, MatDialogRef } from '@angular/material/dialog';
3
+ import { CdkDragDrop } from '@angular/cdk/drag-drop';
3
4
  import { RadioOption, SlideToggleOption } from '@ieeesa-npm/presentation';
5
+ import { AlertsService } from '@ieeesa-npm/utility';
6
+ import { CustomQuestion, GroupRole, QuestionFieldType } from '../../models/custom-question.model';
7
+ import { GroupInfo } from '../../models/group-info.model';
8
+ import { GroupsService } from '../../services/groups.service';
9
+ import { InterestFormService } from '../../services/interest-form.service';
4
10
  import * as i0 from "@angular/core";
5
11
  /**
6
12
  * Standard (locked) question row shown in the Interest Form tab.
@@ -14,16 +20,21 @@ export interface StandardQuestionRow {
14
20
  }
15
21
  /**
16
22
  * "Interest Form" tab content of the Roster Interests dialog (SP3M-317). The
17
- * group admin configures the interest form here: enable/disable the form, view
18
- * the locked standard questions, choose the default membership level and manage
19
- * custom questions. Interactive controls reuse the shared presentation
20
- * components (slide toggle, radio). Persistence via a mocked service and the
21
- * custom-question builder will be added in subsequent increments.
23
+ * group admin enables/disables the form, views locked standard questions,
24
+ * chooses the default membership level and manages custom questions
25
+ * (add / edit / reorder / remove), persisted to the groups backend via
26
+ * InterestFormService. Each question is saved individually (create/update) and
27
+ * the enable mode + default level are saved via the interest-config endpoint.
22
28
  * @export
23
29
  * @class InterestFormComponent
24
30
  */
25
31
  export declare class InterestFormComponent implements OnInit {
26
32
  private dialogRef;
33
+ group: GroupInfo;
34
+ private interestFormService;
35
+ private groupsService;
36
+ private alertService;
37
+ private dialog;
27
38
  constants: any;
28
39
  interestFormTexts: any;
29
40
  changesNotice: any;
@@ -35,41 +46,159 @@ export declare class InterestFormComponent implements OnInit {
35
46
  defaultMembershipDescription: any;
36
47
  customQuestionsTitle: any;
37
48
  addQuestionLabel: any;
49
+ requiredTag: any;
38
50
  standardTag: any;
51
+ newQuestionLabel: any;
52
+ editQuestionAriaLabel: any;
53
+ deleteQuestionAriaLabel: any;
54
+ noLevelsMessage: any;
55
+ typeLabels: any;
39
56
  cancelLabel: any;
40
57
  previewLabel: any;
41
58
  enableFormToggle: SlideToggleOption;
42
59
  standardQuestions: StandardQuestionRow[];
43
60
  membershipLevels: RadioOption[];
44
61
  isFormEnabled: boolean;
45
- constructor(dialogRef: MatDialogRef<unknown>);
62
+ selectedLevelId: string | null;
63
+ levels: GroupRole[];
64
+ customQuestions: CustomQuestion[];
65
+ editingQuestionId: string | null;
66
+ private isEditingNewQuestion;
67
+ private destroy$;
68
+ constructor(dialogRef: MatDialogRef<unknown>, group: GroupInfo, interestFormService: InterestFormService, groupsService: GroupsService, alertService: AlertsService, dialog: MatDialog);
46
69
  /**
47
- * Initializes the toggle, locked standard questions and default membership
48
- * level options from constants.
70
+ * Initializes static UI options and loads the group's interest config and
71
+ * custom questions from the backend.
49
72
  * @memberof InterestFormComponent
50
73
  */
51
74
  ngOnInit(): void;
52
75
  /**
53
- * Updates the form-enabled state when the toggle changes so the dependent
54
- * sections show or hide accordingly.
76
+ * Loads the real membership levels for the group's group type and then the
77
+ * saved interest configuration (so the saved default level can be
78
+ * preselected).
79
+ * @memberof InterestFormComponent
80
+ */
81
+ loadMembershipLevels(): void;
82
+ /**
83
+ * Loads the group's interest configuration (mode + default level) and syncs
84
+ * the toggle and the default-level radio selection.
85
+ * @memberof InterestFormComponent
86
+ */
87
+ loadConfig(): void;
88
+ /**
89
+ * Builds the radio options from the real membership levels, marking the
90
+ * currently selected level as checked.
91
+ * @memberof InterestFormComponent
92
+ */
93
+ buildMembershipLevelOptions(): void;
94
+ /**
95
+ * Loads the group's custom questions.
96
+ * @memberof InterestFormComponent
97
+ */
98
+ loadQuestions(): void;
99
+ /**
100
+ * Updates the form-enabled state and persists the new interest mode.
55
101
  * @param {SlideToggleOption} toggle
56
102
  * @memberof InterestFormComponent
57
103
  */
58
104
  onEnableFormToggle(toggle: SlideToggleOption): void;
105
+ /**
106
+ * Persists the interest config. The default level is only sent for the
107
+ * enabled (FORM) mode; DISABLED clears it. Enabling without a selected level
108
+ * is skipped so an invalid request is never sent.
109
+ * @memberof InterestFormComponent
110
+ */
111
+ saveConfig(): void;
112
+ /**
113
+ * Records the selected default membership level and persists the config when
114
+ * the form is enabled.
115
+ * @param {RadioOption} option
116
+ * @memberof InterestFormComponent
117
+ */
118
+ onMembershipLevelChange(option: RadioOption): void;
119
+ /**
120
+ * Adds a new blank question and opens its editor. Only one editor is open at
121
+ * a time.
122
+ * @memberof InterestFormComponent
123
+ */
124
+ onAddQuestion(): void;
125
+ /**
126
+ * Opens the editor for an existing question.
127
+ * @param {CustomQuestion} question
128
+ * @memberof InterestFormComponent
129
+ */
130
+ onEditQuestion(question: CustomQuestion): void;
131
+ /**
132
+ * Creates or updates the question on the backend and refreshes the list.
133
+ * @param {CustomQuestion} question
134
+ * @memberof InterestFormComponent
135
+ */
136
+ onSaveQuestion(question: CustomQuestion): void;
137
+ /**
138
+ * Closes the editor and reloads the questions after a successful save.
139
+ * @memberof InterestFormComponent
140
+ */
141
+ private afterQuestionSaved;
142
+ /**
143
+ * Closes the editor. A newly added question is removed on cancel; an existing
144
+ * question keeps its previous values.
145
+ * @param {string} questionId
146
+ * @memberof InterestFormComponent
147
+ */
148
+ onCancelQuestion(questionId: string | undefined): void;
149
+ /**
150
+ * Removes a question on the backend and refreshes the list.
151
+ * @param {string} questionId
152
+ * @memberof InterestFormComponent
153
+ */
154
+ onDeleteQuestion(questionId: string | undefined): void;
155
+ /**
156
+ * Reorders questions on drag-drop and persists the new order.
157
+ * @param {CdkDragDrop<CustomQuestion[]>} event
158
+ * @memberof InterestFormComponent
159
+ */
160
+ onReorderQuestions(event: CdkDragDrop<CustomQuestion[]>): void;
161
+ /**
162
+ * Returns the display label for a question's field type.
163
+ * @param {(QuestionFieldType | null)} type
164
+ * @return {string}
165
+ * @memberof InterestFormComponent
166
+ */
167
+ getTypeLabel(type: QuestionFieldType | null): string;
168
+ /**
169
+ * Shows an error alert for a failed interest-form operation.
170
+ * @param {*} error
171
+ * @memberof InterestFormComponent
172
+ */
173
+ handleError(error: any): void;
174
+ /**
175
+ * Opens the interest form in read-only preview mode (SP3M-317 R6.7), reusing
176
+ * the shared express-interest dialog. Shows the admin's current custom
177
+ * questions as an end user would see them; no submit, Cancel only.
178
+ * @memberof InterestFormComponent
179
+ */
180
+ onPreview(): void;
59
181
  /**
60
182
  * Closes the Roster Interests dialog (Cancel action).
61
183
  * @memberof InterestFormComponent
62
184
  */
63
185
  onCancel(): void;
64
186
  /**
65
- * Returns the unique identifier for a standard question row for ngFor
66
- * tracking.
187
+ * ngFor tracking for standard question rows.
67
188
  * @param {number} index
68
189
  * @param {StandardQuestionRow} question
69
190
  * @return {number}
70
191
  * @memberof InterestFormComponent
71
192
  */
72
193
  trackByFn(index: number, question: StandardQuestionRow): number;
73
- static ɵfac: i0.ɵɵFactoryDeclaration<InterestFormComponent, [{ optional: true; }]>;
194
+ /**
195
+ * ngFor tracking for custom questions.
196
+ * @param {number} index
197
+ * @param {CustomQuestion} question
198
+ * @return {string}
199
+ * @memberof InterestFormComponent
200
+ */
201
+ trackByQuestionId(index: number, question: CustomQuestion): string;
202
+ static ɵfac: i0.ɵɵFactoryDeclaration<InterestFormComponent, [{ optional: true; }, { optional: true; }, null, null, null, null]>;
74
203
  static ɵcmp: i0.ɵɵComponentDeclaration<InterestFormComponent, "ieeesa-interest-form", never, {}, {}, never, never, true, never>;
75
204
  }
@@ -0,0 +1,37 @@
1
+ import { MatDialogRef } from '@angular/material/dialog';
2
+ import { InterestFormQuestion } from '@ieeesa-npm/models';
3
+ import { CustomQuestion } from '../../models/custom-question.model';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Data passed to the preview dialog: the group name (subtitle) and the admin's
7
+ * current custom questions.
8
+ */
9
+ export interface InterestFormPreviewData {
10
+ groupName: string;
11
+ customQuestions: CustomQuestion[];
12
+ }
13
+ /**
14
+ * Preview host for the interest form (SP3M-317 R6.7). Reuses the shared,
15
+ * presentational ExpressInterestDialogComponent in read-only preview mode so
16
+ * the admin sees the form exactly as an end user would, without any submit
17
+ * action — only Cancel closes the dialog. No backend call is made; the admin's
18
+ * in-progress custom questions are passed straight through.
19
+ * @export
20
+ * @class InterestFormPreviewComponent
21
+ */
22
+ export declare class InterestFormPreviewComponent {
23
+ dialogRef: MatDialogRef<InterestFormPreviewComponent>;
24
+ private data;
25
+ constants: any;
26
+ previewTexts: any;
27
+ subtitle: string;
28
+ customQuestions: InterestFormQuestion[];
29
+ constructor(dialogRef: MatDialogRef<InterestFormPreviewComponent>, data: InterestFormPreviewData);
30
+ /**
31
+ * Closes the preview dialog.
32
+ * @memberof InterestFormPreviewComponent
33
+ */
34
+ onClose(): void;
35
+ static ɵfac: i0.ɵɵFactoryDeclaration<InterestFormPreviewComponent, never>;
36
+ static ɵcmp: i0.ɵɵComponentDeclaration<InterestFormPreviewComponent, "ieeesa-interest-form-preview", never, {}, {}, never, never, true, never>;
37
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Supported field types for a custom interest-form question (SP3M-317). Values
3
+ * match the backend FieldType enum / field_type CHECK constraint exactly.
4
+ */
5
+ export declare enum QuestionFieldType {
6
+ TEXT = "TEXT",
7
+ TEXTAREA = "TEXTAREA",
8
+ SELECT = "SELECT",
9
+ CHECKBOX = "CHECKBOX",
10
+ RADIO = "RADIO",
11
+ DATE = "DATE"
12
+ }
13
+ /**
14
+ * Field types that require a configurable list of options
15
+ * (SELECT / CHECKBOX / RADIO), matching the backend's choice-type validation.
16
+ */
17
+ export declare const CHOICE_FIELD_TYPES: QuestionFieldType[];
18
+ /**
19
+ * Interest-expression mode for a group (matches backend InterestMode).
20
+ */
21
+ export declare enum InterestMode {
22
+ DISABLED = "DISABLED",
23
+ BASIC = "BASIC",
24
+ FORM = "FORM"
25
+ }
26
+ /**
27
+ * A single admin-configured custom question on a group's interest form. Field
28
+ * names mirror the backend InterestFormQuestion entity so payloads map directly.
29
+ */
30
+ export interface CustomQuestion {
31
+ questionId?: string;
32
+ groupId: string;
33
+ questionLabel: string;
34
+ fieldType: QuestionFieldType | null;
35
+ placeholder?: string;
36
+ helpText?: string;
37
+ isRequired: boolean;
38
+ viewOrder: number;
39
+ options?: string;
40
+ }
41
+ /**
42
+ * Lightweight transfer object for reordering questions (matches backend
43
+ * QuestionOrder).
44
+ */
45
+ export interface QuestionOrder {
46
+ questionId: string;
47
+ viewOrder: number;
48
+ }
49
+ /**
50
+ * Group interest configuration (matches backend GroupInterestConfig).
51
+ */
52
+ export interface GroupInterestConfig {
53
+ groupId: string;
54
+ interestMode: string;
55
+ defaultLevelId: string | null;
56
+ }
57
+ /**
58
+ * A role entry returned by GET /groups/role/{appId}. Member levels are the
59
+ * subset where roleTypeName === "Member Level"; their roleId is used as the
60
+ * interest-config defaultLevelId and roleName is the display label.
61
+ */
62
+ export interface GroupRole {
63
+ roleId: string;
64
+ roleType: string;
65
+ roleName: string;
66
+ roleTypeName: string;
67
+ viewOrder?: number;
68
+ }
69
+ /**
70
+ * roleTypeName value identifying member-level roles.
71
+ */
72
+ export declare const MEMBER_LEVEL_ROLE_TYPE = "Member Level";
@@ -0,0 +1,105 @@
1
+ import { Observable } from 'rxjs';
2
+ import { HttpService } from '@ieeesa-npm/utility';
3
+ import { CustomQuestion, GroupInterestConfig, GroupRole, QuestionOrder } from '../models/custom-question.model';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Standard API response envelope used by the groups backend.
7
+ */
8
+ export interface InterestApiResponse<T> {
9
+ status: boolean | string;
10
+ message?: string;
11
+ body?: T;
12
+ }
13
+ /**
14
+ * Service for managing a group's interest form configuration and custom
15
+ * questions (SP3M-317). Talks to the groups backend endpoints:
16
+ * - GET /v1/groups/interest-config/{groupId}
17
+ * - PUT /v1/groups/interest-config
18
+ * - GET /v1/groups/interest-questions/{groupId}
19
+ * - POST /v1/groups/interest-questions
20
+ * - PUT /v1/groups/interest-questions
21
+ * - PUT /v1/groups/interest-questions/reorder
22
+ * - PUT /v1/groups/interest-questions/{questionId}/remove
23
+ * - GET /v1/groups/interest-questions/{groupId}/preview
24
+ * All write payloads are wrapped in the shared Request envelope ({ payload }).
25
+ * @export
26
+ * @class InterestFormService
27
+ */
28
+ export declare class InterestFormService {
29
+ private httpService;
30
+ apiBaseUrl: string;
31
+ constructor(httpService: HttpService);
32
+ /**
33
+ * Sets the API base URL (resolved from the hosting application's config).
34
+ * @param {string} apiBaseUrl
35
+ * @memberof InterestFormService
36
+ */
37
+ setApiBaseUrl(apiBaseUrl: string): void;
38
+ /**
39
+ * Loads a group's interest configuration (mode + default member level).
40
+ * @param {string} groupId
41
+ * @return {Observable<InterestApiResponse<GroupInterestConfig>>}
42
+ * @memberof InterestFormService
43
+ */
44
+ getInterestConfig(groupId: string): Observable<InterestApiResponse<GroupInterestConfig>>;
45
+ /**
46
+ * Loads the tenant's roles. Member levels (used to populate the default
47
+ * membership level options) are the subset where roleTypeName is
48
+ * "Member Level"; filtering is done by the caller.
49
+ * @param {string} appId
50
+ * @return {Observable<InterestApiResponse<GroupRole[]>>}
51
+ * @memberof InterestFormService
52
+ */
53
+ getRoles(appId: string): Observable<InterestApiResponse<GroupRole[]>>;
54
+ /**
55
+ * Saves a group's interest configuration.
56
+ * @param {GroupInterestConfig} config
57
+ * @return {Observable<InterestApiResponse<string>>}
58
+ * @memberof InterestFormService
59
+ */
60
+ saveInterestConfig(config: GroupInterestConfig): Observable<InterestApiResponse<string>>;
61
+ /**
62
+ * Loads a group's custom interest questions (ordered by view order).
63
+ * @param {string} groupId
64
+ * @return {Observable<InterestApiResponse<CustomQuestion[]>>}
65
+ * @memberof InterestFormService
66
+ */
67
+ getQuestions(groupId: string): Observable<InterestApiResponse<CustomQuestion[]>>;
68
+ /**
69
+ * Creates a new custom interest question.
70
+ * @param {CustomQuestion} question
71
+ * @return {Observable<InterestApiResponse<CustomQuestion>>}
72
+ * @memberof InterestFormService
73
+ */
74
+ createQuestion(question: CustomQuestion): Observable<InterestApiResponse<CustomQuestion>>;
75
+ /**
76
+ * Updates an existing custom interest question.
77
+ * @param {CustomQuestion} question
78
+ * @return {Observable<InterestApiResponse<string>>}
79
+ * @memberof InterestFormService
80
+ */
81
+ updateQuestion(question: CustomQuestion): Observable<InterestApiResponse<string>>;
82
+ /**
83
+ * Persists the updated view order for a group's custom questions.
84
+ * @param {QuestionOrder[]} questionOrders
85
+ * @return {Observable<InterestApiResponse<string>>}
86
+ * @memberof InterestFormService
87
+ */
88
+ reorderQuestions(questionOrders: QuestionOrder[]): Observable<InterestApiResponse<string>>;
89
+ /**
90
+ * Soft-deletes a custom interest question by id.
91
+ * @param {string} questionId
92
+ * @return {Observable<InterestApiResponse<string>>}
93
+ * @memberof InterestFormService
94
+ */
95
+ removeQuestion(questionId: string): Observable<InterestApiResponse<string>>;
96
+ /**
97
+ * Builds a non-publishing preview of a group's interest form.
98
+ * @param {string} groupId
99
+ * @return {Observable<InterestApiResponse<unknown>>}
100
+ * @memberof InterestFormService
101
+ */
102
+ previewForm(groupId: string): Observable<InterestApiResponse<unknown>>;
103
+ static ɵfac: i0.ɵɵFactoryDeclaration<InterestFormService, never>;
104
+ static ɵprov: i0.ɵɵInjectableDeclaration<InterestFormService>;
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ieeesa-npm/groups",
3
- "version": "0.15.14",
3
+ "version": "0.15.15",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^16.1.0",
6
6
  "@angular/core": "^16.1.0"
package/public-api.d.ts CHANGED
@@ -8,6 +8,10 @@ export * from './lib/components/roster-import-history/roster-import-history.comp
8
8
  export * from './lib/components/interest-form-management/interest-form-management.component';
9
9
  export * from './lib/components/interested-rosters/interested-rosters.component';
10
10
  export * from './lib/components/interest-form/interest-form.component';
11
+ export * from './lib/components/custom-question-editor/custom-question-editor.component';
12
+ export * from './lib/components/interest-form-preview/interest-form-preview.component';
13
+ export * from './lib/models/custom-question.model';
14
+ export * from './lib/services/interest-form.service';
11
15
  export * from './lib/groups.component';
12
16
  export * from './lib/models/create-group.model';
13
17
  export * from './lib/models/group-action.model';