@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
@@ -23,7 +23,16 @@ export default {
23
23
  }
24
24
  };
25
25
 
26
- export const Assigned = {};
26
+ export const Assigned = {
27
+ parameters: {
28
+ docs: {
29
+ description: {
30
+ story:
31
+ 'The remove control emits action:remove with a detached station payload. The parent updates planned coverage.'
32
+ }
33
+ }
34
+ }
35
+ };
27
36
 
28
37
  export const AddDirector = {
29
38
  args: {
@@ -42,4 +42,35 @@ describe('BracketDirectorAssignmentCard', () => {
42
42
  expect(wrapper.emitted('action:add')).toEqual([[]]);
43
43
  expect(wrapper.find('base-text-stub').attributes('text')).toBe('Click here to add director');
44
44
  });
45
+
46
+ it('emits a detached station when removing a director and waits for updated props', async () => {
47
+ const selectedStation = structuredClone(station);
48
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { station: selectedStation } });
49
+ const removeButton = wrapper.get('[data-testid="remove-bracket-director"]');
50
+ expect(removeButton.attributes('aria-label')).toBe('Remove Sam Rivera from Ring 3');
51
+ await removeButton.trigger('click');
52
+ const payload = wrapper.emitted('action:remove')[0][0];
53
+ expect(payload).toEqual(selectedStation);
54
+ expect(payload).not.toBe(selectedStation);
55
+ payload.Director.DisplayName = 'Changed';
56
+ expect(selectedStation.Director.DisplayName).toBe('Sam Rivera');
57
+ expect(wrapper.find('[data-testid="bracket-director-assignment"]').exists()).toBe(true);
58
+ await wrapper.setProps({ station: null });
59
+ expect(wrapper.find('[data-testid="bracket-director-assignment"]').exists()).toBe(false);
60
+ });
61
+
62
+ it('has no remove control on the add card or an empty card', () => {
63
+ const addCard = shallowMount(BracketDirectorAssignmentCard, { props: { variant: 'add' } });
64
+ const emptyCard = shallowMount(BracketDirectorAssignmentCard);
65
+ expect(addCard.find('[data-testid="remove-bracket-director"]').exists()).toBe(false);
66
+ expect(emptyCard.find('[data-testid="remove-bracket-director"]').exists()).toBe(false);
67
+ });
68
+
69
+ it('displays the ring as text and reflects parent updates', async () => {
70
+ const wrapper = shallowMount(BracketDirectorAssignmentCard, { props: { station } });
71
+ expect(wrapper.find('select').exists()).toBe(false);
72
+ expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Ring 3');
73
+ await wrapper.setProps({ station: { ...station, RingId: 1, RingName: 'Ring 1' } });
74
+ expect(wrapper.findAll('base-text-stub').map(text => text.attributes('text'))).toContain('Ring 1');
75
+ });
45
76
  });
@@ -45,6 +45,16 @@
45
45
  <BaseText :text="ringLabel" color="quaternary" size="xs" />
46
46
  <BaseText class="truncate" :text="directorName" color="primaryHighlight" size="sm" weight="bold" />
47
47
  </div>
48
+ <button
49
+ type="button"
50
+ data-testid="remove-bracket-director"
51
+ :aria-label="removeLabel"
52
+ :title="removeLabel"
53
+ class="flex h-8 w-8 flex-none items-center justify-center rounded text-quaternary transition hover:bg-dropdownSelect hover:text-alarm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
54
+ @click.stop="removeDirector"
55
+ >
56
+ <BaseIcon iconName="fa-xmark" color="none" size="sm" />
57
+ </button>
48
58
  </article>
49
59
  </template>
50
60
 
