@dcrackel/hematournamentui 1.0.712 → 1.0.714

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.prettierignore +7 -7
  2. package/.storybook/main.js +3 -1
  3. package/README.md +42 -0
  4. package/dist/HemaTournamentUI-lib.es.js +6856 -6243
  5. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  6. package/dist/hematournamentui-tailwind.css +79 -9
  7. package/package.json +96 -96
  8. package/prettier.config.js +16 -16
  9. package/src/index.js +43 -42
  10. package/src/main.js +1 -1
  11. package/src/mocks/getDEBouts-TableOf128.js +37 -37
  12. package/src/mocks/getDEBouts-TableOf64.js +35 -35
  13. package/src/stories/Atoms/Input/BaseInput.test.js +97 -97
  14. package/src/stories/Atoms/Input/BaseInput.vue +149 -149
  15. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -23
  16. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -35
  17. package/src/stories/Molecules/Indicators/RegistrationStatus/RegistrationStatus.vue +30 -30
  18. package/src/stories/Molecules/Menus/HoverActions/HoverActions.test.js +138 -0
  19. package/src/stories/Molecules/Menus/HoverActions/HoverActions.vue +212 -0
  20. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.stories.js +49 -0
  21. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.test.js +129 -0
  22. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue +165 -0
  23. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.stories.js +6 -0
  24. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -18
  25. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +1 -1
  26. package/src/stories/Organisms/Cards/BoutCard/BoutCard.vue +5 -1
  27. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardActive/BoutCardActive.vue +19 -10
  28. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardInactive/BoutCardInactive.vue +11 -7
  29. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +42 -33
  30. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +76 -45
  31. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +122 -103
  32. package/src/stories/Organisms/Cards/Director/DirectorCard.test.js +82 -0
  33. package/src/stories/Organisms/Cards/Director/DirectorCard.vue +32 -51
  34. package/src/stories/Organisms/Cards/EditBout/EditBout.test.js +51 -51
  35. package/src/stories/Organisms/Cards/EventCard/EventCard.test.js +106 -106
  36. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -298
  37. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +77 -60
  38. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +144 -143
  39. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +289 -256
  40. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -69
  41. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -80
  42. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +153 -153
  43. package/src/stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue +4 -0
  44. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -31
  45. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -109
  46. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -192
  47. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +1 -0
  48. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +247 -204
  49. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +89 -15
  50. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +51 -29
  51. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +63 -46
  52. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -21
  53. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -39
  54. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -145
  55. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -64
  56. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +36 -57
  57. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +101 -65
  58. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +14 -1
  59. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +52 -52
  60. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +10 -3
  61. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -226
  62. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  63. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +137 -137
  64. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -172
  65. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -216
  66. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -79
  67. package/src/stories/Organisms/Wizards/EditEventWizard/RuleSerialization.test.js +29 -29
  68. package/src/stories/Organisms/Wizards/EditEventWizard/WeaponSelection.test.js +138 -0
  69. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +149 -9
  70. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +54 -2
  71. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +77 -8
  72. package/src/stories/Templates/EventManagement/Bracket/BracketDirectorAssignment.test.js +242 -0
  73. package/src/stories/Templates/EventManagement/Bracket/BracketMobile.test.js +131 -0
  74. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +50 -39
  75. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +201 -0
  76. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.js +93 -0
  77. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.test.js +152 -0
  78. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -39
  79. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -79
  80. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -93
  81. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -91
  82. package/src/stories/Templates/EventManagement/PoolLive/PoolLive.test.js +121 -121
  83. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.stories.js +9 -2
  84. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue +5 -4
  85. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +81 -81
  86. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -39
  87. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +101 -101
  88. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.stories.js +71 -71
  89. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue +86 -86
  90. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.stories.js +86 -86
  91. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.vue +247 -247
  92. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -170
  93. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -156
  94. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -329
  95. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -329
  96. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -42
  97. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -84
  98. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +114 -114
  99. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -363
  100. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -116
  101. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +307 -307
  102. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -45
  103. package/src/stories/Util/Validations/PriceValidation.js +6 -6
  104. package/src/stories/Util/boutDirectorAssignment.js +46 -0
  105. package/src/stories/Util/bracketDirectors.js +86 -0
  106. package/src/stories/Util/registrationStatus.js +11 -11
  107. package/src/stories/Util/tabs.js +19 -47
