@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
@@ -5,9 +5,9 @@
5
5
 
6
6
  <div v-for="bout in section.bouts" :key="bout.BoutId" class="mb-4">
7
7
  <div
8
- v-if="getPlannedStation(bout)"
8
+ v-if="getPlannedStation(bout) && getBoutDirectorLabel(bout)"
9
9
  data-testid="planned-bout-label"
10
- class="mb-1 flex items-center gap-2 rounded border-l-4 border-dropdownSelect bg-white px-2 py-1 shadow-sm"
10
+ class="mb-1 hidden md:flex items-center justify-center gap-2 rounded border-l-4 border-dropdownSelect bg-white px-2 py-1 shadow-sm"
11
11
  :style="{ borderLeftColor: getPlannedStation(bout).Color }"
12
12
  >
13
13
  <span
@@ -15,19 +15,51 @@
15
15
  :style="{ backgroundColor: getPlannedStation(bout).Color }"
16
16
  aria-hidden="true"
17
17
  ></span>
18
- <BaseText :text="getPlannedStationLabel(bout)" color="quaternary" size="xs" />
18
+ <BaseText :text="getBoutDirectorLabel(bout)" color="quaternary" size="xs" />
19
19
  </div>
20
+ <HoverActions
21
+ :actions="getBoutActions(bout)"
22
+ :focusable="!canOpenBracketBout(bout)"
23
+ @action="handleListBoutAction(bout, $event)"
24
+ >
25
+ <BoutCard
26
+ :bout="bout"
27
+ :hostingClubColors="hostingClubColors"
28
+ :editMode="canOpenBracketBout(bout)"
29
+ :reorderMode="false"
30
+ :timerStatus="bout.TimerStatus || 'stopped'"
31
+ :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
32
+ @action:startBout="handleDirectingFromIds"
33
+ @action:resumeBout="handleDirectingFromIds"
34
+ @action:editBout="handleEditFromIds"
35
+ >
36
+ <template v-if="getBoutDirectorLabel(bout)" #mobile-details>
37
+ <BaseText
38
+ :text="getBoutDirectorLabel(bout)"
39
+ color="quaternary"
40
+ size="xs"
41
+ class="w-full min-w-0 break-words text-center"
42
+ />
43
+ </template>
44
+ </BoutCard>
45
+ </HoverActions>
46
+ </div>
47
+ </div>
48
+ <details v-if="completedListBouts.length" class="my-4 w-full md:w-3/4">
49
+ <summary class="cursor-pointer py-3 text-sm font-bold text-primaryHighlight">
50
+ Completed bouts ({{ completedListBouts.length }})
51
+ </summary>
52
+ <div v-for="bout in completedListBouts" :key="bout.BoutId" class="mb-4">
20
53
  <BoutCard
21
54
  :bout="bout"
22
55
  :hostingClubColors="hostingClubColors"
23
56
  :editMode="true"
24
57
  :reorderMode="false"
25
- @action:startBout="handleDirectingFromIds"
26
- @action:resumeBout="handleDirectingFromIds"
58
+ :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
27
59
  @action:editBout="handleEditFromIds"
28
60
  />
29
61
  </div>
30
- </div>
62
+ </details>
31
63
  </section>
32
64
 
33
65
  <section
@@ -47,6 +79,9 @@
47
79
  :bracketSize="bracketSize"
48
80
  :layout="getRoundLayout(roundIndex)"
49
81
  :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
82
+ :showBoutDirectorAssignment="showBoutDirectorAssignment"
83
+ :plannedStationByBout="plannedStationByBout"
84
+ @request:bout-director="$emit('request:bout-director', $event)"
50
85
  @register-node-anchor="registerNodeAnchor"
51
86
  @action:editBout="handleEditBout"
52
87
  @action:directingBout="handleDirectorBout"
@@ -63,6 +98,9 @@
63
98
  :bracketSize="bracketSize"
64
99
  :layout="placementLayout"
65
100
  :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
101
+ :showBoutDirectorAssignment="showBoutDirectorAssignment"
102
+ :plannedStationByBout="plannedStationByBout"
103
+ @request:bout-director="$emit('request:bout-director', $event)"
66
104
  @register-node-anchor="registerNodeAnchor"