@@ -63,7 +73,7 @@ const getInitials = displayName =>
63
73
  export default {
64
74
  name: 'BracketDirectorAssignmentCard',
65
75
  components: { BaseIcon, BaseText },
66
- emits: ['action:add'],
76
+ emits: ['action:add', 'action:remove'],
67
77
  props: {
68
78
  station: {
69
79
  type: Object,
@@ -79,7 +89,16 @@ export default {
79
89
  default: 'Click here to add director'
80
90
  }
81
91
  },
92
+ methods: {
93
+ removeDirector() {
94
+ if (!this.station) return;
95
+ this.$emit('action:remove', JSON.parse(JSON.stringify(this.station)));
96
+ }
97
+ },
82
98
  computed: {
99
+ removeLabel() {
100
+ return 'Remove ' + this.directorName + ' from ' + this.ringLabel;
101
+ },
83
102
  assignmentLabel() {
84
103
  return `${this.ringLabel}, Director ${this.directorName}`;
85
104
  },
@@ -26,4 +26,26 @@ describe('BracketDirectorAssignmentPanel', () => {
26
26
 
27
27
  expect(wrapper.emitted('request:add-planned-director')).toEqual([[]]);
28
28
  });
29
+
30
+ it('forwards remove intent and reconciles the parent station list', async () => {
31
+ const wrapper = shallowMount(BracketDirectorAssignmentPanel, { props: { stations } });
32
+ wrapper.findComponent(BracketDirectorAssignmentCard).vm.$emit('action:remove', stations[0]);
33
+ expect(wrapper.emitted('request:remove-planned-director')).toEqual([[stations[0]]]);
34
+ expect(wrapper.props('stations')).toHaveLength(2);
35
+ await wrapper.setProps({ stations: stations.slice(1) });
36
+ expect(
37
+ wrapper
38
+ .findAllComponents(BracketDirectorAssignmentCard)
39
+ .filter(card => card.props('variant') !== 'add')
40
+ .map(card => card.props('station'))
41
+ ).toEqual([stations[1]]);
42
+ });
43
+
44
+ it('opens director management', async () => {
45
+ const wrapper = shallowMount(BracketDirectorAssignmentPanel, {
46
+ props: { stations, canManageDirectors: true }
47
+ });
48
+ await wrapper.get('[data-testid="manage-bracket-directors"]').trigger('click');
49
+ expect(wrapper.emitted('request:manage-planned-directors')).toEqual([[]]);
50
+ });
29
51
  });
@@ -3,18 +3,34 @@
3
3
  data-testid="bracket-director-assignment-panel"
4
4
  class="w-full rounded-xl border border-dropdownSelect bg-bannerBackground p-4 shadow-lg xl:sticky xl:top-4 xl:w-72"
5
5
  >
6
- <div class="mb-4">
7
- <BaseText color="primaryHighlight" size="md" text="Director Assignments" weight="bold" />
8
- <BaseText
9
- class="mt-1"
10
- color="quaternary"
11
- size="xs"
12
- text="Plan ring coverage. Scheduled bouts may still be moved between rings."
13
- />
6
+ <div class="mb-4 flex items-start justify-between gap-3">
7
+ <div>
8
+ <BaseText color="primaryHighlight" size="md" text="Director Assignments" weight="bold" />
9
+ <BaseText
10
+ class="mt-1"
11
+ color="quaternary"
12
+ size="xs"
13
+ text="Plan ring coverage. Scheduled bouts may still be moved between rings."
14
+ />
15
+ </div>
16
+ <button
17
+ v-if="canManageDirectors"
18
+ type="button"
19
+ data-testid="manage-bracket-directors"
20
+ class="rounded border border-dropdownSelect bg-white px-2 py-1 text-xs text-primaryHighlight hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
21
+ @click="$emit('request:manage-planned-directors')"
22
+ >
23
+ Manage
24
+ </button>
14
25
  </div>
15
26
 
16
27
  <div class="space-y-3">
17
- <BracketDirectorAssignmentCard v-for="station in stations" :key="station.RingId" :station="station" />
28
+ <BracketDirectorAssignmentCard
29
+ v-for="station in stations"
30
+ :key="station.RingId"
31
+ :station="station"
32
+ @action:remove="$emit('request:remove-planned-director', $event)"
33
+ />
18
34
  <BracketDirectorAssignmentCard
19
35
  v-if="showAddCard"
20
36
  variant="add"
@@ -31,8 +47,9 @@ import BracketDirectorAssignmentCard from '../../../Cards/BracketDirectorAssignm
31
47
  export default {
32
48
  name: 'BracketDirectorAssignmentPanel',
33
49
  components: { BaseText, BracketDirectorAssignmentCard },
34
- emits: ['request:add-planned-director'],
50
+ emits: ['request:add-planned-director', 'request:remove-planned-director', 'request:manage-planned-directors'],
35
51
  props: {
52
+ canManageDirectors: { type: Boolean, default: false },
36
53
  stations: {
37
54
  type: Array,
38
55
  default: () => []
@@ -9,7 +9,12 @@ import {
9
9
  getStoryDirectorId,
10
10
  replaceStoryBout
11
11
  } from './BracketStoryHarness.js';
12
- import { plannedDirectingStations, plannedSuggestionViewerId } from './BracketPlannedSuggestions.js';
12
+ import {
13
+ availableDirectors,
14
+ availableRings,
15
+ plannedDirectingStations,
16
+ plannedSuggestionViewerId
17
+ } from './BracketPlannedSuggestions.js';
13
18
  //import getDEWithBouts from '../../../../mocks/getDEWithBouts.js';
14
19
  import getDEWithBouts6Fencers from '../../../../mocks/getDeBouts-6fencers.js';
15
20
  import getDEWithBouts12Fencers from '../../../../mocks/getDeBouts-12fencers.js';
@@ -29,8 +34,9 @@ const renderInteractiveBracket = args => ({
29
34
  const storyStations = ref(cloneStations(args.plannedDirectingStations));
30
35
  const storyAssignments = ref([]);
31
36
  const canAddDirector = computed(() =>
32
- plannedDirectingStations.some(
33
- station => !storyStations.value.some(item => String(item.RingId) === String(station.RingId))
37
+ (args.availableDirectors || availableDirectors).some(
38
+ director =>
39
+ !storyStations.value.some(station => String(station.Director?.PersonId) === String(director.PersonId))
34
40
  )
35
41
  );
36
42
  const redistributeAssignments = () => {
@@ -93,7 +99,11 @@ const renderInteractiveBracket = args => ({
93
99
  };
94
100
 
95
101
  const getViewerStation = () =>
96
- storyStations.value.find(station => String(station.Director?.PersonId) === String(args.viewerPersonId));
102
+ [
103
+ ...storyStations.value,
104
+ ...plannedDirectingStations,
105
+ ...(args.availableDirectors || []).map(Director => ({ Director }))
106
+ ].find(station => String(station.Director?.PersonId) === String(args.viewerPersonId));
97
107
 
98
108
  const handleAddPlannedDirector = () => {
99
109
  const assignedRingIds = new Set(storyStations.value.map(station => String(station.RingId)));
@@ -125,6 +135,16 @@ const renderInteractiveBracket = args => ({
125
135
  requestBoutDirector,
126
136
  assignPlannedDirector,
127
137
  handleAddPlannedDirector,
138
+ handleRemovePlannedDirector(station) {
139
+ const remainingStations = storyStations.value.filter(item => String(item.RingId) !== String(station?.RingId));
140
+ if (remainingStations.length === storyStations.value.length) return;
141
+ storyStations.value = remainingStations;
142
+ redistributeAssignments();
143
+ },
144
+ handlePlannedDirectorsUpdate(stations) {
145
+ storyStations.value = cloneStations(stations);
146
+ redistributeAssignments();
147
+ },
128
148
  handleCloseDirectingBout,
129
149
  handleDirectingBout,
130
150
  persistStoryBout
@@ -156,6 +176,8 @@ const renderInteractiveBracket = args => ({
156
176
  @request:bout-director="requestBoutDirector"
157
177
  @close:directing-bout="handleCloseDirectingBout"
158
178
  @request:add-planned-director="handleAddPlannedDirector"
179
+ @request:remove-planned-director="handleRemovePlannedDirector"
180
+ @update:planned-directing-stations="handlePlannedDirectorsUpdate"
159
181
  @update:directing-bout="handleDirectingBout"
160
182
  @request:bout-active="handleDirectingBout"
161
183
  @update:bout="persistStoryBout"
@@ -171,6 +193,8 @@ export default {
171
193
  tags: ['Bracket'],
172
194
  render: renderInteractiveBracket,
173
195
  args: {
196
+ availableDirectors,
197
+ rings: availableRings,
174
198
  bouts: getDEWithBouts.pools[0].Bouts,
175
199
  eventRules: getDEWithBouts.eventRules,
176
200
  hostingClubColors: getDEWithBouts.hostingClubColors,
@@ -184,6 +208,11 @@ export default {
184
208
  showBoutDirectorAssignment: true
185
209
  },
186
210
  argTypes: {
211
+ availableDirectors: {
212
+ control: 'object',
213
+ description: 'Eligible staff, including DE-only directors, supplied by the parent.'
214
+ },
215
+ rings: { control: 'object', description: 'Available rings supplied by the parent.' },
187
216
  showBoutDirectorAssignment: {
188
217
  control: 'boolean',
189
218
  description: 'Include Assign director in the pairing hover menu. The parent supplies staff selection and saving.'
@@ -1,178 +1,218 @@
1
- import { shallowMount } from '@vue/test-utils';
2
- import { describe, expect, it } from 'vitest';
3
- import DirectorModal from '../../../Molecules/Modals/DirectorModal/DirectorModal.vue';
4
- import EditBoutModal from '../../../Molecules/Modals/EditBoutModal/EditBoutModal.vue';
5
- import Tableau from '../../../Organisms/Containers/Tableau/Tableau.vue';
6
- import BracketDirectorAssignmentPanel from '../../../Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
7
- import Bracket from './Bracket.vue';
8
-
9
- const createBout = (overrides = {}) => ({
10
- BoutId: 42,
11
- DEBoutId: 1,
12
- RoundLabel: 'Final',
13
- Status: 'Scheduled',
14
- Score1: 4,
15
- Score2: 3,
16
- Person1: { PersonId: 1, DisplayName: 'Fencer One' },
17
- Person2: { PersonId: 2, DisplayName: 'Fencer Two' },
18
- ...overrides
19
- });
20
-
21
- const mountBracket = (bouts = [createBout()], optionalProps = {}) =>
22
- shallowMount(Bracket, {
23
- props: {
24
- bouts,
25
- eventRules: [],
26
- hostingClubColors: {},
27
- connectedToServer: true,
28
- ...optionalProps
29
- }
30
- });
31
-
32
- describe('Bracket bout interaction', () => {
33
- it('emits directing intent for a scheduled bout and waits for Active props', async () => {
34
- const bout = createBout();
35
- const wrapper = mountBracket([bout]);
36
-
37
- wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
38
- await wrapper.vm.$nextTick();
39
-
40
- expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
41
- expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
42
-
43
- await wrapper.setProps({ bouts: [{ ...bout, Status: 'Active' }] });
44
-
45
- expect(wrapper.findComponent(DirectorModal).props('show')).toBe(true);
46
- expect(wrapper.findComponent(DirectorModal).props('bout').Status).toBe('Active');
47
- });
48
-
49
- it('resumes an active bout through the directing path', async () => {
50
- const bout = createBout({ Status: 'Active' });
51
- const wrapper = mountBracket([bout]);
52
-
53
- wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
54
- await wrapper.vm.$nextTick();
55
-
56
- expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
57
- expect(wrapper.findComponent(DirectorModal).props('show')).toBe(true);
58
- });
59
-
60
- it('opens completed bouts for editing instead of directing', async () => {
61
- const bout = createBout({ Status: 'Completed' });
62
- const wrapper = mountBracket([bout]);
63
-
64
- wrapper.findComponent(Tableau).vm.$emit('action:editBout', bout);
65
- await wrapper.vm.$nextTick();
66
-
67
- expect(wrapper.findComponent(EditBoutModal).props('show')).toBe(true);
68
- expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
69
- expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
70
- });
71
-
72
- it('does not direct a BYE bout', async () => {
73
- const bout = createBout({
74
- Person2: { PersonId: 0, DisplayName: 'BYE' }
75
- });
76
- const wrapper = mountBracket([bout]);
77
-
78
- wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
79
- await wrapper.vm.$nextTick();
80
-
81
- expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
82
- expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
83
- });
84
-
85
- it('closes the scoring modal without changing status or scores', async () => {
86
- const bout = createBout({ Status: 'Active', Score1: 6, Score2: 5 });
87
- const wrapper = mountBracket([bout]);
88
-
89
- wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
90
- await wrapper.vm.$nextTick();
91
- wrapper.findComponent(DirectorModal).vm.$emit('update:closeModal', bout);
92
- await wrapper.vm.$nextTick();
93
-
94
- expect(wrapper.vm.boutsData[0]).toMatchObject({ Status: 'Active', Score1: 6, Score2: 5 });
95
- expect(wrapper.emitted('update:bout')).toBeUndefined();
96
- expect(wrapper.emitted('close:directing-bout')).toEqual([[bout]]);
97
- expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
98
- });
99
-
100
- it('emits submitted bout data without assigning completion fields', async () => {
101
- const bout = createBout({ Status: 'Active' });
102
- const submittedBout = { ...bout, Score1: 7 };
103
- const wrapper = mountBracket([bout]);
104
-
105
- wrapper.findComponent(DirectorModal).vm.$emit('submit:bout', submittedBout);
106
- await wrapper.vm.$nextTick();
107
-
108
- const emittedBout = wrapper.emitted('submit:bout')[0][0];
109
- expect(emittedBout).toEqual(submittedBout);
110
- expect(emittedBout.Status).toBe('Active');
111
- expect(emittedBout.Ended).toBeUndefined();
112
- });
113
-
114
- it('does not render a director-selection modal', () => {
115
- const wrapper = mountBracket();
116
-
117
- expect(wrapper.findComponent({ name: 'AssignDirectorModal' }).exists()).toBe(false);
118
- });
119
-
120
- it('renders optional planned suggestions without changing the directing contract', async () => {
121
- const bout = createBout();
122
- const plannedDirectingStations = [
123
- {
124
- RingId: 1,
125
- RingName: 'Ring 1',
126
- Color: '#2563eb',
127
- Director: { PersonId: 1001, DisplayName: 'Alex Morgan' }
128
- }
129
- ];
130
- const plannedBoutAssignments = [{ DEBoutId: bout.DEBoutId, RingId: 1 }];
131
- const wrapper = mountBracket([bout], {
132
- plannedDirectingStations,
133
- plannedBoutAssignments,
134
- viewerPersonId: 1001
135
- });
136
-
137
- expect(wrapper.findComponent(BracketDirectorAssignmentPanel).props('stations')).toEqual(plannedDirectingStations);
138
- expect(wrapper.findComponent(Tableau).props()).toMatchObject({
139
- plannedDirectingStations,
140
- plannedBoutAssignments,
141
- viewerPersonId: 1001
142
- });
143
-
144
- wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
145
- await wrapper.vm.$nextTick();
146
-
147
- expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
148
- expect(wrapper.emitted('assign:director')).toBeUndefined();
149
- });
150
-
151
- it('emits add-planned-director intent from the assignment panel', async () => {
152
- const wrapper = mountBracket([], { showPlannedDirectorAssignments: true });
153
-
154
- wrapper.findComponent(BracketDirectorAssignmentPanel).vm.$emit('request:add-planned-director');
155
- await wrapper.vm.$nextTick();
156
-
157
- expect(wrapper.emitted('request:add-planned-director')).toEqual([[]]);
158
- });
159
-
160
- it('does not render planned directing controls when explicitly disabled', () => {
161
- const wrapper = mountBracket(undefined, {
162
- showPlannedDirectorAssignments: false,
163
- showBoutDirectorAssignment: false
164
- });
165
-
166
- expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(false);
167
- expect(wrapper.findComponent(Tableau).props('plannedDirectingStations')).toEqual([]);
168
- });
169
- });
170
-
171
- describe('Bracket assignment defaults', () => {
172
- it('shows assignment actions and the director panel without opt-in props', () => {
173
- const wrapper = mountBracket();
174
- expect(wrapper.findComponent(Tableau).props('showBoutDirectorAssignment')).toBe(true);
175
- expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(true);
176
- wrapper.unmount();
177
- });
178
- });
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import DirectorModal from '../../../Molecules/Modals/DirectorModal/DirectorModal.vue';
4
+ import EditBoutModal from '../../../Molecules/Modals/EditBoutModal/EditBoutModal.vue';
5
+ import Tableau from '../../../Organisms/Containers/Tableau/Tableau.vue';
6
+ import BracketDirectorAssignmentPanel from '../../../Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
7
+ import Bracket from './Bracket.vue';
8
+
9
+ const createBout = (overrides = {}) => ({
10
+ BoutId: 42,
11
+ DEBoutId: 1,
12
+ RoundLabel: 'Final',
13
+ Status: 'Scheduled',
14
+ Score1: 4,
15
+ Score2: 3,
16
+ Person1: { PersonId: 1, DisplayName: 'Fencer One' },
17
+ Person2: { PersonId: 2, DisplayName: 'Fencer Two' },
18
+ ...overrides
19
+ });
20
+
21
+ const mountBracket = (bouts = [createBout()], optionalProps = {}) =>
22
+ shallowMount(Bracket, {
23
+ props: {
24
+ bouts,
25
+ eventRules: [],
26
+ hostingClubColors: {},
27
+ connectedToServer: true,
28
+ ...optionalProps
29
+ }
30
+ });
31
+
32
+ describe('Bracket bout interaction', () => {
33
+ it('emits directing intent for a scheduled bout and waits for Active props', async () => {
34
+ const bout = createBout();
35
+ const wrapper = mountBracket([bout]);
36
+
37
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
38
+ await wrapper.vm.$nextTick();
39
+
40
+ expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
41
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
42
+
43
+ await wrapper.setProps({ bouts: [{ ...bout, Status: 'Active' }] });
44
+
45
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(true);
46
+ expect(wrapper.findComponent(DirectorModal).props('bout').Status).toBe('Active');
47
+ });
48
+
49
+ it('resumes an active bout through the directing path', async () => {
50
+ const bout = createBout({ Status: 'Active' });
51
+ const wrapper = mountBracket([bout]);
52
+
53
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
54
+ await wrapper.vm.$nextTick();
55
+
56
+ expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
57
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(true);
58
+ });
59
+
60
+ it('opens completed bouts for editing instead of directing', async () => {
61
+ const bout = createBout({ Status: 'Completed' });
62
+ const wrapper = mountBracket([bout]);
63
+
64
+ wrapper.findComponent(Tableau).vm.$emit('action:editBout', bout);
65
+ await wrapper.vm.$nextTick();
66
+
67
+ expect(wrapper.findComponent(EditBoutModal).props('show')).toBe(true);
68
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
69
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
70
+ });
71
+
72
+ it('does not direct a BYE bout', async () => {
73
+ const bout = createBout({
74
+ Person2: { PersonId: 0, DisplayName: 'BYE' }
75
+ });
76
+ const wrapper = mountBracket([bout]);
77
+
78
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
79
+ await wrapper.vm.$nextTick();
80
+
81
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
82
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
83
+ });
84
+
85
+ it('closes the scoring modal without changing status or scores', async () => {
86
+ const bout = createBout({ Status: 'Active', Score1: 6, Score2: 5 });
87
+ const wrapper = mountBracket([bout]);
88
+
89
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
90
+ await wrapper.vm.$nextTick();
91
+ wrapper.findComponent(DirectorModal).vm.$emit('update:closeModal', bout);
92
+ await wrapper.vm.$nextTick();
93
+
94
+ expect(wrapper.vm.boutsData[0]).toMatchObject({ Status: 'Active', Score1: 6, Score2: 5 });
95
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
96
+ expect(wrapper.emitted('close:directing-bout')).toEqual([[bout]]);
97
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
98
+ });
99
+
100
+ it('emits submitted bout data without assigning completion fields', async () => {
101
+ const bout = createBout({ Status: 'Active' });
102
+ const submittedBout = { ...bout, Score1: 7 };
103
+ const wrapper = mountBracket([bout]);
104
+
105
+ wrapper.findComponent(DirectorModal).vm.$emit('submit:bout', submittedBout);
106
+ await wrapper.vm.$nextTick();
107
+
108
+ const emittedBout = wrapper.emitted('submit:bout')[0][0];
109
+ expect(emittedBout).toEqual(submittedBout);
110
+ expect(emittedBout.Status).toBe('Active');
111
+ expect(emittedBout.Ended).toBeUndefined();
112
+ });
113
+
114
+ it('does not render a director-selection modal', () => {
115
+ const wrapper = mountBracket();
116
+
117
+ expect(wrapper.findComponent({ name: 'AssignDirectorModal' }).exists()).toBe(false);
118
+ });
119
+
120
+ it('renders optional planned suggestions without changing the directing contract', async () => {
121
+ const bout = createBout();
122
+ const plannedDirectingStations = [
123
+ {
124
+ RingId: 1,
125
+ RingName: 'Ring 1',
126
+ Color: '#2563eb',
127
+ Director: { PersonId: 1001, DisplayName: 'Alex Morgan' }
128
+ }
129
+ ];
130
+ const plannedBoutAssignments = [{ DEBoutId: bout.DEBoutId, RingId: 1 }];
131
+ const wrapper = mountBracket([bout], {
132
+ plannedDirectingStations,
133
+ plannedBoutAssignments,
134
+ viewerPersonId: 1001
135
+ });
136
+
137
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).props('stations')).toEqual(plannedDirectingStations);
138
+ expect(wrapper.findComponent(Tableau).props()).toMatchObject({
139
+ plannedDirectingStations,
140
+ plannedBoutAssignments,
141
+ viewerPersonId: 1001
142
+ });
143
+
144
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
145
+ await wrapper.vm.$nextTick();
146
+
147
+ expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
148
+ expect(wrapper.emitted('assign:director')).toBeUndefined();
149
+ });
150
+
151
+ it('emits add-planned-director intent from the assignment panel', async () => {
152
+ const wrapper = mountBracket([], { showPlannedDirectorAssignments: true });
153
+
154
+ wrapper.findComponent(BracketDirectorAssignmentPanel).vm.$emit('request:add-planned-director');
155
+ await wrapper.vm.$nextTick();
156
+
157
+ expect(wrapper.emitted('request:add-planned-director')).toEqual([[]]);
158
+ });
159
+
160
+ it('does not render planned directing controls when explicitly disabled', () => {
161
+ const wrapper = mountBracket(undefined, {
162
+ showPlannedDirectorAssignments: false,
163
+ showBoutDirectorAssignment: false
164
+ });
165
+
166
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(false);
167
+ expect(wrapper.findComponent(Tableau).props('plannedDirectingStations')).toEqual([]);
168
+ });
169
+ });
170
+
171
+ describe('Bracket assignment defaults', () => {
172
+ it('shows assignment actions and the director panel without opt-in props', () => {
173
+ const wrapper = mountBracket();
174
+ expect(wrapper.findComponent(Tableau).props('showBoutDirectorAssignment')).toBe(true);
175
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(true);
176
+ wrapper.unmount();
177
+ });
178
+
179
+ it('forwards removal of a right-column director without changing authoritative bout fields', async () => {
180
+ const station = { RingId: 1, RingName: 'Ring 1', Director: { PersonId: 99, DisplayName: 'Dave Smith' } };
181
+ const bout = createBout({ Status: 'Active', RefereeId: 99, RefName: 'Dave Smith' });
182
+ const wrapper = mountBracket([bout], { plannedDirectingStations: [station] });
183
+ wrapper.findComponent(BracketDirectorAssignmentPanel).vm.$emit('request:remove-planned-director', station);
184
+ expect(wrapper.emitted('request:remove-planned-director')).toEqual([[station]]);
185
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
186
+ expect(wrapper.vm.boutsData).toEqual([bout]);
187
+ expect(wrapper.props('plannedDirectingStations')).toEqual([station]);
188
+ await wrapper.setProps({ plannedDirectingStations: [] });
189
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).props('stations')).toEqual([]);
190
+ wrapper.unmount();
191
+ });
192
+
193
+ it('opens the staff-fed modal and forwards detached roster save intent without changing bout or station props', async () => {
194
+ const selected = {
195
+ RingId: 1,
196
+ RingName: 'Ring 1',
197
+ Director: { PersonId: 99, DisplayName: 'Dave Smith' },
198
+ Color: '#2563eb'
199
+ };
200
+ const wrapper = mountBracket(undefined, {
201
+ availableDirectors: [selected.Director],
202
+ rings: [{ RingId: 1, RingName: 'Ring 1' }],
203
+ plannedDirectingStations: [selected]
204
+ });
205
+ wrapper.findComponent(BracketDirectorAssignmentPanel).vm.$emit('request:add-planned-director');
206
+ await wrapper.vm.$nextTick();
207
+ const modal = wrapper.findComponent({ name: 'BracketDirectorsModal' });
208
+ expect(modal.props('show')).toBe(true);
209
+ expect(wrapper.emitted('request:add-planned-director')).toBeUndefined();
210
+ modal.vm.$emit('submit:stations', []);
211
+ await wrapper.vm.$nextTick();
212
+ expect(wrapper.emitted('update:planned-directing-stations')).toEqual([[[]]]);
213
+ expect(wrapper.props('plannedDirectingStations')).toEqual([selected]);
214
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
215
+ expect(modal.props('show')).toBe(false);
216
+ wrapper.unmount();
217
+ });
218
+ });