@@ -0,0 +1,138 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import BasicEventInfo from './BasicEventInfo/BasicEventInfo.vue';
4
+ import EditEventWizard from './EditEventWizard.vue';
5
+ import DropDownMenu from '../../ComplexInputs/DropDown/DropDownMenu.vue';
6
+
7
+ const wrappers = [];
8
+ const weapons = [
9
+ { text: 'Sword & Buckler', link: 8 },
10
+ { text: 'MG Longsword', link: 17 },
11
+ { text: 'Longsword', link: 1 }
12
+ ];
13
+
14
+ afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount()));
15
+
16
+ function mountBasic(eventConfig, options = weapons) {
17
+ const wrapper = mount(BasicEventInfo, {
18
+ props: {
19
+ eventConfig,
20
+ weapons: options,
21
+ validation: { EventName: { invalid: false } },
22
+ steps: [],
23
+ currentStep: 0
24
+ },
25
+ global: { stubs: { DatePicker: true, StepIndicator: true } }
26
+ });
27
+ wrappers.push(wrapper);
28
+ return wrapper;
29
+ }
30
+
31
+ function selectedText(wrapper) {
32
+ return wrapper.findComponent(DropDownMenu).find('input').element.value;
33
+ }
34
+
35
+ describe('event weapon selection', () => {
36
+ it.each([1, '1', 8, '8', 17, '17'])('shows the actual saved weapon for ID %s without emitting a change', WeaponId => {
37
+ const wrapper = mountBasic({ WeaponId });
38
+ expect(selectedText(wrapper)).toBe(weapons.find(option => String(option.link) === String(WeaponId)).text);
39
+ expect(wrapper.emitted('update')).toBeUndefined();
40
+ });
41
+
42
+ it('accepts raw weapon records as well as dropdown options', () => {
43
+ const wrapper = mountBasic(
44
+ { WeaponId: '8' },
45
+ weapons.map(option => ({ Id: option.link, Name: option.text }))
46
+ );
47
+ expect(selectedText(wrapper)).toBe('Sword & Buckler');
48
+ expect(wrapper.findComponent(DropDownMenu).props('items')).toEqual(
49
+ weapons.map(option => ({ ...option, Id: option.link, Name: option.text }))
50
+ );
51
+ });
52
+
53
+ it.each([undefined, 99])('does not pretend an unavailable weapon %s is Longsword', WeaponId => {
54
+ const wrapper = mountBasic({ WeaponId });
55
+ expect(selectedText(wrapper)).toBe('Select a weapon');
56
+ expect(wrapper.emitted('update')).toBeUndefined();
57
+ });
58
+
59
+ it('reconciles a parent weapon change and restores that selection after closing the menu', async () => {
60
+ const wrapper = mountBasic({ WeaponId: 8 });
61
+ await wrapper.setProps({ eventConfig: { WeaponId: 1 } });
62
+ expect(selectedText(wrapper)).toBe('Longsword');
63
+ const dropdown = wrapper.findComponent(DropDownMenu);
64
+ dropdown.vm.clearInput();
65
+ dropdown.vm.restorePreviousValue();
66
+ await wrapper.vm.$nextTick();
67
+ expect(selectedText(wrapper)).toBe('Longsword');
68
+ expect(wrapper.emitted('update')).toBeUndefined();
69
+ });
70
+
71
+ it('shows the saved selection when the weapon options arrive later', async () => {
72
+ const wrapper = mountBasic({ WeaponId: 8 }, []);
73
+ await wrapper.setProps({ weapons });
74
+ expect(selectedText(wrapper)).toBe('Sword & Buckler');
75
+ expect(wrapper.emitted('update')).toBeUndefined();
76
+ });
77
+
78
+ it('emits the chosen option ID only after an explicit selection', async () => {
79
+ const wrapper = mountBasic({ WeaponId: 8 });
80
+ const dropdown = wrapper.findComponent(DropDownMenu);
81
+ dropdown.vm.handleDropDown();
82
+ await wrapper.vm.$nextTick();
83
+ await dropdown
84
+ .findAll('a')
85
+ .find(option => option.text() === 'Longsword')
86
+ .trigger('click');
87
+ expect(wrapper.emitted('update')).toEqual([[{ WeaponId: 1 }]]);
88
+ });
89
+
90
+ it('preserves the saved weapon when another field is edited and saves a deliberate weapon change', async () => {
91
+ const event = { EventId: 12, EventName: 'Example Event', EventPrice: 25, WeaponId: '8', rules: [] };
92
+ const wrapper = mount(EditEventWizard, {
93
+ props: { event, weapons, editMode: true },
94
+ global: { stubs: { DatePicker: true, StepIndicator: true } }
95
+ });
96
+ wrappers.push(wrapper);
97
+ await wrapper.vm.$nextTick();
98
+ expect(selectedText(wrapper)).toBe('Sword & Buckler');
99
+ wrapper.vm.handleValueChange({ EventName: 'Renamed Event' });
100
+ wrapper.vm.handleUpdate();
101
+ expect(wrapper.emitted('save:step')[0][0].event.WeaponId).toBe('8');
102
+
103
+ wrapper.findComponent(DropDownMenu).vm.handleClick(weapons.find(option => option.link === 17));
104
+ await wrapper.vm.$nextTick();
105
+ wrapper.vm.handleUpdate();
106
+ expect(wrapper.emitted('save:step')[1][0].event.WeaponId).toBe(17);
107
+ expect(event.WeaponId).toBe('8');
108
+ });
109
+ });
110
+
111
+ it.each([false, true])(
112
+ 'actual dropdown choices retain catalog IDs through submit with edit mode %s',
113
+ async editMode => {
114
+ const event = { EventId: 230, EventName: 'MG Longsword', EventPrice: 45, WeaponId: 17, rules: [] };
115
+ const wrapper = mount(EditEventWizard, {
116
+ props: { event, weapons, editMode },
117
+ global: { stubs: { DatePicker: true, StepIndicator: true } }
118
+ });
119
+ wrappers.push(wrapper);
120
+ await wrapper.vm.$nextTick();
121
+ expect(selectedText(wrapper)).toBe('MG Longsword');
122
+ for (const option of weapons) {
123
+ const dropdown = wrapper.findComponent(DropDownMenu);
124
+ dropdown.vm.handleDropDown();
125
+ await wrapper.vm.$nextTick();
126
+ await dropdown
127
+ .findAll('a')
128
+ .find(item => item.text() === option.text)
129
+ .trigger('click');
130
+ expect(selectedText(wrapper)).toBe(option.text);
131
+ wrapper.vm.handleSubmit();
132
+ const eventName = editMode ? 'save:step' : 'update:submit';
133
+ const submitted = wrapper.emitted(eventName).at(-1)[0];
134
+ expect((editMode ? submitted.event : submitted).WeaponId).toBe(option.link);
135
+ }
136
+ expect(event.WeaponId).toBe(17);
137
+ }
138
+ );
@@ -1,5 +1,6 @@
1
- import { ref, watch } from 'vue';
1
+ import { computed, nextTick, ref, watch } from 'vue';
2
2
  import Bracket from './Bracket.vue';