67
105
  @action:editBout="handleEditBout"
68
106
  @action:directingBout="handleDirectorBout"
@@ -81,6 +119,9 @@
81
119
  :bracketSize="bracketSize"
82
120
  :layout="getRoundLayout(roundIndex)"
83
121
  :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
122
+ :showBoutDirectorAssignment="showBoutDirectorAssignment"
123
+ :plannedStationByBout="plannedStationByBout"
124
+ @request:bout-director="$emit('request:bout-director', $event)"
84
125
  @register-node-anchor="registerNodeAnchor"
85
126
  @action:editBout="handleEditBout"
86
127
  @action:directingBout="handleDirectorBout"
@@ -95,6 +136,8 @@
95
136
 
96
137
  <script>
97
138
  import BaseText from '../../../Atoms/Text/BaseText.vue';
139
+ import { canAssignBoutDirector, canOpenBracketBout, createBoutActions } from '../../../Util/boutDirectorAssignment.js';
140
+ import HoverActions from '../../../Molecules/Menus/HoverActions/HoverActions.vue';
98
141
  import BoutCard from '../../../Organisms/Cards/BoutCard/BoutCard.vue';
99
142
  import TableauColumn from '../TableauColumn/TableauColumn.vue';
100
143
  import TableauSvgOverlay from './components/TableauSvgOverlay.vue';
