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