3
+ import { distributeStoryAssignments } from './BracketStoryAssignments.js';
3
4
  import {
4
5
  activateStoryBout,
5
6
  canStoryDirectorDirectBout,
@@ -9,7 +10,8 @@ import {
9
10
  replaceStoryBout
10
11
  } from './BracketStoryHarness.js';
11
12
  import {
12
- plannedBoutAssignments,
13
+ availableDirectors,
14
+ availableRings,
13
15
  plannedDirectingStations,
14
16
  plannedSuggestionViewerId
15
17
  } from './BracketPlannedSuggestions.js';
@@ -30,11 +32,55 @@ const renderInteractiveBracket = args => ({
30
32
  setup() {
31
33
  const storyBouts = ref(cloneStoryBouts(args.bouts));
32
34
  const storyStations = ref(cloneStations(args.plannedDirectingStations));
35
+ const storyAssignments = ref([]);
36
+ const canAddDirector = computed(() =>
37
+ (args.availableDirectors || availableDirectors).some(
38
+ director =>
39
+ !storyStations.value.some(station => String(station.Director?.PersonId) === String(director.PersonId))
40
+ )
41
+ );
42
+ const redistributeAssignments = () => {
43
+ storyAssignments.value = distributeStoryAssignments({
44
+ bouts: storyBouts.value,
45
+ stations: storyStations.value,
46
+ previousAssignments: storyAssignments.value
47
+ });
48
+ };
49
+ const applyProvidedAssignments = assignments => {
50
+ redistributeAssignments();
51
+ const providedIds = new Set(assignments.map(assignment => String(assignment.DEBoutId)));
52
+ storyAssignments.value = [
53
+ ...storyAssignments.value.filter(assignment => !providedIds.has(String(assignment.DEBoutId))),
54
+ ...assignments.map(assignment => ({ ...assignment }))
55
+ ];
56
+ };
57
+ const assignmentBout = ref(null);
58
+ const directorPicker = ref(null);
59
+ const assignmentDialog = ref(null);
60
+
61
+ watch(() => args.plannedBoutAssignments, applyProvidedAssignments, { deep: true, immediate: true });
62
+
63
+ const requestBoutDirector = async bout => {
64
+ assignmentBout.value = bout;
65
+ await nextTick();
66
+ assignmentDialog.value?.showModal();
67
+ directorPicker.value?.focus();
68
+ };
69
+
70
+ const assignPlannedDirector = ringId => {
71
+ const bout = assignmentBout.value;
72
+ const station = storyStations.value.find(item => String(item.RingId) === String(ringId));
73
+ if (!bout || !station) return;
74
+ const remaining = storyAssignments.value.filter(item => String(item.DEBoutId) !== String(bout.DEBoutId));
75
+ storyAssignments.value = [...remaining, { DEBoutId: bout.DEBoutId, RingId: station.RingId }];
76
+ assignmentBout.value = null;
77
+ };
33
78
 
34
79
  watch(
35
80
  () => args.bouts,
36
81
  bouts => {
37
82
  storyBouts.value = cloneStoryBouts(bouts);
83
+ redistributeAssignments();
38
84
  },
39
85
  { deep: true }
40
86
  );
@@ -43,6 +89,7 @@ const renderInteractiveBracket = args => ({
43
89
  () => args.plannedDirectingStations,
44
90
  stations => {
45
91
  storyStations.value = cloneStations(stations);
92
+ redistributeAssignments();
46
93
  },
47
94
  { deep: true }
48
95
  );
@@ -52,13 +99,18 @@ const renderInteractiveBracket = args => ({
52
99
  };
53
100
 
54
101
  const getViewerStation = () =>
55
- 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));
56
107
 
57
108
  const handleAddPlannedDirector = () => {
58
109
  const assignedRingIds = new Set(storyStations.value.map(station => String(station.RingId)));
59
110
  const nextStation = plannedDirectingStations.find(station => !assignedRingIds.has(String(station.RingId)));
60
111
  if (!nextStation) return;
61
112
  storyStations.value = [...storyStations.value, ...cloneStations([nextStation])];
113
+ redistributeAssignments();
62
114
  };
63
115
 
64
116
  const handleDirectingBout = bout => {
@@ -75,13 +127,43 @@ const renderInteractiveBracket = args => ({
75
127
  args,
76
128
  storyBouts,
77
129
  storyStations,
130
+ storyAssignments,
131
+ canAddDirector,
132
+ assignmentBout,
133
+ directorPicker,
134
+ assignmentDialog,
135
+ requestBoutDirector,
136
+ assignPlannedDirector,
78
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
+ },
79
148
  handleCloseDirectingBout,
80
149
  handleDirectingBout,
81
150
  persistStoryBout
82
151
  };
83
152
  },
84
153
  template: `
154
+ <div>
155
+ <dialog v-if="assignmentBout" ref="assignmentDialog" aria-label="Assign director" class="fixed inset-0 m-auto rounded border border-dropdownSelect bg-white p-4 shadow-lg backdrop:bg-black/20" @cancel="assignmentBout = null">
156
+ <label class="text-sm text-primaryHighlight">
157
+ Assign director to bout {{ assignmentBout.DEBoutId }}
158
+ <select ref="directorPicker" class="ml-2 rounded border border-dropdownSelect p-2" @change="assignPlannedDirector($event.target.value)">
159
+ <option value="">Choose director</option>
160
+ <option v-for="station in storyStations" :key="station.RingId" :value="station.RingId">
161
+ {{ station.Director?.DisplayName || 'Director TBD' }} / {{ station.RingName }}
162
+ </option>
163
+ </select>
164
+ </label>
165
+ <button type="button" class="ml-4 text-sm underline" @click="assignmentBout = null">Cancel</button>
166
+ </dialog>
85
167
  <Bracket
86
168
  v-bind="args"
87
169
  :connectedToServer="args.connectedToServer"
@@ -89,13 +171,19 @@ const renderInteractiveBracket = args => ({
89
171
  :hostingClubColors="args.hostingClubColors"
90
172
  :bouts="storyBouts"
91
173
  :plannedDirectingStations="storyStations"
174
+ :plannedBoutAssignments="storyAssignments"
175
+ :showAddPlannedDirectorCard="args.showAddPlannedDirectorCard && canAddDirector"
176
+ @request:bout-director="requestBoutDirector"
92
177
  @close:directing-bout="handleCloseDirectingBout"
93
178
  @request:add-planned-director="handleAddPlannedDirector"
179
+ @request:remove-planned-director="handleRemovePlannedDirector"
180
+ @update:planned-directing-stations="handlePlannedDirectorsUpdate"
94
181
  @update:directing-bout="handleDirectingBout"
95
182
  @request:bout-active="handleDirectingBout"
96
183
  @update:bout="persistStoryBout"
97
184
  @submit:bout="persistStoryBout"
98
185
  />
186
+ </div>
99
187
  `
100
188
  });
101
189
 
@@ -105,18 +193,30 @@ export default {
105
193
  tags: ['Bracket'],
106
194
  render: renderInteractiveBracket,
107
195
  args: {
196
+ availableDirectors,
197
+ rings: availableRings,
108
198
  bouts: getDEWithBouts.pools[0].Bouts,
109
199
  eventRules: getDEWithBouts.eventRules,
110
200
  hostingClubColors: getDEWithBouts.hostingClubColors,
111
201
  connectedToServer: true,
112
202
  role: 'admin',
113
- plannedDirectingStations: [],
203
+ plannedDirectingStations: plannedDirectingStations.slice(0, 2),
114
204
  plannedBoutAssignments: [],
115
- viewerPersonId: null,
116
- showPlannedDirectorAssignments: false,
117
- showAddPlannedDirectorCard: true
205
+ viewerPersonId: plannedSuggestionViewerId,
206
+ showPlannedDirectorAssignments: true,
207
+ showAddPlannedDirectorCard: true,
208
+ showBoutDirectorAssignment: true
118
209
  },
119
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.' },
216
+ showBoutDirectorAssignment: {
217
+ control: 'boolean',
218
+ description: 'Include Assign director in the pairing hover menu. The parent supplies staff selection and saving.'
219
+ },
120
220
  bouts: {
121
221
  control: 'object'
122
222
  },
@@ -220,10 +320,38 @@ export const PlannedDirectorSuggestions = {
220
320
  status: 'de',
221
321
  role: 'Admin',
222
322
  plannedDirectingStations: plannedDirectingStations.slice(0, 2),
223
- plannedBoutAssignments,
224
323
  viewerPersonId: plannedSuggestionViewerId,
225
324
  showPlannedDirectorAssignments: true,
226
- showAddPlannedDirectorCard: true
325
+ showAddPlannedDirectorCard: true,
326
+ showBoutDirectorAssignment: true
327
+ }
328
+ };
329
+
330
+ export const ThreeDirectors = {
331
+ args: { ...PlannedDirectorSuggestions.args, plannedDirectingStations: plannedDirectingStations.slice(0, 3) }
332
+ };
333
+
334
+ export const FourDirectors = {
335
+ args: { ...PlannedDirectorSuggestions.args, plannedDirectingStations: plannedDirectingStations.slice(0, 4) }
336
+ };
337
+
338
+ export const FiveDirectors = {
339
+ args: { ...PlannedDirectorSuggestions.args, plannedDirectingStations: plannedDirectingStations.slice(0, 5) }
340
+ };
341
+
342
+ export const SixDirectors = {
343
+ args: { ...PlannedDirectorSuggestions.args, plannedDirectingStations }
344
+ };
345
+
346
+ export const HoverBoutActions = {
347
+ args: { ...PlannedDirectorSuggestions.args },
348
+ parameters: {
349
+ docs: {
350
+ description: {
351
+ story:
352
+ 'Hover over a pairing for Open bout and Assign director. Clicking the pairing still opens it directly. The picker in this story simulates the consuming app; the story simulates balanced section coverage through the semifinals, while production assignment and persistence remain app-owned.'
353
+ }
354
+ }
227
355
  }
228
356
  };
229
357
 
@@ -246,3 +374,15 @@ export const noBoutsYet = {
246
374
  status: 'live'
247
375
  }
248
376
  };
377
+
378
+ export const PhoneDirector = {
379
+ args: { ...PlannedDirectorSuggestions.args, role: 'Director' },
380
+ parameters: {
381
+ docs: {
382
+ description: {
383
+ story:
384
+ 'Use a viewport narrower than 768px to see the phone directing view: a bout list, ring suggestions, connection status, and collapsed completed bouts. Organising controls stay on desktop, even when assignment options are enabled. Returning to desktop restores the selected List View or Tableau.'
385
+ }
386
+ }
387
+ }
388
+ };
@@ -157,10 +157,62 @@ describe('Bracket bout interaction', () => {
157
157
  expect(wrapper.emitted('request:add-planned-director')).toEqual([[]]);
158
158
  });
159
159
 
160
- it('does not render planned directing controls when optional behavior is disabled', () => {
161
- const wrapper = mountBracket();
160
+ it('does not render planned directing controls when explicitly disabled', () => {
161
+ const wrapper = mountBracket(undefined, {
162
+ showPlannedDirectorAssignments: false,
163
+ showBoutDirectorAssignment: false
164
+ });
162
165
 
163
166
  expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(false);
164
167
  expect(wrapper.findComponent(Tableau).props('plannedDirectingStations')).toEqual([]);
165
168
  });
166
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
+ });
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <section>
3
- <EditEventsTopMenu :currentTab="'Bracket'" :tabs="tabs(role, isListView)" @tabMenuClick="handleTabMenuClick" />
3
+ <EditEventsTopMenu :currentTab="'Bracket'" :tabs="tabs(role, isMobile)" @tabMenuClick="handleTabMenuClick" />
4
4
  <ToggleHeader
5
5
  :currentSelection="viewName"
6
6
  :key="viewName"
@@ -11,6 +11,12 @@
11
11
  @update:selection="handleViewChange"
12
12
  />
13
13
 
14
+ <ServerConnected
15
+ class="mb-2 md:hidden"
16
+ :connectedToServer="connectedToServer"
17
+ :statusText="connectedToServer ? 'Connected' : 'Disconnected'"
18
+ />
19
+
14
20
  <div class="flex flex-col gap-4 xl:flex-row">
15
21
  <div class="min-w-0 flex-1">
16
22
  <Tableau
@@ -22,6 +28,8 @@
22
28
  :plannedDirectingStations="plannedDirectingStations"
23
29
  :plannedBoutAssignments="plannedBoutAssignments"
24
30
  :viewerPersonId="viewerPersonId"
31
+ :showBoutDirectorAssignment="canShowBoutDirectorAssignment"
32
+ @request:bout-director="handleBoutDirectorRequest"
25
33
  @action:editBout="editBout"
26
34
  @action:directingBout="directBout"
27
35
  />
@@ -32,13 +40,16 @@
32
40
  class="order-first flex-none xl:order-last"
33
41
  :stations="plannedDirectingStations"
34
42
  :showAddCard="showAddPlannedDirectorCard"
35
- @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)"
36
47
  />
