@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
@@ -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>