@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.
Files changed (20) hide show
  1. package/README.md +12 -0
  2. package/dist/HemaTournamentUI-lib.es.js +6856 -6243
  3. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  4. package/dist/hematournamentui-tailwind.css +79 -9
  5. package/package.json +1 -1
  6. package/src/index.js +43 -42
  7. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.stories.js +49 -0
  8. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.test.js +129 -0
  9. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue +165 -0
  10. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +10 -1
  11. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +31 -0
  12. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +20 -1
  13. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +22 -0
  14. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +27 -10
  15. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +33 -4
  16. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +218 -178
  17. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +42 -1
  18. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +8 -0
  19. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +121 -0
  20. 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
- @request:add-planned-director="$emit('request:add-planned-director')"
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
+ };