@dcrackel/hematournamentui 1.0.712 → 1.0.714

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 (107) hide show
  1. package/.prettierignore +7 -7
  2. package/.storybook/main.js +3 -1
  3. package/README.md +42 -0
  4. package/dist/HemaTournamentUI-lib.es.js +6856 -6243
  5. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  6. package/dist/hematournamentui-tailwind.css +79 -9
  7. package/package.json +96 -96
  8. package/prettier.config.js +16 -16
  9. package/src/index.js +43 -42
  10. package/src/main.js +1 -1
  11. package/src/mocks/getDEBouts-TableOf128.js +37 -37
  12. package/src/mocks/getDEBouts-TableOf64.js +35 -35
  13. package/src/stories/Atoms/Input/BaseInput.test.js +97 -97
  14. package/src/stories/Atoms/Input/BaseInput.vue +149 -149
  15. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -23
  16. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -35
  17. package/src/stories/Molecules/Indicators/RegistrationStatus/RegistrationStatus.vue +30 -30
  18. package/src/stories/Molecules/Menus/HoverActions/HoverActions.test.js +138 -0
  19. package/src/stories/Molecules/Menus/HoverActions/HoverActions.vue +212 -0
  20. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.stories.js +49 -0
  21. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.test.js +129 -0
  22. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue +165 -0
  23. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.stories.js +6 -0
  24. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -18
  25. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +1 -1
  26. package/src/stories/Organisms/Cards/BoutCard/BoutCard.vue +5 -1
  27. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardActive/BoutCardActive.vue +19 -10
  28. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardInactive/BoutCardInactive.vue +11 -7
  29. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +42 -33
  30. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +76 -45
  31. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +122 -103
  32. package/src/stories/Organisms/Cards/Director/DirectorCard.test.js +82 -0
  33. package/src/stories/Organisms/Cards/Director/DirectorCard.vue +32 -51
  34. package/src/stories/Organisms/Cards/EditBout/EditBout.test.js +51 -51
  35. package/src/stories/Organisms/Cards/EventCard/EventCard.test.js +106 -106
  36. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -298
  37. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +77 -60
  38. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +144 -143
  39. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +289 -256
  40. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -69
  41. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -80
  42. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +153 -153
  43. package/src/stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue +4 -0
  44. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -31
  45. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -109
  46. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -192
  47. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +1 -0
  48. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +247 -204
  49. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +89 -15
  50. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +51 -29
  51. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +63 -46
  52. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -21
  53. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -39
  54. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -145
  55. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -64
  56. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +36 -57
  57. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +101 -65
  58. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +14 -1
  59. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +52 -52
  60. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +10 -3
  61. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -226
  62. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  63. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +137 -137
  64. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -172
  65. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -216
  66. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -79
  67. package/src/stories/Organisms/Wizards/EditEventWizard/RuleSerialization.test.js +29 -29
  68. package/src/stories/Organisms/Wizards/EditEventWizard/WeaponSelection.test.js +138 -0
  69. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +149 -9
  70. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +54 -2
  71. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +77 -8
  72. package/src/stories/Templates/EventManagement/Bracket/BracketDirectorAssignment.test.js +242 -0
  73. package/src/stories/Templates/EventManagement/Bracket/BracketMobile.test.js +131 -0
  74. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +50 -39
  75. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +201 -0
  76. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.js +93 -0
  77. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.test.js +152 -0
  78. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -39
  79. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -79
  80. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -93
  81. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -91
  82. package/src/stories/Templates/EventManagement/PoolLive/PoolLive.test.js +121 -121
  83. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.stories.js +9 -2
  84. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue +5 -4
  85. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +81 -81
  86. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -39
  87. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +101 -101
  88. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.stories.js +71 -71
  89. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue +86 -86
  90. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.stories.js +86 -86
  91. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.vue +247 -247
  92. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -170
  93. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -156
  94. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -329
  95. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -329
  96. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -42
  97. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -84
  98. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +114 -114
  99. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -363
  100. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -116
  101. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +307 -307
  102. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -45
  103. package/src/stories/Util/Validations/PriceValidation.js +6 -6
  104. package/src/stories/Util/boutDirectorAssignment.js +46 -0
  105. package/src/stories/Util/bracketDirectors.js +86 -0
  106. package/src/stories/Util/registrationStatus.js +11 -11
  107. package/src/stories/Util/tabs.js +19 -47
@@ -1,329 +1,329 @@
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>
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>