@dcrackel/hematournamentui 1.0.712 → 1.0.713

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 (99) hide show
  1. package/.prettierignore +7 -7
  2. package/.storybook/main.js +3 -1
  3. package/README.md +30 -0
  4. package/package.json +96 -96
  5. package/prettier.config.js +16 -16
  6. package/src/main.js +1 -1
  7. package/src/mocks/getDEBouts-TableOf128.js +37 -37
  8. package/src/mocks/getDEBouts-TableOf64.js +35 -35
  9. package/src/stories/Atoms/Input/BaseInput.test.js +97 -97
  10. package/src/stories/Atoms/Input/BaseInput.vue +149 -149
  11. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -23
  12. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -35
  13. package/src/stories/Molecules/Indicators/RegistrationStatus/RegistrationStatus.vue +30 -30
  14. package/src/stories/Molecules/Menus/HoverActions/HoverActions.test.js +138 -0
  15. package/src/stories/Molecules/Menus/HoverActions/HoverActions.vue +212 -0
  16. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.stories.js +6 -0
  17. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -18
  18. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +1 -1
  19. package/src/stories/Organisms/Cards/BoutCard/BoutCard.vue +5 -1
  20. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardActive/BoutCardActive.vue +19 -10
  21. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardInactive/BoutCardInactive.vue +11 -7
  22. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +33 -33
  23. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +45 -45
  24. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +103 -103
  25. package/src/stories/Organisms/Cards/Director/DirectorCard.test.js +82 -0
  26. package/src/stories/Organisms/Cards/Director/DirectorCard.vue +32 -51
  27. package/src/stories/Organisms/Cards/EditBout/EditBout.test.js +51 -51
  28. package/src/stories/Organisms/Cards/EventCard/EventCard.test.js +106 -106
  29. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -298
  30. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +77 -60
  31. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +144 -143
  32. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +289 -256
  33. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -69
  34. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -80
  35. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +153 -153
  36. package/src/stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue +4 -0
  37. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -31
  38. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -109
  39. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -192
  40. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +1 -0
  41. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +247 -204
  42. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +89 -15
  43. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +29 -29
  44. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +46 -46
  45. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -21
  46. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -39
  47. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -145
  48. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -64
  49. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +36 -57
  50. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +101 -65
  51. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +14 -1
  52. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +52 -52
  53. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +10 -3
  54. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -226
  55. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  56. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +137 -137
  57. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -172
  58. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -216
  59. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -79
  60. package/src/stories/Organisms/Wizards/EditEventWizard/RuleSerialization.test.js +29 -29
  61. package/src/stories/Organisms/Wizards/EditEventWizard/WeaponSelection.test.js +138 -0
  62. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +123 -12
  63. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +178 -166
  64. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +35 -7
  65. package/src/stories/Templates/EventManagement/Bracket/BracketDirectorAssignment.test.js +242 -0
  66. package/src/stories/Templates/EventManagement/Bracket/BracketMobile.test.js +131 -0
  67. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +42 -39
  68. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +80 -0
  69. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.js +93 -0
  70. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.test.js +152 -0
  71. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -39
  72. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -79
  73. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -93
  74. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -91
  75. package/src/stories/Templates/EventManagement/PoolLive/PoolLive.test.js +121 -121
  76. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.stories.js +9 -2
  77. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue +5 -4
  78. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +81 -81
  79. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -39
  80. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +101 -101
  81. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.stories.js +71 -71
  82. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue +86 -86
  83. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.stories.js +86 -86
  84. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.vue +247 -247
  85. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -170
  86. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -156
  87. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -329
  88. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -329
  89. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -42
  90. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -84
  91. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +114 -114
  92. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -363
  93. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -116
  94. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +307 -307
  95. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -45
  96. package/src/stories/Util/Validations/PriceValidation.js +6 -6
  97. package/src/stories/Util/boutDirectorAssignment.js +46 -0
  98. package/src/stories/Util/registrationStatus.js +11 -11
  99. package/src/stories/Util/tabs.js +19 -47
