@dcrackel/hematournamentui 1.0.708 → 1.0.710

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 (38) hide show
  1. package/README.md +148 -106
  2. package/dist/HemaTournamentUI-lib.es.js +6678 -6196
  3. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  4. package/dist/hematournamentui-tailwind.css +16 -6
  5. package/package.json +1 -1
  6. package/src/stories/Atoms/Input/BaseInput.test.js +97 -85
  7. package/src/stories/Atoms/Input/BaseInput.vue +149 -150
  8. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -20
  9. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -0
  10. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -133
  11. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +48 -41
  12. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +227 -206
  13. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -225
  14. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  15. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +133 -91
  16. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.test.js +191 -0
  17. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.vue +366 -309
  18. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -171
  19. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -215
  20. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -0
  21. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -52
  22. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -75
  23. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +14 -0
  24. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -0
  25. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +13 -3
  26. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -172
  27. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -103
  28. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -0
  29. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -213
  30. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -0
  31. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -83
  32. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +89 -0
  33. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -360
  34. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -113
  35. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +292 -280
  36. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -29
  37. package/src/stories/Util/Validations/PriceValidation.js +6 -0
  38. package/src/stories/Util/Validations/TournamentFieldsValidations.js +4 -0
@@ -1,213 +1,329 @@
1
- <template>
2
- <section v-if="loading">
3
- <PersonManagementSkeleton />
4
- </section>
5
- <section v-if="!loading" class="flex px-3 bg-neutral" id="person-management">
6
- <div class="rounded-lg md:p-9 w-full">
7
- <div class="w-full flex flex-row justify-center mb-4">
8
- <BaseText :color="'primaryHighlight'" :size="'2xl'" :text="getHeaderText" :weight="'normal'" />
9
- </div>
10
- <div class="w-full flex-row justify-center mb-4 flex drop-shadow">
11
- <ImageCropper
12
- :urlToImage="getImageURL"
13
- :uploadImageName="uploadImageName"
14
- :uploadServer="uploadServer"
15
- :imageServer="imageServer"
16
- :customClass="getClass"
17
- :isProfile="true"
18
- customSkeletonClass="w-32 h-32 border-2 border-dashed rounded-full animate-pulse bg-quaternaryHighlight"
19
- :cropperOptions="{
20
- aspectRatio: 4 / 4,
21
- autoCropArea: 1,
22
- viewMode: 1,
23
- movable: true,
24
- zoomable: true
25
- }"
26
- @update:url="handleProfilePicUpdate"
27
- />
28
- </div>
29
-
30
- <EditEventsTopMenu :tabs="personProfileTabs(userLevel)" :currentTab="currentTab" @tabMenuClick="handleTab" />
31
- <BasicEdit
32
- v-if="currentTab === 'Basic'"
33
- :person="person"
34
- :fencingClubs="fencingClubs"
35
- :validation="validation"
36
- @update:person="updatePerson"
37
- />
38
- <FencerAdmin
39
- v-if="currentTab === 'Admin'"
40
- :person="person"
41
- :people="people"
42
- :userLevel="userLevel"
43
- @update:person="updatePerson"
44
- @merge:merge="doMerge"
45
- />
46
- <Attendance v-if="currentTab === 'Stats'" :attendance="attendance" />
47
- <Attendance v-if="currentTab === 'Attendance'" :attendance="attendance" />
48
-
49
- <div v-if="showButtons" class="flex flex-row justify-end mt-8">
50
- <BaseButton
51
- v-if="userLevel === 'Admin'"
52
- class="mr-8"
53
- color="warn"
54
- label="Delete"
55
- size="sm"
56
- type="secondary"
57
- @buttonClick="handleDelete"
58
- />
59
- <BaseButton
60
- class="w-full"
61
- color="neutral"
62
- :label="saveButtonLabel"
63
- size="sm"
64
- type="secondary"
65
- @buttonClick="handleSubmit"
66
- />
67
- </div>
68
- </div>
69
- </section>
70
- </template>
71
-
72
- <script>
73
- import ImageCropper from '../../../stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue';
74
- import EditEventsTopMenu from '../../Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue';
75
- import { personProfileTabs } from '../../Util/tabs.js';
76
- import PersonManagementSkeleton from '../../Organisms/Skeletons/PersonManagement/PersonManagementSkeleton.vue';
77
- import BaseText from '../../Atoms/Text/BaseText.vue';
78
- import BaseButton from '../../Molecules/Buttons/BaseButton/BaseButton.vue';
79
- import BasicEdit from './Basic/BasicEdit.vue';
80
- import Attendance from '../../Templates/PersonManagement/Attendance/Attendance.vue';
81
- import FencerAdmin from '../../Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue';
82
- import { validateField } from '../../Util/Validations/PersonManagementValidations.js';
83
-
84
- export default {
85
- name: 'PersonManagement',
86
- emits: ['submit:person', 'delete:person', 'update:image', 'merge:merge'],
87
- components: {
88
- FencerAdmin,
89
- Attendance,
90
- BasicEdit,
91
- BaseButton,
92
- BaseText,
93
- PersonManagementSkeleton,
94
- EditEventsTopMenu,
95
- ImageCropper
96
- },
97
- props: {
98
- personProfile: {
99
- type: Object,
100
- default: () => ({})
101
- },
102
- fencingClubs: {
103
- type: Array,
104
- default: () => []
105
- },
106
- people: {
107
- type: Array,
108
- default: () => []
109
- },
110
- attendance: {
111
- type: Array,
112
- default: () => []
113
- },
114
- uploadImageName: {
115
- type: String,
116
- default: 'profile-pic'
117
- },
118
- uploadServer: {
119
- type: String
120
- },
121
- imageServer: {
122
- type: String
123
- },
124
- userLevel: {
125
- type: String,
126
- default: 'user'
127
- },
128
- loading: {
129
- type: Boolean,
130
- default: false
131
- },
132
- saveButtonLabel: {
133
- type: String,
134
- default: 'Save'
135
- }
136
- },
137
- data() {
138
- const firstName = this.personProfile.FirstName || '';
139
- const lastName = this.personProfile.LastName || '';
140
- return {
141
- person: { ...this.personProfile, LegalName: this.personProfile.LegalName || `${firstName} ${lastName}`.trim() },
142
- currentTab: 'Basic',
143
- validation: {
144
- DisplayName: { invalid: false, message: '' },
145
- LegalName: { invalid: false, message: '' },
146
- EmailAddress: { invalid: false, message: '' },
147
- Club: { invalid: false, message: '' }
148
- }
149
- };
150
- },
151
- computed: {
152
- getHeaderText() {
153
- return this.person.DisplayName ? this.person.DisplayName : 'Fencer Profile';
154
- },
155
- getImageURL() {
156
- return this.person.Images ? this.person.Images[0].URL : '';
157
- },
158
- getClass() {
159
- return 'border-2 border-dashed rounded-full w-32 h-32';
160
- },
161
- showButtons() {
162
- return (
163
- ((this.userLevel === 'Admin' || this.userLevel === 'Club') &&
164
- (this.currentTab === 'Basic' || this.currentTab === 'Admin')) ||
165
- (this.userLevel === 'User' && this.currentTab === 'Basic')
166
- );
167
- }
168
- },
169
- methods: {
170
- personProfileTabs,
171
- handleProfilePicUpdate(url) {
172
- this.person.ProfileImage = url;
173
- this.$emit('update:image', url);
174
- },
175
- setClub() {
176
- this.person.ClubId = this.person.Club?.ClubId ?? 17;
177
- },
178
- handleSubmit() {
179
- this.setClub();
180
- if (this.validateAllFields()) {
181
- const nameParts = this.person.LegalName.trim().split(' ');
182
- this.person.FirstName = nameParts[0] || '';
183
- this.person.LastName = nameParts.slice(1).join(' ') || '';
184
- this.$emit('submit:person', this.person);
185
- }
186
- },
187
- handleDelete() {
188
- this.$emit('delete:person', this.person);
189
- },
190
- updatePerson({ field, value }) {
191
- this.person = { ...this.person, [field]: value };
192
-
193
- if (this.validation[field]) {
194
- this.validation[field] = validateField(field, value);
195
- }
196
- },
197
- validateAllFields() {
198
- const fieldsToValidate = ['DisplayName', 'LegalName', 'EmailAddress'];
199
- fieldsToValidate.forEach(field => {
200
- this.validation[field] = validateField(field, this.person[field]);
201
- });
202
-
203
- return !Object.values(this.validation).some(field => field.invalid);
204
- },
205
- handleTab(tabId) {
206
- this.currentTab = tabId;
207
- },
208
- doMerge(data) {
209
- this.$emit('merge:merge', data);
210
- }
211
- }
212
- };
213
- </script>
1
+ <template>
2
+ <section v-if="loading">
3
+ <PersonManagementSkeleton />
4
+ </section>
5
+ <section
6
+ v-if="!loading"
7
+ class="flex flex-col px-3 bg-neutral"
8
+ id="person-management"
9
+ :aria-busy="saving || imageUploading"
10
+ >
11
+ <p v-if="saving" role="status" class="text-quinary text-sm">Saving profile…</p>
12
+ <fieldset
13
+ :key="draftVersion"
14
+ :disabled="isFormDisabled"
15
+ :inert="isFormDisabled"
16
+ class="min-w-0 rounded-lg md:p-9 w-full border-0 p-0"
17
+ >
18
+ <div class="w-full flex flex-row justify-center mb-4">
19
+ <BaseText :color="'primaryHighlight'" :size="'2xl'" :text="getHeaderText" :weight="'normal'" />
20
+ </div>
21
+ <div class="w-full flex-row justify-center mb-4 flex drop-shadow">
22
+ <ImageCropper
23
+ :urlToImage="getImageURL"
24
+ :uploadImageName="uploadImageName"
25
+ :uploadServer="uploadServer"
26
+ :imageServer="imageServer"
27
+ :customClass="getClass"
28
+ :isProfile="true"
29
+ :disabled="isFormDisabled"
30
+ customSkeletonClass="w-32 h-32 border-2 border-dashed rounded-full animate-pulse bg-quaternaryHighlight"
31
+ :cropperOptions="{
32
+ aspectRatio: 4 / 4,
33
+ autoCropArea: 1,
34
+ viewMode: 1,
35
+ movable: true,
36
+ zoomable: true
37
+ }"
38
+ @update:url="handleProfilePicUpdate"
39
+ @update:uploading="handleImageUploading"
40
+ @error:load="handleImageLoadError"
41
+ @error:upload="handleImageUploadError"
42
+ />
43
+ </div>
44
+
45
+ <p v-if="imageUploading" role="status" class="mb-4 text-sm text-quinary">Uploading profile image…</p>
46
+ <p v-if="imageErrorMessage" role="alert" class="mb-4 text-sm text-alarm">{{ imageErrorMessage }}</p>
47
+
48
+ <EditEventsTopMenu :tabs="personProfileTabs(userLevel)" :currentTab="currentTab" @tabMenuClick="handleTab" />
49
+ <div
50
+ v-if="showValidationSummary"
51
+ role="alert"
52
+ data-testid="profile-validation-summary"
53
+ class="my-4 rounded-lg border border-alarm p-3 text-sm text-alarm"
54
+ >
55
+ <p class="font-semibold">Please correct these fields before saving:</p>
56
+ <ul class="mt-2 list-disc pl-5">
57
+ <li v-for="error in validationErrors" :key="error.field">
58
+ <button
59
+ type="button"
60
+ class="text-left underline"
61
+ :disabled="!canOpenTab(error.tab)"
62
+ @click="handleTab(error.tab)"
63
+ >
64
+ {{ error.label }}: {{ error.message }} ({{ error.tab }} tab)
65
+ </button>
66
+ </li>
67
+ </ul>
68
+ </div>
69
+ <BasicEdit
70
+ v-if="currentTab === 'Basic'"
71
+ :person="person"
72
+ :fencingClubs="fencingClubs"
73
+ :validation="validation"
74
+ @update:person="updatePerson"
75
+ />
76
+ <FencerAdmin
77
+ v-if="currentTab === 'Admin'"
78
+ :person="person"
79
+ :people="people"
80
+ :userLevel="userLevel"
81
+ @update:person="updatePerson"
82
+ @merge:merge="doMerge"
83
+ />
84
+ <Attendance v-if="currentTab === 'Stats'" :attendance="attendance" />
85
+ <Attendance v-if="currentTab === 'Attendance'" :attendance="attendance" />
86
+
87
+ <div v-if="showButtons" class="flex flex-row justify-end mt-8">
88
+ <BaseButton
89
+ v-if="userLevel === 'Admin'"
90
+ class="mr-8"
91
+ color="warn"
92
+ label="Delete"
93
+ size="sm"
94
+ type="secondary"
95
+ :disabled="isFormDisabled"
96
+ @buttonClick="handleDelete"
97
+ />
98
+ <BaseButton
99
+ class="w-full"
100
+ color="neutral"
101
+ :label="saveButtonLabel"
102
+ size="sm"
103
+ type="secondary"
104
+ :disabled="isSaveDisabled"
105
+ @buttonClick="handleSubmit"
106
+ />
107
+ </div>
108
+ </fieldset>
109
+ </section>
110
+ </template>
111
+
112
+ <script>
113
+ import ImageCropper from '../../../stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue';
114
+ import EditEventsTopMenu from '../../Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue';
115
+ import { personProfileTabs } from '../../Util/tabs.js';
116
+ import PersonManagementSkeleton from '../../Organisms/Skeletons/PersonManagement/PersonManagementSkeleton.vue';
117
+ import BaseText from '../../Atoms/Text/BaseText.vue';
118
+ import BaseButton from '../../Molecules/Buttons/BaseButton/BaseButton.vue';
119
+ import BasicEdit from './Basic/BasicEdit.vue';
120
+ import Attendance from '../../Templates/PersonManagement/Attendance/Attendance.vue';
121
+ import FencerAdmin from '../../Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue';
122
+ import { validateField } from '../../Util/Validations/PersonManagementValidations.js';
123
+ import {
124
+ createPersonDraft,
125
+ createPersonValidation,
126
+ getDraftHemaRatingsId,
127
+ profileFields
128
+ } from './PersonManagementDraft.js';
129
+
130
+ export default {
131
+ name: 'PersonManagement',
132
+ emits: ['submit:person', 'delete:person', 'update:image', 'merge:merge', 'error:image'],
133
+ components: {
134
+ FencerAdmin,
135
+ Attendance,
136
+ BasicEdit,
137
+ BaseButton,
138
+ BaseText,
139
+ PersonManagementSkeleton,
140
+ EditEventsTopMenu,
141
+ ImageCropper
142
+ },
143
+ props: {
144
+ personProfile: {
145
+ type: Object,
146
+ default: () => ({})
147
+ },
148
+ fencingClubs: {
149
+ type: Array,
150
+ default: () => []
151
+ },
152
+ people: {
153
+ type: Array,
154
+ default: () => []
155
+ },
156
+ attendance: {
157
+ type: Array,
158
+ default: () => []
159
+ },
160
+ uploadImageName: {
161
+ type: String,
162
+ default: 'profile-pic'
163
+ },
164
+ uploadServer: {
165
+ type: String
166
+ },
167
+ imageServer: {
168
+ type: String
169
+ },
170
+ userLevel: {
171
+ type: String,
172
+ default: 'user'
173
+ },
174
+ loading: {
175
+ type: Boolean,
176
+ default: false
177
+ },
178
+ saveButtonLabel: {
179
+ type: String,
180
+ default: 'Save'
181
+ },
182
+ saving: {
183
+ type: Boolean,
184
+ default: false
185
+ },
186
+ disabled: {
187
+ type: Boolean,
188
+ default: false
189
+ },
190
+ draftResetKey: {
191
+ type: [String, Number],
192
+ default: 0
193
+ }
194
+ },
195
+ data() {
196
+ return {
197
+ person: createPersonDraft(this.personProfile),
198
+ currentTab: 'Basic',
199
+ validation: createPersonValidation(),
200
+ draftVersion: 0,
201
+ submitAttempted: false,
202
+ imageUploading: false,
203
+ imageErrorMessage: ''
204
+ };
205
+ },
206
+ computed: {
207
+ profileIdentity() {
208
+ const id = this.personProfile?.PersonId;
209
+ return id === undefined || id === null ? null : String(id);
210
+ },
211
+ isFormDisabled() {
212
+ return this.saving || this.disabled;
213
+ },
214
+ isSaveDisabled() {
215
+ return this.isFormDisabled || this.imageUploading;
216
+ },
217
+ validationErrors() {
218
+ return profileFields
219
+ .filter(({ field }) => this.validation[field]?.invalid)
220
+ .map(field => ({ ...field, message: this.validation[field.field].message }));
221
+ },
222
+ showValidationSummary() {
223
+ return this.submitAttempted && this.validationErrors.length > 0;
224
+ },
225
+ getHeaderText() {
226
+ return this.person.DisplayName ? this.person.DisplayName : 'Fencer Profile';
227
+ },
228
+ getImageURL() {
229
+ if (this.person.ProfileImage) return this.person.ProfileImage;
230
+ const images = Array.isArray(this.person.Images) ? this.person.Images : [];
231
+ const portrait = images.find(image => image?.Type === 'Portrait') ?? images[0];
232
+ return portrait?.URL ?? '';
233
+ },
234
+ getClass() {
235
+ return 'border-2 border-dashed rounded-full w-32 h-32';
236
+ },
237
+ showButtons() {
238
+ const role = this.userLevel.toLowerCase();
239
+ return (
240
+ ((role === 'admin' || role === 'club') && (this.currentTab === 'Basic' || this.currentTab === 'Admin')) ||
241
+ (role === 'user' && this.currentTab === 'Basic')
242
+ );
243
+ }
244
+ },
245
+ watch: {
246
+ profileIdentity() {
247
+ this.currentTab = 'Basic';
248
+ this.resetDraft();
249
+ },
250
+ draftResetKey() {
251
+ this.resetDraft();
252
+ }
253
+ },
254
+ methods: {
255
+ personProfileTabs,
256
+ resetDraft() {
257
+ this.person = createPersonDraft(this.personProfile);
258
+ this.validation = createPersonValidation();
259
+ this.submitAttempted = false;
260
+ this.imageUploading = false;
261
+ this.imageErrorMessage = '';
262
+ this.draftVersion += 1;
263
+ },
264
+ handleProfilePicUpdate(url) {
265
+ if (typeof url !== 'string' || !url.trim()) return;
266
+ this.person = { ...this.person, ProfileImage: url };
267
+ this.imageErrorMessage = '';
268
+ this.$emit('update:image', url);
269
+ },
270
+ handleImageUploading(uploading) {
271
+ this.imageUploading = uploading;
272
+ if (uploading) this.imageErrorMessage = '';
273
+ },
274
+ handleImageLoadError(error) {
275
+ this.reportImageError('load', error);
276
+ },
277
+ handleImageUploadError(error) {
278
+ this.imageUploading = false;
279
+ this.reportImageError('upload', error);
280
+ },
281
+ reportImageError(type, error) {
282
+ this.imageErrorMessage = error?.message || 'Could not update the profile image.';
283
+ this.$emit('error:image', { ...error, type, message: this.imageErrorMessage });
284
+ },
285
+ handleSubmit() {
286
+ if (this.isSaveDisabled) return;
287
+ this.submitAttempted = true;
288
+ if (!this.validateAllFields()) return;
289
+ const person = createPersonDraft(this.person);
290
+ const nameParts = person.LegalName.trim().split(/\s+/);
291
+ person.ClubId = person.Club?.ClubId ?? 17;
292
+ person.FirstName = nameParts[0] || '';
293
+ person.LastName = nameParts.slice(1).join(' ');
294
+ this.$emit('submit:person', person);
295
+ },
296
+ handleDelete() {
297
+ if (this.isFormDisabled) return;
298
+ this.$emit('delete:person', createPersonDraft(this.person));
299
+ },
300
+ updatePerson({ field, value }) {
301
+ if (this.isFormDisabled) return;
302
+ this.person = { ...this.person, [field]: value };
303
+
304
+ if (this.validation[field]) {
305
+ this.validation[field] = validateField(field, value);
306
+ }
307
+ },
308
+ validateAllFields() {
309
+ profileFields.forEach(({ field }) => {
310
+ const value = field === 'HemaRatingsId' ? getDraftHemaRatingsId(this.person) : this.person[field];
311
+ this.validation[field] = validateField(field, value);
312
+ });
313
+
314
+ return !Object.values(this.validation).some(field => field.invalid);
315
+ },
316
+ handleTab(tabId) {
317
+ if (this.isFormDisabled || !this.canOpenTab(tabId)) return;
318
+ this.currentTab = tabId;
319
+ },
320
+ canOpenTab(tabId) {
321
+ return personProfileTabs(this.userLevel).some(tab => tab.id === tabId);
322
+ },
323
+ doMerge(data) {
324
+ if (this.isFormDisabled) return;
325
+ this.$emit('merge:merge', data);
326
+ }
327
+ }
328
+ };
329
+ </script>
@@ -0,0 +1,42 @@
1
+ export const profileFields = [
2
+ { field: 'DisplayName', label: 'Display name', tab: 'Basic' },
3
+ { field: 'LegalName', label: 'Legal name', tab: 'Basic' },
4
+ { field: 'EmailAddress', label: 'Email address', tab: 'Basic' },
5
+ { field: 'HemaRatingsId', label: 'HEMA Ratings ID', tab: 'Admin' }
6
+ ];
7
+
8
+ export function getDraftLegalName(person) {
9
+ if (person?.LegalName !== undefined) return String(person.LegalName ?? '');
10
+ return [person?.FirstName, person?.LastName].filter(Boolean).join(' ');
11
+ }
12
+
13
+ function isPrimaryEmail(email) {
14
+ return email?.IsPrimary === true;
15
+ }
16
+
17
+ function getPrimaryEmailAddress(person) {
18
+ const emails = Array.isArray(person?.PersonEmails) ? person.PersonEmails : [];
19
+ return emails.find(isPrimaryEmail)?.EmailAddress ?? '';
20
+ }
21
+
22
+ export function getDraftEmailAddress(person) {
23
+ if (person?.EmailAddress !== undefined) return String(person.EmailAddress ?? '');
24
+ if (person?.Email !== undefined) return String(person.Email ?? '');
25
+ return getPrimaryEmailAddress(person);
26
+ }
27
+
28
+ export function getDraftHemaRatingsId(person) {
29
+ if (person?.HemaRatingsId !== undefined) return person.HemaRatingsId ?? '';
30
+ return person?.HEMARatings?.[0]?.HemaRatingId ?? '';
31
+ }
32
+
33
+ export function createPersonDraft(profile) {
34
+ const person = JSON.parse(JSON.stringify(profile ?? {}));
35
+ return { ...person, LegalName: getDraftLegalName(person), EmailAddress: getDraftEmailAddress(person) };
36
+ }
37
+
38
+ export function createPersonValidation() {
39
+ return Object.fromEntries(
40
+ [...profileFields.map(({ field }) => field), 'Club'].map(field => [field, { invalid: false, message: '' }])
41
+ );
42
+ }