37
48
  </div>
38
49
 
39
50
  <div v-if="bouts.length < 1" class="w-full flex justify-center items-center">
40
51
  <div class="text-center mb-10">
41
- <img alt="Small desert scene with wind blowing." class="w-96 mx-auto" :src="emptyDesertIcon" />
52
+ <img alt="Small desert scene with wind blowing." class="hidden md:block w-96 mx-auto" :src="emptyDesertIcon" />
42
53
  <BaseText text="Nothing to Show Yet!" size="lg" color="quinary" weight="bold" class="text-center" />
43
54
  <BaseText
44
55
  text="Check Back when this event has progress to Direct Elimination"
@@ -47,6 +58,7 @@
47
58
  weight="normal"
48
59
  />
49
60
  <BaseText
61
+ class="hidden md:block"
50
62
  text="The Generate bracket button will appear on the bottom of the pools screen"
51
63
  size="sm"
52
64
  color="quinary"
@@ -55,6 +67,15 @@
55
67
  </div>
56
68
  </div>
57
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
+
58
79
  <DirectorModal
59
80
  :bout="selectedBout"
60
81
  :eventRules="eventRules"
@@ -84,13 +105,17 @@
84
105
  <script>
85
106
  import EditEventsTopMenu from '../../Menus/EditEventsTopMenu/EditEventsTopMenu.vue';