@@ -1,256 +1,289 @@
1
- <template>
2
- <section v-if="showBout" class="relative w-40">
3
- <div
4
- v-if="showRingLabel"
5
- data-testid="active-ring"
6
- class="pointer-events-none absolute -top-4 left-0 z-20 flex h-4 w-40 items-center rounded-t bg-primary px-2"
7
- >
8
- <BaseText :text="ringName" size="xs" color="neutral" weight="bold" class="truncate uppercase" />
9
- </div>
10
-
11
- <div class="relative w-40 text-left" :class="{ 'opacity-70': !isSelectable }">
12
- <TableauFencerCard
13
- :fencer="person1Display"
14
- :score="score1"
15
- :position="dePosition1"
16
- :color="hostingClubColors.Color1"
17
- :scoreColor="getScoreColor('person1')"
18
- :showScore="showScore1"
19
- :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
20
- corners="top"
21
- />
22
-
23
- <div class="relative h-px bg-dropdownSelect">
24
- <span
25
- :ref="element => registerAnchor('input', element)"
26
- class="pointer-events-none absolute left-0 top-1/2 h-px w-px -translate-y-1/2 opacity-0"
27
- ></span>
28
- <span
29
- :ref="element => registerAnchor('output', element)"
30
- class="pointer-events-none absolute right-0 top-1/2 h-px w-px -translate-y-1/2 opacity-0"
31
- ></span>
32
- </div>
33
-
34
- <TableauFencerCard
35
- :fencer="person2Display"
36
- :score="score2"
37
- :position="dePosition2"
38
- :color="hostingClubColors.Color2"
39
- :scoreColor="getScoreColor('person2')"
40
- :showScore="showScore2"
41
- :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
42
- corners="bottom"
43
- />
44
- <button
45
- type="button"
46
- data-testid="tableau-bout"
47
- class="absolute inset-0 z-10 w-40 rounded text-left shadow-md outline-none ring-1 ring-transparent transition hover:shadow-lg hover:ring-2 hover:ring-bright focus-visible:ring-2 focus-visible:ring-bright disabled:cursor-default"
48
- :disabled="!isSelectable"
49
- :aria-label="boutLabel"
50
- @click="handleBoutClick"
51
- ></button>
52
- </div>
53
-
54
- <div
55
- v-if="showActiveChrome"
56
- data-testid="active-time"
57
- class="pointer-events-none absolute left-full top-1/2 z-20 flex h-[57px] w-10 -translate-y-1/2 items-center justify-center rounded-r-md bg-primary px-1"
58
- >
59
- <BaseText :text="displayTime" size="xs" color="neutral" weight="bold" class="font-sourceCodePro" />
60
- </div>
61
- </section>
62
- </template>
63
-
64
- <script>
65
- import BaseText from '../../../Atoms/Text/BaseText.vue';
66
- import TableauFencerCard from '../TableauFencerCard/TableauFencerCard.vue';
67
-
68
- const EMPTY_FENCER = { DisplayName: '', Club: { Name: '' }, EventPersons: [] };
69
-
70
- const getFencerName = fencer => String(fencer?.DisplayName || '');
71
- const isByeName = fencer => getFencerName(fencer).toUpperCase() === 'BYE';
72
- const isMissingFencer = fencer => !fencer?.PersonId || Number(fencer.PersonId) === 0;
73
- const isUnavailableFencer = fencer => isMissingFencer(fencer) || isByeName(fencer);
74
- const isCompletedBout = bout => String(bout?.Status || '').toLowerCase() === 'completed';
75
- const getNumericScore = score => Number(score ?? 0);
76
- const normalizeSeconds = value => Math.max(0, Number(value) || 0);
77
- const formatSeconds = value => {
78
- const seconds = normalizeSeconds(value);
79
- return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
80
- };
81
- const isRunningActiveBout = bout =>
82
- String(bout?.Status || '').toLowerCase() === 'active' && String(bout?.TimerStatus || '').toLowerCase() === 'running';
83
-
84
- const scoreColorRules = {
85
- person1: [
86
- { matches: bout => !isByeName(bout.Person1) && isByeName(bout.Person2), color: 'bg-winGreen' },
87
- { matches: bout => getNumericScore(bout.Score1) > getNumericScore(bout.Score2), color: 'bg-winGreen' },
88
- {
89
- matches: bout =>
90
- getNumericScore(bout.Score1) < getNumericScore(bout.Score2) && String(bout.Status).toLowerCase() === 'active',
91
- color: 'bg-lossRed'
92
- }
93
- ],
94
- person2: [
95
- { matches: bout => !isByeName(bout.Person2) && isByeName(bout.Person1), color: 'bg-winGreen' },
96
- { matches: bout => getNumericScore(bout.Score2) > getNumericScore(bout.Score1), color: 'bg-winGreen' },
97
- {
98
- matches: bout =>
99
- getNumericScore(bout.Score2) < getNumericScore(bout.Score1) && String(bout.Status).toLowerCase() === 'active',
100
- color: 'bg-lossRed'
101
- }
102
- ]
103
- };
104
-
105
- const getMatchingScoreColor = (bout, fencerKey) =>
106
- scoreColorRules[fencerKey].find(rule => rule.matches(bout))?.color || 'bg-dropdownSelect';
107
-
108
- const getDisplayFencer = (bout, fencer) => {
109
- if (!fencer) return EMPTY_FENCER;
110
- if (String(bout.Status).toLowerCase() !== 'scheduled' || !isByeName(fencer)) return fencer;
111
- return { ...fencer, DisplayName: '', Club: { Name: '' } };
112
- };
113
-
114
- export default {
115
- name: 'TableauBoutCard',
116
- components: { BaseText, TableauFencerCard },
117
- emits: ['action:directingBout', 'action:editBout', 'register-node-anchor'],
118
- props: {
119
- bout: {
120
- type: Object,
121
- required: true
122
- },
123
- hostingClubColors: {
124
- type: Object,
125
- required: true
126
- },
127
- roundName: {
128
- type: String,
129
- required: true
130
- },
131
- bracketSize: {
132
- type: String,
133
- required: true
134
- },
135
- isCountingBackwardsMaxScore: {
136
- type: Number,
137
- default: 0
138
- }
139
- },
140
- data() {
141
- return {
142
- remainingTime: normalizeSeconds(this.bout.TimeLeft),
143
- timerInterval: null
144
- };
145
- },
146
- computed: {
147
- boutNodeId() {
148
- return this.bout.DEBoutId ?? this.bout.BoutId;
149
- },
150
- boutLabel() {
151
- return `${this.person1Display.DisplayName || 'Unassigned'} versus ${
152
- this.person2Display.DisplayName || 'Unassigned'
153
- }`;
154
- },
155
- dePosition1() {
156
- return this.bout.Person1?.EventPersons?.[0]?.DEPosition ?? '';
157
- },
158
- dePosition2() {
159
- return this.bout.Person2?.EventPersons?.[0]?.DEPosition ?? '';
160
- },
161
- displayTime() {
162
- return formatSeconds(this.remainingTime);
163
- },
164
- isActiveBout() {
165
- return String(this.bout.Status || '').toLowerCase() === 'active';
166
- },
167
- isDirectable() {
168
- return !isUnavailableFencer(this.bout.Person1) && !isUnavailableFencer(this.bout.Person2);
169
- },
170
- isSelectable() {
171
- return isCompletedBout(this.bout) || this.isDirectable;
172
- },
173
- person1Display() {
174
- return getDisplayFencer(this.bout, this.bout.Person1);
175
- },
176
- person2Display() {
177
- return getDisplayFencer(this.bout, this.bout.Person2);
178
- },
179
- ringName() {
180
- return String(this.bout.RingName || '');
181
- },
182
- score1() {
183
- return String(this.bout.Score1 ?? 0);
184
- },
185
- score2() {
186
- return String(this.bout.Score2 ?? 0);
187
- },
188
- showBout() {
189
- if (this.roundName !== 'Grand Final') return true;
190
- return !isUnavailableFencer(this.bout.Person1) && !isUnavailableFencer(this.bout.Person2);
191
- },
192
- showActiveChrome() {
193
- return this.isActiveBout && (this.ringName.length > 0 || this.remainingTime > 0 || this.shouldTickTimer);
194
- },
195
- showScore1() {
196
- return !isUnavailableFencer(this.bout.Person1);
197
- },
198
- showScore2() {
199
- return !isUnavailableFencer(this.bout.Person2);
200
- },
201
- showRingLabel() {
202
- return this.isActiveBout && this.ringName.length > 0;
203
- },
204
- shouldTickTimer() {
205
- return isRunningActiveBout(this.bout) && this.remainingTime > 0;
206
- }
207
- },
208
- watch: {
209
- 'bout.TimeLeft'(value) {
210
- this.remainingTime = normalizeSeconds(value);
211
- this.restartTimer();
212
- },
213
- 'bout.TimerStatus'() {
214
- this.restartTimer();
215
- },
216
- 'bout.Status'() {
217
- this.restartTimer();
218
- }
219
- },
220
- mounted() {
221
- this.restartTimer();
222
- },
223
- beforeUnmount() {
224
- this.stopTimer();
225
- },
226
- methods: {
227
- getScoreColor(fencerKey) {
228
- return getMatchingScoreColor(this.bout, fencerKey);
229
- },
230
- handleBoutClick() {
231
- if (isCompletedBout(this.bout)) {
232
- this.$emit('action:editBout', this.bout);
233
- return;
234
- }
235
- if (this.isDirectable) this.$emit('action:directingBout', this.bout);
236
- },
237
- restartTimer() {
238
- this.stopTimer();
239
- if (!this.shouldTickTimer) return;
240
- this.timerInterval = setInterval(this.tickTimer, 1000);
241
- },
242
- registerAnchor(kind, element) {
243
- if (this.boutNodeId === null || this.boutNodeId === undefined) return;
244
- this.$emit('register-node-anchor', { id: this.boutNodeId, kind, el: element });
245
- },
246
- stopTimer() {
247
- if (this.timerInterval) clearInterval(this.timerInterval);
248
- this.timerInterval = null;
249
- },
250
- tickTimer() {
251
- this.remainingTime = Math.max(0, this.remainingTime - 1);
252
- if (this.remainingTime === 0) this.stopTimer();
253
- }
254
- }
255
- };
256
- </script>
1
+ <template>
2
+ <HoverActions
3
+ v-if="showBout"
4
+ v-slot="{ isOpen }"
5
+ class="relative w-40"
6
+ :actions="boutActions"
7
+ :focusable="!isSelectable"
8
+ @action="handleBoutAction"
9
+ >
10
+ <div
11
+ v-if="showRingLabel"
12
+ data-testid="active-ring"
13
+ class="pointer-events-none absolute -top-4 left-0 z-20 flex h-4 w-40 items-center rounded-t bg-primary px-2"
14
+ >
15
+ <BaseText :text="ringName" size="xs" color="neutral" weight="bold" class="truncate uppercase" />
16
+ </div>
17
+
18
+ <div class="relative w-40 text-left" :class="{ 'opacity-70': !isSelectable }">
19
+ <TableauFencerCard
20
+ :fencer="person1Display"
21
+ :score="score1"
22
+ :position="dePosition1"
23
+ :color="hostingClubColors.Color1"
24
+ :scoreColor="getScoreColor('person1')"
25
+ :showScore="showScore1"
26
+ :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
27
+ corners="top"
28
+ />
29
+
30
+ <div class="relative h-px bg-dropdownSelect">
31
+ <span
32
+ :ref="element => registerAnchor('input', element)"
33
+ class="pointer-events-none absolute left-0 top-1/2 h-px w-px -translate-y-1/2 opacity-0"
34
+ ></span>
35
+ <span
36
+ :ref="element => registerAnchor('output', element)"
37
+ class="pointer-events-none absolute right-0 top-1/2 h-px w-px -translate-y-1/2 opacity-0"
38
+ ></span>
39
+ </div>
40
+
41
+ <TableauFencerCard
42
+ :fencer="person2Display"
43
+ :score="score2"
44
+ :position="dePosition2"
45
+ :color="hostingClubColors.Color2"
46
+ :scoreColor="getScoreColor('person2')"
47
+ :showScore="showScore2"
48
+ :isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
49
+ corners="bottom"
50
+ />
51
+ <button
52
+ type="button"
53
+ data-testid="tableau-bout"
54
+ class="absolute inset-0 z-10 w-40 rounded text-left shadow-md outline-none ring-1 ring-transparent transition hover:shadow-lg hover:ring-2 hover:ring-bright focus-visible:ring-2 focus-visible:ring-bright disabled:cursor-default"
55
+ :disabled="!isSelectable"
56
+ :aria-label="boutLabel"
57
+ :aria-haspopup="boutActions.length ? 'menu' : undefined"
58
+ :aria-expanded="boutActions.length ? isOpen : undefined"
59
+ @click="handleBoutClick"
60
+ ></button>
61
+ </div>
62
+
63
+ <div
64
+ v-if="showActiveChrome"
65
+ data-testid="active-time"
66
+ class="pointer-events-none absolute left-full top-1/2 z-20 flex h-[57px] w-10 -translate-y-1/2 items-center justify-center rounded-r-md bg-primary px-1"
67
+ >
68
+ <BaseText :text="displayTime" size="xs" color="neutral" weight="bold" class="font-sourceCodePro" />
69
+ </div>
70
+ </HoverActions>
71
+ </template>
72
+
73
+ <script>
74
+ import BaseText from '../../../Atoms/Text/BaseText.vue';
75
+ import HoverActions from '../../../Molecules/Menus/HoverActions/HoverActions.vue';
76
+ import { canAssignBoutDirector, createBoutActions } from '../../../Util/boutDirectorAssignment.js';
77
+ import TableauFencerCard from '../TableauFencerCard/TableauFencerCard.vue';
78
+
79
+ const EMPTY_FENCER = { DisplayName: '', Club: { Name: '' }, EventPersons: [] };
80
+
81
+ const getFencerName = fencer => String(fencer?.DisplayName || '');
82
+ const isByeName = fencer => getFencerName(fencer).toUpperCase() === 'BYE';
83
+ const isMissingFencer = fencer => !fencer?.PersonId || Number(fencer.PersonId) === 0;
84
+ const isUnavailableFencer = fencer => isMissingFencer(fencer) || isByeName(fencer);
85
+ const isCompletedBout = bout => String(bout?.Status || '').toLowerCase() === 'completed';
86
+ const getNumericScore = score => Number(score ?? 0);
87
+ const normalizeSeconds = value => Math.max(0, Number(value) || 0);
88
+ const formatSeconds = value => {
89
+ const seconds = normalizeSeconds(value);
90
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
91
+ };
92
+ const isRunningActiveBout = bout =>
93
+ String(bout?.Status || '').toLowerCase() === 'active' && String(bout?.TimerStatus || '').toLowerCase() === 'running';
94
+
95
+ const scoreColorRules = {
96
+ person1: [
97
+ { matches: bout => !isByeName(bout.Person1) && isByeName(bout.Person2), color: 'bg-winGreen' },
98
+ { matches: bout => getNumericScore(bout.Score1) > getNumericScore(bout.Score2), color: 'bg-winGreen' },
99
+ {
100
+ matches: bout =>
101
+ getNumericScore(bout.Score1) < getNumericScore(bout.Score2) && String(bout.Status).toLowerCase() === 'active',
102
+ color: 'bg-lossRed'
103
+ }
104
+ ],
105
+ person2: [
106
+ { matches: bout => !isByeName(bout.Person2) && isByeName(bout.Person1), color: 'bg-winGreen' },
107
+ { matches: bout => getNumericScore(bout.Score2) > getNumericScore(bout.Score1), color: 'bg-winGreen' },
108
+ {
109
+ matches: bout =>
110
+ getNumericScore(bout.Score2) < getNumericScore(bout.Score1) && String(bout.Status).toLowerCase() === 'active',
111
+ color: 'bg-lossRed'
112
+ }
113
+ ]
114
+ };
115
+
116
+ const getMatchingScoreColor = (bout, fencerKey) =>
117
+ scoreColorRules[fencerKey].find(rule => rule.matches(bout))?.color || 'bg-dropdownSelect';
118
+
119
+ const getDisplayFencer = (bout, fencer) => {
120
+ if (!fencer) return EMPTY_FENCER;
121
+ if (String(bout.Status).toLowerCase() !== 'scheduled' || !isByeName(fencer)) return fencer;
122
+ return { ...fencer, DisplayName: '', Club: { Name: '' } };
123
+ };
124
+
125
+ export default {
126
+ name: 'TableauBoutCard',
127
+ components: { BaseText, TableauFencerCard, HoverActions },
128
+ emits: ['action:directingBout', 'action:editBout', 'register-node-anchor', 'request:bout-director'],
129
+ props: {
130
+ showBoutDirectorAssignment: { type: Boolean, default: true },
131
+ plannedDirectorName: { type: String, default: '' },
132
+ plannedDirectorColor: { type: String, default: '' },
133
+ bout: {
134
+ type: Object,
135
+ required: true
136
+ },
137
+ hostingClubColors: {
138
+ type: Object,
139
+ required: true
140
+ },
141
+ roundName: {
142
+ type: String,
143
+ required: true
144
+ },
145
+ bracketSize: {
146
+ type: String,
147
+ required: true
148
+ },
149
+ isCountingBackwardsMaxScore: {
150
+ type: Number,
151
+ default: 0
152
+ }
153
+ },
154
+ data() {
155
+ return {
156
+ remainingTime: normalizeSeconds(this.bout.TimeLeft),
157
+ timerInterval: null
158
+ };
159
+ },
160
+ computed: {
161
+ boutForActions() {
162
+ return { ...this.bout, RoundLabel: this.bout.RoundLabel || this.roundName };
163
+ },
164
+ boutActions() {
165
+ return createBoutActions(
166
+ this.boutForActions,
167
+ this.showBoutDirectorAssignment,
168
+ this.plannedDirectorName,
169
+ this.plannedDirectorColor
170
+ );
171
+ },
172
+ boutNodeId() {
173
+ return this.bout.DEBoutId ?? this.bout.BoutId;
174
+ },
175
+ boutLabel() {
176
+ return `${this.person1Display.DisplayName || 'Unassigned'} versus ${
177
+ this.person2Display.DisplayName || 'Unassigned'
178
+ }`;
179
+ },
180
+ dePosition1() {
181
+ return this.bout.Person1?.EventPersons?.[0]?.DEPosition ?? '';
182
+ },
183
+ dePosition2() {
184
+ return this.bout.Person2?.EventPersons?.[0]?.DEPosition ?? '';
185
+ },
186
+ displayTime() {
187
+ return formatSeconds(this.remainingTime);
188
+ },
189
+ isActiveBout() {
190
+ return String(this.bout.Status || '').toLowerCase() === 'active';
191
+ },
192
+ isDirectable() {
193
+ return !isUnavailableFencer(this.bout.Person1) && !isUnavailableFencer(this.bout.Person2);
194
+ },
195
+ isSelectable() {
196
+ return isCompletedBout(this.bout) || this.isDirectable;
197
+ },
198
+ person1Display() {
199
+ return getDisplayFencer(this.bout, this.bout.Person1);
200
+ },
201
+ person2Display() {
202
+ return getDisplayFencer(this.bout, this.bout.Person2);
203
+ },
204
+ ringName() {
205
+ return String(this.bout.RingName || '');
206
+ },
207
+ score1() {
208
+ return String(this.bout.Score1 ?? 0);
209
+ },
210
+ score2() {
211
+ return String(this.bout.Score2 ?? 0);
212
+ },
213
+ showBout() {
214
+ if (this.roundName !== 'Grand Final') return true;
215
+ return !isUnavailableFencer(this.bout.Person1) && !isUnavailableFencer(this.bout.Person2);
216
+ },
217
+ showActiveChrome() {
218
+ return this.isActiveBout && (this.ringName.length > 0 || this.remainingTime > 0 || this.shouldTickTimer);
219
+ },
220
+ showScore1() {
221
+ return !isUnavailableFencer(this.bout.Person1);
222
+ },
223
+ showScore2() {
224
+ return !isUnavailableFencer(this.bout.Person2);
225
+ },
226
+ showRingLabel() {
227
+ return this.isActiveBout && this.ringName.length > 0;
228
+ },
229
+ shouldTickTimer() {
230
+ return isRunningActiveBout(this.bout) && this.remainingTime > 0;
231
+ }
232
+ },
233
+ watch: {
234
+ 'bout.TimeLeft'(value) {
235
+ this.remainingTime = normalizeSeconds(value);
236
+ this.restartTimer();
237
+ },
238
+ 'bout.TimerStatus'() {
239
+ this.restartTimer();
240
+ },
241
+ 'bout.Status'() {
242
+ this.restartTimer();
243
+ }
244
+ },
245
+ mounted() {
246
+ this.restartTimer();
247
+ },
248
+ beforeUnmount() {
249
+ this.stopTimer();
250
+ },
251
+ methods: {
252
+ handleBoutAction(actionId) {
253
+ const handlers = { open: this.handleBoutClick, 'assign-director': this.requestDirectorAssignment };
254
+ handlers[actionId]?.();
255
+ },
256
+ requestDirectorAssignment() {
257
+ if (!this.showBoutDirectorAssignment || !canAssignBoutDirector(this.boutForActions)) return;
258
+ this.$emit('request:bout-director', JSON.parse(JSON.stringify(this.bout)));
259
+ },
260
+ getScoreColor(fencerKey) {
261
+ return getMatchingScoreColor(this.bout, fencerKey);
262
+ },
263
+ handleBoutClick() {
264
+ if (isCompletedBout(this.bout)) {
265
+ this.$emit('action:editBout', this.bout);
266
+ return;
267
+ }
268
+ if (this.isDirectable) this.$emit('action:directingBout', this.bout);
269
+ },
270
+ restartTimer() {
271
+ this.stopTimer();
272
+ if (!this.shouldTickTimer) return;
273
+ this.timerInterval = setInterval(this.tickTimer, 1000);
274
+ },
275
+ registerAnchor(kind, element) {
276
+ if (this.boutNodeId === null || this.boutNodeId === undefined) return;
277
+ this.$emit('register-node-anchor', { id: this.boutNodeId, kind, el: element });
278
+ },
279
+ stopTimer() {
280
+ if (this.timerInterval) clearInterval(this.timerInterval);
281
+ this.timerInterval = null;
282
+ },
283
+ tickTimer() {
284
+ this.remainingTime = Math.max(0, this.remainingTime - 1);
285
+ if (this.remainingTime === 0) this.stopTimer();
286
+ }
287
+ }
288
+ };
289
+ </script>