@dcrackel/hematournamentui 1.0.711 → 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 (109) hide show
  1. package/.prettierignore +7 -7
  2. package/.storybook/main.js +3 -1
  3. package/README.md +182 -148
  4. package/dist/HemaTournamentUI-lib.es.js +4150 -4039
  5. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  6. package/dist/hematournamentui-tailwind.css +11 -0
  7. package/package.json +96 -96
  8. package/prettier.config.js +16 -16
  9. package/src/main.js +1 -1
  10. package/src/mocks/getDEBouts-TableOf128.js +37 -37
  11. package/src/mocks/getDEBouts-TableOf64.js +35 -35
  12. package/src/stories/Atoms/Input/BaseInput.test.js +97 -97
  13. package/src/stories/Atoms/Input/BaseInput.vue +149 -149
  14. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -23
  15. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -35
  16. package/src/stories/Molecules/Indicators/RegistrationStatus/RegistrationStatus.vue +30 -0
  17. package/src/stories/Molecules/Menus/HoverActions/HoverActions.test.js +138 -0
  18. package/src/stories/Molecules/Menus/HoverActions/HoverActions.vue +212 -0
  19. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.stories.js +6 -0
  20. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -18
  21. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +1 -1
  22. package/src/stories/Organisms/Cards/BoutCard/BoutCard.vue +5 -1
  23. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardActive/BoutCardActive.vue +19 -10
  24. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardInactive/BoutCardInactive.vue +11 -7
  25. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +33 -33
  26. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +45 -45
  27. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +103 -103
  28. package/src/stories/Organisms/Cards/Director/DirectorCard.test.js +82 -0
  29. package/src/stories/Organisms/Cards/Director/DirectorCard.vue +32 -51
  30. package/src/stories/Organisms/Cards/EditBout/EditBout.test.js +51 -51
  31. package/src/stories/Organisms/Cards/EventCard/EventCard.stories.js +41 -1
  32. package/src/stories/Organisms/Cards/EventCard/EventCard.test.js +106 -0
  33. package/src/stories/Organisms/Cards/EventCard/EventCard.vue +62 -30
  34. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -298
  35. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +77 -60
  36. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +144 -143
  37. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +289 -256
  38. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -69
  39. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -80
  40. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +153 -153
  41. package/src/stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue +4 -0
  42. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -31
  43. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -109
  44. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -192
  45. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +1 -0
  46. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +247 -204
  47. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +89 -15
  48. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +29 -29
  49. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +46 -46
  50. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -21
  51. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -39
  52. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -145
  53. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -64
  54. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +36 -57
  55. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +101 -65
  56. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +14 -1
  57. package/src/stories/Organisms/Headers/AttendanceSummary/AttendanceSummary.vue +12 -5
  58. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +52 -48
  59. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +256 -227
  60. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -226
  61. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  62. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +137 -133
  63. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.test.js +221 -191
  64. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.vue +372 -366
  65. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -172
  66. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -216
  67. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -79
  68. package/src/stories/Organisms/Wizards/EditEventWizard/RuleSerialization.test.js +29 -0
  69. package/src/stories/Organisms/Wizards/EditEventWizard/WeaponSelection.test.js +138 -0
  70. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +123 -12
  71. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +178 -166
  72. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +35 -7
  73. package/src/stories/Templates/EventManagement/Bracket/BracketDirectorAssignment.test.js +242 -0
  74. package/src/stories/Templates/EventManagement/Bracket/BracketMobile.test.js +131 -0
  75. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +42 -39
  76. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +80 -0
  77. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.js +93 -0
  78. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.test.js +152 -0
  79. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -39
  80. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -79
  81. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -93
  82. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -91
  83. package/src/stories/Templates/EventManagement/EventAttendance/EventAttendance.stories.js +31 -1
  84. package/src/stories/Templates/EventManagement/PoolLive/PoolLive.test.js +121 -121
  85. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.stories.js +9 -2
  86. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue +5 -4
  87. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +81 -81
  88. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -39
  89. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +101 -101
  90. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.stories.js +71 -71
  91. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue +86 -86
  92. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.stories.js +86 -86
  93. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.vue +247 -247
  94. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -170
  95. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -156
  96. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -329
  97. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -329
  98. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -42
  99. package/src/stories/Templates/TournamentManagement/ListEvents/ListEvents.stories.js +58 -45
  100. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -84
  101. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +114 -114
  102. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -363
  103. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -116
  104. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +307 -307
  105. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -45
  106. package/src/stories/Util/Validations/PriceValidation.js +6 -6
  107. package/src/stories/Util/boutDirectorAssignment.js +46 -0
  108. package/src/stories/Util/registrationStatus.js +11 -0
  109. 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>