86
107
  import ToggleHeader from '../../../Organisms/Headers/ToggleHeader/ToggleHeader.vue';
108
+ import ServerConnected from '../../../Molecules/Indicators/ServerConnected/ServerConnected.vue';
87
109
  import BaseText from '../../../Atoms/Text/BaseText.vue';
88
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';
89
113
  import EditBoutModal from '../../../Molecules/Modals/EditBoutModal/EditBoutModal.vue';
90
114
  import Tableau from '../../../Organisms/Containers/Tableau/Tableau.vue';
91
115
  import BracketDirectorAssignmentPanel from '../../../Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
92
116
  import emptyDesertIcon from '../../../../assets/empty_desert_icon.png';
93
117
  import { tabs } from '../../../Util/tabs.js';
118
+ import { canAssignBoutDirector } from '../../../Util/boutDirectorAssignment.js';
94
119
 
95
120
  const isByeFencer = person => {
96
121
  if (!person?.PersonId || Number(person.PersonId) === 0) return true;
@@ -110,7 +135,10 @@ export default {
110
135
  'penalty:remove',
111
136
  'penalty:update',
112
137
  'request:add-planned-director',
138
+ 'request:remove-planned-director',
139
+ 'update:planned-directing-stations',
113
140
  'request:bout-active',
141
+ 'request:bout-director',
114
142
  'submit:bout',
115
143
  'tab:menu-click',
116
144
  'update:bout',
@@ -121,11 +149,16 @@ export default {
121
149
  EditBoutModal,
122
150
  DirectorModal,
123
151
  BracketDirectorAssignmentPanel,
152
+ BracketDirectorsModal,
124
153
  BaseText,
125
154
  ToggleHeader,
155
+ ServerConnected,
126
156
  EditEventsTopMenu
127
157
  },
128
158
  props: {
159
+ availableDirectors: { type: Array, default: undefined },
160
+ rings: { type: Array, default: undefined },
161
+ showBoutDirectorAssignment: { type: Boolean, default: true },
129
162
  bouts: {
130
163
  type: Array,
131
164
  required: true
@@ -164,7 +197,7 @@ export default {
164
197
  },
165
198
  showPlannedDirectorAssignments: {
166
199
  type: Boolean,
167
- default: false
200
+ default: true
168
201
  },
169
202
  showAddPlannedDirectorCard: {
170
203
  type: Boolean,
@@ -175,17 +208,30 @@ export default {
175
208
  return {
176
209
  emptyDesertIcon,
177
210
  showDirectorModal: false,
211
+ showPlannedDirectorsModal: false,
178
212
  showEditBoutModal: false,
179
213
  selectedBout: {},
180
214
  tabs,
181
215
  viewName: 'Tableau',
182
216
  boutsData: this.cloneBouts(this.bouts),
183
- isListView: false
217
+ windowWidth: typeof window === 'undefined' ? 1024 : window.innerWidth
184
218
  };
185
219
  },
186
220
  computed: {
221
+ canManagePlannedDirectors() {
222
+ return !this.isMobile && Array.isArray(this.availableDirectors) && Array.isArray(this.rings);
223
+ },
224
+ isMobile() {
225
+ return this.windowWidth < 768;
226
+ },
227
+ isListView() {
228
+ return this.isMobile || this.viewName === 'List View';
229
+ },
230
+ canShowBoutDirectorAssignment() {
231
+ return !this.isMobile && this.showBoutDirectorAssignment;
232
+ },
187
233
  showPlannedAssignmentPanel() {
188
- return this.showPlannedDirectorAssignments || this.plannedDirectingStations.length > 0;
234
+ return !this.isMobile && (this.showPlannedDirectorAssignments || this.plannedDirectingStations.length > 0);
189
235
  },
190
236
  isCountingBackwardsMaxScore() {
191
237
  // 1) see if PoolScoreDirection exists and is explicitly "false"
@@ -213,6 +259,9 @@ export default {
213
259
  }
214
260
  },
215
261
  watch: {
262
+ isMobile(value) {
263
+ if (value) this.showPlannedDirectorsModal = false;
264
+ },
216
265
  bouts: {
217
266
  handler(newBouts) {
218
267
  this.boutsData = this.cloneBouts(newBouts);
@@ -230,6 +279,27 @@ export default {
230
279
  window.removeEventListener('resize', this.updateViewBasedOnScreenSize);
231
280
  },
232
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
+ },
297
+ handleBoutDirectorRequest(bout) {
298
+ if (!this.canShowBoutDirectorAssignment) return;
299
+ const currentBout = this.boutsData.find(item => String(item.BoutId) === String(bout?.BoutId));
300
+ if (!canAssignBoutDirector(currentBout)) return;
301
+ this.$emit('request:bout-director', JSON.parse(JSON.stringify(currentBout)));
302
+ },
233
303
  handleCloseModal() {
234
304
  const closedDirectingBout = this.showDirectorModal && this.selectedBout?.BoutId ? { ...this.selectedBout } : null;
235
305
  this.showDirectorModal = false;
@@ -242,7 +312,6 @@ export default {
242
312
  },
243
313
  handleViewChange(value) {
244
314
  this.viewName = value;
245
- this.isListView = value === 'List View';
246
315
  },
247
316
  handleUpdateBout(bout) {
248
317
  if (!bout?.BoutId) return;
@@ -279,7 +348,7 @@ export default {
279
348
  this.selectedBout = updatedBout;
280
349
  },
281
350
  updateViewBasedOnScreenSize() {
282
- this.isListView = window.innerWidth <= 768;
351
+ this.windowWidth = window.innerWidth;
283
352
  },
284
353
  handleUpdatePenalty(penalty) {
285
354
  this.$emit('penalty:update', penalty);