@dcrackel/hematournamentui 1.0.713 → 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/README.md +12 -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 +1 -1
- package/src/index.js +43 -42
- 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/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +10 -1
- package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +31 -0
- package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +20 -1
- package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +22 -0
- package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +27 -10
- package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +33 -4
- package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +218 -178
- package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +42 -1
- package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +8 -0
- package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +121 -0
- package/src/stories/Util/bracketDirectors.js +86 -0
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
class="order-first flex-none xl:order-last"
|
|
41
41
|
:stations="plannedDirectingStations"
|
|
42
42
|
:showAddCard="showAddPlannedDirectorCard"
|
|
43
|
-
|
|
43
|
+
:canManageDirectors="canManagePlannedDirectors"
|
|
44
|
+
@request:manage-planned-directors="handleManagePlannedDirectors"
|
|
45
|
+
@request:add-planned-director="handleAddPlannedDirector"
|
|
46
|
+
@request:remove-planned-director="$emit('request:remove-planned-director', $event)"
|
|
44
47
|
/>
|
|
45
48
|
</div>
|
|
46
49
|
|
|
@@ -64,6 +67,15 @@
|
|
|
64
67
|
</div>
|
|
65
68
|
</div>
|
|
66
69
|
|
|
70
|
+
<BracketDirectorsModal
|
|
71
|
+
v-if="canManagePlannedDirectors"
|
|
72
|
+
v-model:show="showPlannedDirectorsModal"
|
|
73
|
+
:availableDirectors="availableDirectors"
|
|
74
|
+
:rings="rings"
|
|
75
|
+
:stations="plannedDirectingStations"
|
|
76
|
+
@submit:stations="handlePlannedDirectorsSubmit"
|
|
77
|
+
/>
|
|
78
|
+
|
|
67
79
|
<DirectorModal
|
|
68
80
|
:bout="selectedBout"
|
|
69
81
|
:eventRules="eventRules"
|
|
@@ -96,6 +108,8 @@ import ToggleHeader from '../../../Organisms/Headers/ToggleHeader/ToggleHeader.v
|
|
|
96
108
|
import ServerConnected from '../../../Molecules/Indicators/ServerConnected/ServerConnected.vue';
|
|
97
109
|
import BaseText from '../../../Atoms/Text/BaseText.vue';
|
|
98
110
|
import DirectorModal from '../../../Molecules/Modals/DirectorModal/DirectorModal.vue';
|
|
111
|
+
import BracketDirectorsModal from '../../../Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue';
|
|
112
|
+
import { cloneDirectorData } from '../../../Util/bracketDirectors.js';
|
|
99
113
|
import EditBoutModal from '../../../Molecules/Modals/EditBoutModal/EditBoutModal.vue';
|
|
100
114
|
import Tableau from '../../../Organisms/Containers/Tableau/Tableau.vue';
|
|
101
115
|
import BracketDirectorAssignmentPanel from '../../../Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
|
|
@@ -121,6 +135,8 @@ export default {
|
|
|
121
135
|
'penalty:remove',
|
|
122
136
|
'penalty:update',
|
|
123
137
|
'request:add-planned-director',
|
|
138
|
+
'request:remove-planned-director',
|
|
139
|
+
'update:planned-directing-stations',
|
|
124
140
|
'request:bout-active',
|
|
125
141
|
'request:bout-director',
|
|
126
142
|
'submit:bout',
|
|
@@ -133,12 +149,15 @@ export default {
|
|
|
133
149
|
EditBoutModal,
|
|
134
150
|
DirectorModal,
|
|
135
151
|
BracketDirectorAssignmentPanel,
|
|
152
|
+
BracketDirectorsModal,
|
|
136
153
|
BaseText,
|
|
137
154
|
ToggleHeader,
|
|
138
155
|
ServerConnected,
|
|
139
156
|
EditEventsTopMenu
|
|
140
157
|
},
|
|
141
158
|
props: {
|
|
159
|
+
availableDirectors: { type: Array, default: undefined },
|
|
160
|
+
rings: { type: Array, default: undefined },
|
|
142
161
|
showBoutDirectorAssignment: { type: Boolean, default: true },
|
|
143
162
|
bouts: {
|
|
144
163
|
type: Array,
|
|
@@ -189,6 +208,7 @@ export default {
|
|
|
189
208
|
return {
|
|
190
209
|
emptyDesertIcon,
|
|
191
210
|
showDirectorModal: false,
|
|
211
|
+
showPlannedDirectorsModal: false,
|
|
192
212
|
showEditBoutModal: false,
|
|
193
213
|
selectedBout: {},
|
|
194
214
|
tabs,
|
|
@@ -198,6 +218,9 @@ export default {
|
|
|
198
218
|
};
|
|
199
219
|
},
|
|
200
220
|
computed: {
|
|
221
|
+
canManagePlannedDirectors() {
|
|
222
|
+
return !this.isMobile && Array.isArray(this.availableDirectors) && Array.isArray(this.rings);
|
|
223
|
+
},
|
|
201
224
|
isMobile() {
|
|
202
225
|
return this.windowWidth < 768;
|
|
203
226
|
},
|
|
@@ -236,6 +259,9 @@ export default {
|
|
|
236
259
|
}
|
|
237
260
|
},
|
|
238
261
|
watch: {
|
|
262
|
+
isMobile(value) {
|
|
263
|
+
if (value) this.showPlannedDirectorsModal = false;
|
|
264
|
+
},
|
|
239
265
|
bouts: {
|
|
240
266
|
handler(newBouts) {
|
|
241
267
|
this.boutsData = this.cloneBouts(newBouts);
|
|
@@ -253,6 +279,21 @@ export default {
|
|
|
253
279
|
window.removeEventListener('resize', this.updateViewBasedOnScreenSize);
|
|
254
280
|
},
|
|
255
281
|
methods: {
|
|
282
|
+
handleAddPlannedDirector() {
|
|
283
|
+
if (this.canManagePlannedDirectors) {
|
|
284
|
+
this.handleManagePlannedDirectors();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.$emit('request:add-planned-director');
|
|
288
|
+
},
|
|
289
|
+
handleManagePlannedDirectors() {
|
|
290
|
+
if (!this.canManagePlannedDirectors) return;
|
|
291
|
+
this.showPlannedDirectorsModal = true;
|
|
292
|
+
},
|
|
293
|
+
handlePlannedDirectorsSubmit(stations) {
|
|
294
|
+
this.$emit('update:planned-directing-stations', cloneDirectorData(stations));
|
|
295
|
+
this.showPlannedDirectorsModal = false;
|
|
296
|
+
},
|
|
256
297
|
handleBoutDirectorRequest(bout) {
|
|
257
298
|
if (!this.canShowBoutDirectorAssignment) return;
|
|
258
299
|
const currentBout = this.boutsData.find(item => String(item.BoutId) === String(bout?.BoutId));
|
|
@@ -40,3 +40,11 @@ export const plannedDirectingStations = [
|
|
|
40
40
|
];
|
|
41
41
|
|
|
42
42
|
export const plannedSuggestionViewerId = plannedDirectingStations[0].Director.PersonId;
|
|
43
|
+
|
|
44
|
+
export const availableDirectors = plannedDirectingStations.map((station, index) => ({
|
|
45
|
+
...station.Director,
|
|
46
|
+
Color: station.Color,
|
|
47
|
+
DefaultRingId: index < 2 ? station.RingId : null
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
export const availableRings = plannedDirectingStations.map(({ RingId, RingName }) => ({ RingId, RingName }));
|
|
@@ -77,4 +77,125 @@ describe('Bracket story assignment simulation', () => {
|
|
|
77
77
|
expect(shared.every(bout => getRing(bracket(), bout) === undefined)).toBe(true);
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
it.each(['Active', 'Completed'])(
|
|
82
|
+
'redistributes scheduled work after removal while preserving a %s bout',
|
|
83
|
+
async Status => {
|
|
84
|
+
const { bracket } = mountStory(stories.PlannedDirectorSuggestions);
|
|
85
|
+
const station = bracket().props('plannedDirectingStations')[0];
|
|
86
|
+
const bout = bracket()
|
|
87
|
+
.props('bouts')
|
|
88
|
+
.find(item => item.Person1.PersonId > 0 && item.Person2.PersonId > 0);
|
|
89
|
+
const savedBout = {
|
|
90
|
+
...bout,
|
|
91
|
+
Status,
|
|
92
|
+
RefereeId: station.Director.PersonId,
|
|
93
|
+
RefName: station.Director.DisplayName,
|
|
94
|
+
Score1: 4,
|
|
95
|
+
Score2: 3
|
|
96
|
+
};
|
|
97
|
+
bracket().vm.$emit('update:bout', savedBout);
|
|
98
|
+
await nextTick();
|
|
99
|
+
bracket().vm.$emit('request:remove-planned-director', station);
|
|
100
|
+
await nextTick();
|
|
101
|
+
expect(
|
|
102
|
+
bracket()
|
|
103
|
+
.props('plannedDirectingStations')
|
|
104
|
+
.map(item => item.RingId)
|
|
105
|
+
).toEqual([2]);
|
|
106
|
+
expect(
|
|
107
|
+
bracket()
|
|
108
|
+
.props('bouts')
|
|
109
|
+
.find(item => item.BoutId === bout.BoutId)
|
|
110
|
+
).toEqual(savedBout);
|
|
111
|
+
const scheduledIds = new Set(
|
|
112
|
+
bracket()
|
|
113
|
+
.props('bouts')
|
|
114
|
+
.filter(item => item.Status === 'Scheduled')
|
|
115
|
+
.map(item => item.DEBoutId)
|
|
116
|
+
);
|
|
117
|
+
expect(
|
|
118
|
+
bracket()
|
|
119
|
+
.props('plannedBoutAssignments')
|
|
120
|
+
.filter(item => scheduledIds.has(item.DEBoutId))
|
|
121
|
+
.every(item => item.RingId === 2)
|
|
122
|
+
).toBe(true);
|
|
123
|
+
expect(getRing(bracket(), savedBout)).toBe(1);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
it('supports removing every director and adding a removed director again without changing story args', async () => {
|
|
128
|
+
const { bracket, args } = mountStory(stories.PlannedDirectorSuggestions);
|
|
129
|
+
for (const station of args.plannedDirectingStations) {
|
|
130
|
+
bracket().vm.$emit('request:remove-planned-director', station);
|
|
131
|
+
await nextTick();
|
|
132
|
+
}
|
|
133
|
+
expect(bracket().props('plannedDirectingStations')).toEqual([]);
|
|
134
|
+
expect(bracket().props('plannedBoutAssignments')).toEqual([]);
|
|
135
|
+
expect(bracket().props('showAddPlannedDirectorCard')).toBe(true);
|
|
136
|
+
bracket().vm.$emit('request:add-planned-director');
|
|
137
|
+
await nextTick();
|
|
138
|
+
expect(
|
|
139
|
+
bracket()
|
|
140
|
+
.props('plannedDirectingStations')
|
|
141
|
+
.map(station => station.RingId)
|
|
142
|
+
).toEqual([1]);
|
|
143
|
+
expect(
|
|
144
|
+
bracket()
|
|
145
|
+
.props('plannedBoutAssignments')
|
|
146
|
+
.every(item => item.RingId === 1)
|
|
147
|
+
).toBe(true);
|
|
148
|
+
expect(args.plannedDirectingStations).toHaveLength(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('keeps the signed-in director identity when resuming a bout after removing their planned station', async () => {
|
|
152
|
+
const { bracket } = mountStory(stories.PlannedDirectorSuggestions);
|
|
153
|
+
const station = bracket().props('plannedDirectingStations')[0];
|
|
154
|
+
const bout = bracket()
|
|
155
|
+
.props('bouts')
|
|
156
|
+
.find(item => item.Person1.PersonId > 0 && item.Person2.PersonId > 0);
|
|
157
|
+
bracket().vm.$emit('update:directing-bout', bout);
|
|
158
|
+
await nextTick();
|
|
159
|
+
const active = bracket()
|
|
160
|
+
.props('bouts')
|
|
161
|
+
.find(item => item.BoutId === bout.BoutId);
|
|
162
|
+
bracket().vm.$emit('request:remove-planned-director', station);
|
|
163
|
+
await nextTick();
|
|
164
|
+
bracket().vm.$emit('update:directing-bout', { ...active, Score1: 3 });
|
|
165
|
+
await nextTick();
|
|
166
|
+
expect(
|
|
167
|
+
bracket()
|
|
168
|
+
.props('bouts')
|
|
169
|
+
.find(item => item.BoutId === bout.BoutId)
|
|
170
|
+
).toMatchObject({
|
|
171
|
+
RefereeId: station.Director.PersonId,
|
|
172
|
+
RefName: station.Director.DisplayName,
|
|
173
|
+
Status: 'Active',
|
|
174
|
+
Score1: 3
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('applies staff-fed roster saves and ring moves only to DE plans', async () => {
|
|
179
|
+
const { bracket, args } = mountStory(stories.PlannedDirectorSuggestions);
|
|
180
|
+
const beforeBouts = JSON.parse(JSON.stringify(bracket().props('bouts')));
|
|
181
|
+
const original = bracket().props('plannedDirectingStations')[0];
|
|
182
|
+
const updated = { ...original, RingId: 3, RingName: 'Ring 3' };
|
|
183
|
+
bracket().vm.$emit('update:planned-directing-stations', [
|
|
184
|
+
updated,
|
|
185
|
+
...bracket().props('plannedDirectingStations').slice(1)
|
|
186
|
+
]);
|
|
187
|
+
await nextTick();
|
|
188
|
+
expect(bracket().props('plannedDirectingStations')[0]).toEqual(updated);
|
|
189
|
+
expect(
|
|
190
|
+
bracket()
|
|
191
|
+
.props('plannedBoutAssignments')
|
|
192
|
+
.every(item => [2, 3].includes(item.RingId))
|
|
193
|
+
).toBe(true);
|
|
194
|
+
expect(args.plannedDirectingStations[0].RingId).toBe(1);
|
|
195
|
+
expect(args.availableDirectors[0].DefaultRingId).toBe(1);
|
|
196
|
+
bracket().vm.$emit('update:planned-directing-stations', [updated]);
|
|
197
|
+
await nextTick();
|
|
198
|
+
expect(bracket().props('plannedDirectingStations')).toEqual([updated]);
|
|
199
|
+
expect(bracket().props('bouts')).toEqual(beforeBouts);
|
|
200
|
+
});
|
|
80
201
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const getIdentityKey = value => String(value ?? '');
|
|
2
|
+
export const cloneDirectorData = value => JSON.parse(JSON.stringify(value));
|
|
3
|
+
const hasIdentity = value => value !== null && value !== undefined && String(value) !== '';
|
|
4
|
+
const isDirector = director => hasIdentity(director?.PersonId) && getIdentityKey(director.PersonId) !== '0';
|
|
5
|
+
|
|
6
|
+
export const getRingOptions = rings => {
|
|
7
|
+
const options = new Map();
|
|
8
|
+
for (const ring of rings || []) {
|
|
9
|
+
if (!hasIdentity(ring?.RingId)) continue;
|
|
10
|
+
options.set(getIdentityKey(ring.RingId), { ...ring, RingName: ring.RingName || 'Ring ' + ring.RingId });
|
|
11
|
+
}
|
|
12
|
+
return [...options.values()];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getDirectorOptions = (availableDirectors, stations) => {
|
|
16
|
+
const options = new Map();
|
|
17
|
+
for (const director of availableDirectors || []) {
|
|
18
|
+
if (isDirector(director)) options.set(getIdentityKey(director.PersonId), cloneDirectorData(director));
|
|
19
|
+
}
|
|
20
|
+
for (const station of stations || []) {
|
|
21
|
+
const director = station?.Director;
|
|
22
|
+
if (!isDirector(director) || options.has(getIdentityKey(director.PersonId))) continue;
|
|
23
|
+
options.set(getIdentityKey(director.PersonId), cloneDirectorData(director));
|
|
24
|
+
}
|
|
25
|
+
return [...options.values()];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createDirectorDraft = (availableDirectors, stations) => {
|
|
29
|
+
const stationByDirector = new Map(
|
|
30
|
+
(stations || [])
|
|
31
|
+
.filter(station => isDirector(station?.Director))
|
|
32
|
+
.map(station => [getIdentityKey(station.Director.PersonId), station])
|
|
33
|
+
);
|
|
34
|
+
return getDirectorOptions(availableDirectors, stations).map(candidate => {
|
|
35
|
+
const station = stationByDirector.get(getIdentityKey(candidate.PersonId));
|
|
36
|
+
const savedStation = station ? cloneDirectorData(station) : {};
|
|
37
|
+
const { DefaultRingId, Color, ...director } = candidate;
|
|
38
|
+
return {
|
|
39
|
+
Director: { ...savedStation.Director, ...director, DisplayName: director.DisplayName || 'Unnamed director' },
|
|
40
|
+
Color: savedStation.Color || Color || '',
|
|
41
|
+
RingId: savedStation.RingId ?? DefaultRingId ?? '',
|
|
42
|
+
Selected: Boolean(station),
|
|
43
|
+
station: savedStation
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const reconcileDirectorDraft = (availableDirectors, stations, draft) => {
|
|
49
|
+
const previous = new Map(draft.map(row => [getIdentityKey(row.Director.PersonId), row]));
|
|
50
|
+
return createDirectorDraft(availableDirectors, stations).map(row => {
|
|
51
|
+
const existing = previous.get(getIdentityKey(row.Director.PersonId));
|
|
52
|
+
if (!existing) return row;
|
|
53
|
+
return { ...row, Selected: existing.Selected, RingId: existing.RingId };
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const getDirectorDraftError = (draft, rings) => {
|
|
58
|
+
const selected = draft.filter(row => row.Selected);
|
|
59
|
+
const availableIds = new Set(
|
|
60
|
+
getRingOptions(rings)
|
|
61
|
+
.filter(ring => !ring.Disabled)
|
|
62
|
+
.map(ring => getIdentityKey(ring.RingId))
|
|
63
|
+
);
|
|
64
|
+
if (selected.some(row => !availableIds.has(getIdentityKey(row.RingId))))
|
|
65
|
+
return 'Choose a ring for each selected director.';
|
|
66
|
+
const selectedIds = selected.map(row => getIdentityKey(row.RingId));
|
|
67
|
+
if (new Set(selectedIds).size !== selectedIds.length) return 'Each selected director needs a different ring.';
|
|
68
|
+
return '';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const createDirectorStations = (draft, rings) => {
|
|
72
|
+
if (getDirectorDraftError(draft, rings)) return null;
|
|
73
|
+
const ringById = new Map(getRingOptions(rings).map(ring => [getIdentityKey(ring.RingId), ring]));
|
|
74
|
+
return draft
|
|
75
|
+
.filter(row => row.Selected)
|
|
76
|
+
.map(row => {
|
|
77
|
+
const ring = ringById.get(getIdentityKey(row.RingId));
|
|
78
|
+
return cloneDirectorData({
|
|
79
|
+
...row.station,
|
|
80
|
+
RingId: ring.RingId,
|
|
81
|
+
RingName: ring.RingName,
|
|
82
|
+
Color: row.Color || ring.Color || '',
|
|
83
|
+
Director: row.Director
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
};
|