@@ -149,12 +192,16 @@ const addBoutToRound = (rounds, bout) => {
149
192
 
150
193
  const groupBoutsByRound = bouts => (bouts || []).reduce(addBoutToRound, {});
151
194
 
152
- const isVisibleListBout = bout =>
153
- bout.Person1?.DisplayName !== 'BYE' && bout.Person2?.DisplayName !== 'BYE' && bout.Status !== 'Completed';
195
+ const isCompletedBout = bout => String(bout.Status || '').toLowerCase() === 'completed';
196
+
197
+ const isVisibleListBout = (bout, showBoutDirectorAssignment) => {
198
+ if (showBoutDirectorAssignment && canAssignBoutDirector(bout)) return true;
199
+ return canOpenBracketBout(bout) && !isCompletedBout(bout);
200
+ };
154
201
 
155
- const createVisibleRoundEntry = ([roundName, bouts]) => [
202
+ const createVisibleRoundEntry = ([roundName, bouts], showBoutDirectorAssignment) => [
156
203
  roundName,
157
- bouts.filter(isVisibleListBout).sort(sortBoutsByNodeId)
204
+ bouts.filter(bout => isVisibleListBout(bout, showBoutDirectorAssignment)).sort(sortBoutsByNodeId)
158
205
  ];
159
206
 
160
207
  const hasBouts = ([, bouts]) => bouts.length > 0;
@@ -176,9 +223,10 @@ const createRoundLayout = depth => {
176
223
 
177
224
  export default {
178
225
  name: 'Tableau',
179
- components: { BaseText, BoutCard, TableauColumn, TableauSvgOverlay },
180
- emits: ['action:editBout', 'action:directingBout'],
226
+ components: { BaseText, BoutCard, TableauColumn, TableauSvgOverlay, HoverActions },
227
+ emits: ['action:editBout', 'action:directingBout', 'request:bout-director'],
181
228
  props: {
229
+ showBoutDirectorAssignment: { type: Boolean, default: true },
182
230
  bouts: { type: Array, required: true },
183
231
  bracketSize: { type: String, required: true },
184
232
  hostingClubColors: { type: Object, required: true },
@@ -216,9 +264,15 @@ export default {
216
264
  loserRoundEntries() {
217
265
  return this.roundEntries.filter(([roundName]) => isConsolationRoundName(roundName)).sort(sortRoundEntries);
218
266
  },
267
+ completedListBouts() {
268
+ return this.bouts.filter(bout => isCompletedBout(bout) && canOpenBracketBout(bout)).sort(sortBoutsByNodeId);
269
+ },
219
270
  filteredBouts() {
220
271
  return Object.fromEntries(
221
- [...this.roundEntries].sort(sortRoundEntries).map(createVisibleRoundEntry).filter(hasBouts)
272
+ [...this.roundEntries]
273
+ .sort(sortRoundEntries)
274
+ .map(entry => createVisibleRoundEntry(entry, this.showBoutDirectorAssignment))
275
+ .filter(hasBouts)
222
276
  );
223
277
  },
224
278
  listSections() {
@@ -290,6 +344,22 @@ export default {
290
344
  }
291
345
  },
292
346
  methods: {
347
+ canOpenBracketBout,
348
+ getBoutActions(bout) {
349
+ const station = this.getPlannedStation(bout);
350
+ return createBoutActions(bout, this.showBoutDirectorAssignment, station?.Director?.DisplayName, station?.Color);
351
+ },
352
+ handleListBoutAction(bout, actionId) {
353
+ if (actionId === 'assign-director') {
354
+ if (!this.showBoutDirectorAssignment || !canAssignBoutDirector(bout)) return;
355
+ this.$emit('request:bout-director', JSON.parse(JSON.stringify(bout)));
356
+ return;
357
+ }
358
+ if (actionId !== 'open' || !canOpenBracketBout(bout)) return;
359
+ const event =
360
+ String(bout.Status || '').toLowerCase() === 'completed' ? 'action:editBout' : 'action:directingBout';
361
+ this.$emit(event, bout);
362
+ },
293
363
  findBoutByIds(idsOrBout) {
294
364
  if (!idsOrBout) return null;
295
365
  if (idsOrBout.BoutId && idsOrBout.Person1) return idsOrBout;
@@ -317,11 +387,15 @@ export default {
317
387
  getPlannedStation(bout) {
318
388
  return getPlannedStationForBout(this.plannedStationByBout, bout);
319
389
  },
320
- getPlannedStationLabel(bout) {
390
+ isActiveListBout(bout) {
391
+ return String(bout.Status || '').toLowerCase() === 'active';
392
+ },
393
+ getBoutDirectorLabel(bout) {
394
+ if (this.isActiveListBout(bout)) return [bout.RefName, bout.RingName].filter(Boolean).join(' / ');
321
395
  const station = this.getPlannedStation(bout);
322
396
  if (!station) return '';
323
397
  const directorName = station.Director?.DisplayName || 'Director TBD';
324
- return `Suggested: ${station.RingName} / ${directorName}`;
398
+ return [directorName, station.RingName].filter(Boolean).join(' / ');
325
399
  },
326
400
  registerNodeAnchor({ id, kind, el }) {
327
401
  const anchors = kind === 'input' ? this.inputAnchors : this.outputAnchors;
@@ -1,29 +1,51 @@
1
- import { shallowMount } from '@vue/test-utils';
2
- import { describe, expect, it } from 'vitest';
3
- import BracketDirectorAssignmentCard from '../../../Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
4
- import BracketDirectorAssignmentPanel from './BracketDirectorAssignmentPanel.vue';
5
-
6
- const stations = [
7
- { RingId: 1, RingName: 'Ring 1', Director: { PersonId: 10, DisplayName: 'Director One' } },
8
- { RingId: 2, RingName: 'Ring 2', Director: { PersonId: 20, DisplayName: 'Director Two' } }
9
- ];
10
-
11
- describe('BracketDirectorAssignmentPanel', () => {
12
- it('renders assigned directors followed by the add summary card', () => {
13
- const wrapper = shallowMount(BracketDirectorAssignmentPanel, { props: { stations } });
14
- const cards = wrapper.findAllComponents(BracketDirectorAssignmentCard);
15
-
16
- expect(cards).toHaveLength(3);
17
- expect(cards.slice(0, 2).map(card => card.props('station'))).toEqual(stations);
18
- expect(cards[2].props('variant')).toBe('add');
19
- });
20
-
21
- it('forwards add intent to its parent', async () => {
22
- const wrapper = shallowMount(BracketDirectorAssignmentPanel, { props: { stations: [] } });
23
-
24
- wrapper.findComponent(BracketDirectorAssignmentCard).vm.$emit('action:add');
25
- await wrapper.vm.$nextTick();
26
-
27
- expect(wrapper.emitted('request:add-planned-director')).toEqual([[]]);
28
- });
29
- });
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import BracketDirectorAssignmentCard from '../../../Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
4
+ import BracketDirectorAssignmentPanel from './BracketDirectorAssignmentPanel.vue';
5
+
6
+ const stations = [
7
+ { RingId: 1, RingName: 'Ring 1', Director: { PersonId: 10, DisplayName: 'Director One' } },
8
+ { RingId: 2, RingName: 'Ring 2', Director: { PersonId: 20, DisplayName: 'Director Two' } }
9
+ ];
10
+
11
+ describe('BracketDirectorAssignmentPanel', () => {
12
+ it('renders assigned directors followed by the add summary card', () => {
13
+ const wrapper = shallowMount(BracketDirectorAssignmentPanel, { props: { stations } });
14
+ const cards = wrapper.findAllComponents(BracketDirectorAssignmentCard);
15
+
16
+ expect(cards).toHaveLength(3);
17
+ expect(cards.slice(0, 2).map(card => card.props('station'))).toEqual(stations);
18
+ expect(cards[2].props('variant')).toBe('add');
19
+ });
20
+
21
+ it('forwards add intent to its parent', async () => {
22
+ const wrapper = shallowMount(BracketDirectorAssignmentPanel, { props: { stations: [] } });
23
+
24
+ wrapper.findComponent(BracketDirectorAssignmentCard).vm.$emit('action:add');
25
+ await wrapper.vm.$nextTick();
26
+
27
+ expect(wrapper.emitted('request:add-planned-director')).toEqual([[]]);
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
+ });
51
+ });
@@ -1,46 +1,63 @@
1
- <template>
2
- <aside
3
- data-testid="bracket-director-assignment-panel"
4
- class="w-full rounded-xl border border-dropdownSelect bg-bannerBackground p-4 shadow-lg xl:sticky xl:top-4 xl:w-72"
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
- />
14
- </div>
15
-
16
- <div class="space-y-3">
17
- <BracketDirectorAssignmentCard v-for="station in stations" :key="station.RingId" :station="station" />
18
- <BracketDirectorAssignmentCard
19
- v-if="showAddCard"
20
- variant="add"
21
- @action:add="$emit('request:add-planned-director')"
22
- />
23
- </div>
24
- </aside>
25
- </template>
26
-
27
- <script>
28
- import BaseText from '../../../../Atoms/Text/BaseText.vue';
29
- import BracketDirectorAssignmentCard from '../../../Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
30
-
31
- export default {
32
- name: 'BracketDirectorAssignmentPanel',
33
- components: { BaseText, BracketDirectorAssignmentCard },
34
- emits: ['request:add-planned-director'],
35
- props: {
36
- stations: {
37
- type: Array,
38
- default: () => []
39
- },
40
- showAddCard: {
41
- type: Boolean,
42
- default: true
43
- }
44
- }
45
- };
46
- </script>
1
+ <template>
2
+ <aside
3
+ data-testid="bracket-director-assignment-panel"
4
+ class="w-full rounded-xl border border-dropdownSelect bg-bannerBackground p-4 shadow-lg xl:sticky xl:top-4 xl:w-72"
5
+ >
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>
25
+ </div>
26
+
27
+ <div class="space-y-3">
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
+ />
34
+ <BracketDirectorAssignmentCard
35
+ v-if="showAddCard"
36
+ variant="add"
37
+ @action:add="$emit('request:add-planned-director')"
38
+ />
39
+ </div>
40
+ </aside>
41
+ </template>
42
+
43
+ <script>
44
+ import BaseText from '../../../../Atoms/Text/BaseText.vue';
45
+ import BracketDirectorAssignmentCard from '../../../Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
46
+
47
+ export default {
48
+ name: 'BracketDirectorAssignmentPanel',
49
+ components: { BaseText, BracketDirectorAssignmentCard },
50
+ emits: ['request:add-planned-director', 'request:remove-planned-director', 'request:manage-planned-directors'],
51
+ props: {
52
+ canManageDirectors: { type: Boolean, default: false },
53
+ stations: {
54
+ type: Array,
55
+ default: () => []
56
+ },
57
+ showAddCard: {
58
+ type: Boolean,
59
+ default: true
60
+ }
61
+ }
62
+ };
63
+ </script>
@@ -1,21 +1,21 @@
1
- import { shallowMount } from '@vue/test-utils';
2
- import { describe, expect, it } from 'vitest';
3
- import TableauSvgOverlay from './TableauSvgOverlay.vue';
4
-
5
- describe('TableauSvgOverlay', () => {
6
- it('uses the standard connector thickness for neutral and assigned paths', () => {
7
- const wrapper = shallowMount(TableauSvgOverlay, {
8
- props: {
9
- size: { width: 200, height: 100 },
10
- paths: [
11
- { key: 'neutral', path: 'M 0 0 L 10 0', stroke: null },
12
- { key: 'assigned', path: 'M 0 10 L 10 10', stroke: '#2563eb' }
13
- ]
14
- }
15
- });
16
- const paths = wrapper.findAll('path');
17
-
18
- expect(paths.map(path => path.attributes('stroke-width'))).toEqual(['1.5', '1.5']);
19
- expect(paths.map(path => path.attributes('stroke'))).toEqual(['currentColor', '#2563eb']);
20
- });
21
- });
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { describe, expect, it } from 'vitest';
3
+ import TableauSvgOverlay from './TableauSvgOverlay.vue';
4
+
5
+ describe('TableauSvgOverlay', () => {
6
+ it('uses the standard connector thickness for neutral and assigned paths', () => {
7
+ const wrapper = shallowMount(TableauSvgOverlay, {
8
+ props: {
9
+ size: { width: 200, height: 100 },
10
+ paths: [
11
+ { key: 'neutral', path: 'M 0 0 L 10 0', stroke: null },
12
+ { key: 'assigned', path: 'M 0 10 L 10 10', stroke: '#2563eb' }
13
+ ]
14
+ }
15
+ });
16
+ const paths = wrapper.findAll('path');
17
+
18
+ expect(paths.map(path => path.attributes('stroke-width'))).toEqual(['1.5', '1.5']);
19
+ expect(paths.map(path => path.attributes('stroke'))).toEqual(['currentColor', '#2563eb']);
20
+ });
21
+ });
@@ -1,39 +1,39 @@
1
- <template>
2
- <svg
3
- class="pointer-events-none absolute inset-0 z-0 text-dropdownSelect"
4
- :width="size.width"
5
- :height="size.height"
6
- :viewBox="`0 0 ${size.width} ${size.height}`"
7
- shape-rendering="crispEdges"
8
- overflow="visible"
9
- aria-hidden="true"
10
- >
11
- <path
12
- v-for="connector in paths"
13
- :key="connector.key"
14
- :d="connector.path"
15
- :data-segment-role="connector.segmentRole"
16
- fill="none"
17
- :stroke="connector.stroke || 'currentColor'"
18
- stroke-width="1.5"
19
- stroke-linecap="square"
20
- stroke-linejoin="miter"
21
- />
22
- </svg>
23
- </template>
24
-
25
- <script>
26
- export default {
27
- name: 'TableauSvgOverlay',
28
- props: {
29
- size: {
30
- type: Object,
31
- required: true
32
- },
33
- paths: {
34
- type: Array,
35
- required: true
36
- }
37
- }
38
- };
39
- </script>
1
+ <template>
2
+ <svg
3
+ class="pointer-events-none absolute inset-0 z-0 text-dropdownSelect"
4
+ :width="size.width"
5
+ :height="size.height"
6
+ :viewBox="`0 0 ${size.width} ${size.height}`"
7
+ shape-rendering="crispEdges"
8
+ overflow="visible"
9
+ aria-hidden="true"
10
+ >
11
+ <path
12
+ v-for="connector in paths"
13
+ :key="connector.key"
14
+ :d="connector.path"
15
+ :data-segment-role="connector.segmentRole"
16
+ fill="none"
17
+ :stroke="connector.stroke || 'currentColor'"
18
+ stroke-width="1.5"
19
+ stroke-linecap="square"
20
+ stroke-linejoin="miter"
21
+ />
22
+ </svg>
23
+ </template>
24
+
25
+ <script>
26
+ export default {
27
+ name: 'TableauSvgOverlay',
28
+ props: {
29
+ size: {
30
+ type: Object,
31
+ required: true
32
+ },
33
+ paths: {
34
+ type: Array,
35
+ required: true
36
+ }
37
+ }
38
+ };
39
+ </script>