@dcrackel/hematournamentui 1.0.707 → 1.0.708
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.
- package/AGENTS.md +118 -118
- package/dist/HemaTournamentUI-lib.es.js +4483 -4618
- package/dist/HemaTournamentUI-lib.umd.js +27 -27
- package/dist/hematournamentui-tailwind.css +185 -406
- package/package.json +10 -10
- package/src/index.js +2 -0
- package/src/mocks/getDEBouts-42Fencers.js +4 -4
- package/src/mocks/getDEBouts-TableOf128.js +37 -0
- package/src/mocks/getDEBouts-TableOf64.js +35 -0
- package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -0
- package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -0
- package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -0
- package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +3 -2
- package/src/stories/Molecules/Modals/EditBoutModal/EditBoutModal.vue +1 -1
- package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +33 -0
- package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +45 -0
- package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +103 -0
- package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -278
- package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +60 -98
- package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +103 -4
- package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +256 -215
- package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -92
- package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -128
- package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +14 -4
- package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +2 -2
- package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +131 -3
- package/src/stories/Organisms/Containers/Tableau/Tableau.vue +287 -89
- package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +29 -0
- package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +46 -0
- package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -0
- package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -0
- package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -0
- package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -0
- package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +57 -0
- package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +65 -0
- package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +31 -29
- package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +146 -18
- package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +52 -2
- package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +53 -17
- package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +39 -0
- package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -0
- package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -0
- package/src/stories/Organisms/Cards/TableauBoutCard/SpacingConfig.js +0 -111
- package/src/stories/Organisms/Containers/TableauLines/SpacingConfig.js +0 -162
- package/src/stories/Organisms/Containers/TableauLines/TableauLines.stories.js +0 -21
- package/src/stories/Organisms/Containers/TableauLines/TableauLines.vue +0 -78
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section v-if="listView" class="flex flex-col items-center">
|
|
3
|
-
<div v-for="
|
|
4
|
-
<BaseText class="my-5" color="primaryHighlight" size="sm" :text="
|
|
3
|
+
<div v-for="section in listSections" :key="section.key" class="my-4 w-full flex-col md:w-3/4">
|
|
4
|
+
<BaseText class="my-5" color="primaryHighlight" size="sm" :text="section.label" weight="bold" />
|
|
5
5
|
|
|
6
|
-
<div v-for="bout in bouts" :key="bout.BoutId" class="mb-4">
|
|
6
|
+
<div v-for="bout in section.bouts" :key="bout.BoutId" class="mb-4">
|
|
7
|
+
<div
|
|
8
|
+
v-if="getPlannedStation(bout)"
|
|
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"
|
|
11
|
+
:style="{ borderLeftColor: getPlannedStation(bout).Color }"
|
|
12
|
+
>
|
|
13
|
+
<span
|
|
14
|
+
class="h-2.5 w-2.5 flex-none rounded-full"
|
|
15
|
+
:style="{ backgroundColor: getPlannedStation(bout).Color }"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
></span>
|
|
18
|
+
<BaseText :text="getPlannedStationLabel(bout)" color="quaternary" size="xs" />
|
|
19
|
+
</div>
|
|
7
20
|
<BoutCard
|
|
8
21
|
:bout="bout"
|
|
9
22
|
:hostingClubColors="hostingClubColors"
|
|
@@ -17,165 +30,350 @@
|
|
|
17
30
|
</div>
|
|
18
31
|
</section>
|
|
19
32
|
|
|
20
|
-
<section
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
<section
|
|
34
|
+
v-else
|
|
35
|
+
ref="scrollElement"
|
|
36
|
+
class="mt-4 w-full overflow-x-auto scrollbar-thin"
|
|
37
|
+
@scroll.passive="scheduleConnectorUpdate"
|
|
38
|
+
>
|
|
39
|
+
<div ref="tableauRoot" class="relative min-w-max pb-6">
|
|
40
|
+
<div class="inline-flex items-start">
|
|
23
41
|
<TableauColumn
|
|
24
|
-
|
|
42
|
+
v-for="([roundName, roundBouts], roundIndex) in winnerRoundEntries"
|
|
43
|
+
:key="`winner-${roundName}`"
|
|
44
|
+
:bouts="roundBouts"
|
|
25
45
|
:hostingClubColors="hostingClubColors"
|
|
26
46
|
:roundName="roundName"
|
|
27
47
|
:bracketSize="bracketSize"
|
|
28
|
-
:
|
|
48
|
+
:layout="getRoundLayout(roundIndex)"
|
|
29
49
|
:isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
|
|
50
|
+
@register-node-anchor="registerNodeAnchor"
|
|
30
51
|
@action:editBout="handleEditBout"
|
|
31
52
|
@action:directingBout="handleDirectorBout"
|
|
32
53
|
/>
|
|
33
54
|
</div>
|
|
34
|
-
</div>
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
<div class="min-w-full flex">
|
|
38
|
-
<div v-for="(bouts, roundName) in groupedBouts.loserBouts" :key="roundName">
|
|
56
|
+
<div v-if="placementRoundEntries.length" class="mt-10 flex justify-end pr-10">
|
|
39
57
|
<TableauColumn
|
|
40
|
-
|
|
58
|
+
v-for="[roundName, roundBouts] in placementRoundEntries"
|
|
59
|
+
:key="`placement-${roundName}`"
|
|
60
|
+
:bouts="roundBouts"
|
|
41
61
|
:hostingClubColors="hostingClubColors"
|
|
42
62
|
:roundName="roundName"
|
|
43
63
|
:bracketSize="bracketSize"
|
|
44
|
-
:
|
|
64
|
+
:layout="placementLayout"
|
|
45
65
|
:isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
|
|
66
|
+
@register-node-anchor="registerNodeAnchor"
|
|
46
67
|
@action:editBout="handleEditBout"
|
|
47
68
|
@action:directingBout="handleDirectorBout"
|
|
48
69
|
/>
|
|
49
70
|
</div>
|
|
71
|
+
|
|
72
|
+
<div v-if="loserRoundEntries.length" class="mt-12 border-t border-dropdownSelect pt-6">
|
|
73
|
+
<BaseText class="mb-4" color="quaternary" size="sm" text="Consolation" weight="bold" />
|
|
74
|
+
<div class="inline-flex items-start">
|
|
75
|
+
<TableauColumn
|
|
76
|
+
v-for="([roundName, roundBouts], roundIndex) in loserRoundEntries"
|
|
77
|
+
:key="`loser-${roundName}`"
|
|
78
|
+
:bouts="roundBouts"
|
|
79
|
+
:hostingClubColors="hostingClubColors"
|
|
80
|
+
:roundName="roundName"
|
|
81
|
+
:bracketSize="bracketSize"
|
|
82
|
+
:layout="getRoundLayout(roundIndex)"
|
|
83
|
+
:isCountingBackwardsMaxScore="isCountingBackwardsMaxScore"
|
|
84
|
+
@register-node-anchor="registerNodeAnchor"
|
|
85
|
+
@action:editBout="handleEditBout"
|
|
86
|
+
@action:directingBout="handleDirectorBout"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<TableauSvgOverlay :size="svgSize" :paths="connectorPaths" />
|
|
50
92
|
</div>
|
|
51
93
|
</section>
|
|
52
94
|
</template>
|
|
53
95
|
|
|
54
96
|
<script>
|
|
55
|
-
import TableauColumn from '../TableauColumn/TableauColumn.vue';
|
|
56
|
-
import TableauBoutCard from '../../Cards/TableauBoutCard/TableauBoutCard.vue';
|
|
57
|
-
import BoutCard from '../../../Organisms/Cards/BoutCard/BoutCard.vue';
|
|
58
97
|
import BaseText from '../../../Atoms/Text/BaseText.vue';
|
|
98
|
+
import BoutCard from '../../../Organisms/Cards/BoutCard/BoutCard.vue';
|
|
99
|
+
import TableauColumn from '../TableauColumn/TableauColumn.vue';
|
|
100
|
+
import TableauSvgOverlay from './components/TableauSvgOverlay.vue';
|
|
101
|
+
import { buildBracketPaths, createDomAnchorLayout } from './layout/TableauLayoutEngine';
|
|
102
|
+
import {
|
|
103
|
+
addPlannedStrokesToConnectors,
|
|
104
|
+
createPlannedStationByBout,
|
|
105
|
+
getPlannedStationForBout,
|
|
106
|
+
isBoutSuggestedForViewer
|
|
107
|
+
} from './plannedAssignments';
|
|
59
108
|
|
|
60
|
-
const
|
|
61
|
-
const
|
|
109
|
+
const MATCH_HEIGHT_PX = 57;
|
|
110
|
+
const BASE_GAP_PX = 16;
|
|
111
|
+
const BASE_PITCH_PX = MATCH_HEIGHT_PX + BASE_GAP_PX;
|
|
112
|
+
const PLACEMENT_ROUND_NAMES = new Set(['Third Place', 'Third Place Match']);
|
|
113
|
+
const ROUND_ORDER = [
|
|
114
|
+
'Table of 256',
|
|
115
|
+
'Table of 128',
|
|
116
|
+
'Table of 64',
|
|
117
|
+
'Table of 32',
|
|
118
|
+
'Table of 16',
|
|
119
|
+
'Table of 8',
|
|
120
|
+
'Semifinal',
|
|
121
|
+
'Final',
|
|
122
|
+
'Grand Final'
|
|
123
|
+
];
|
|
62
124
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
'Third Place Match': 2,
|
|
66
|
-
Semifinal: 3
|
|
125
|
+
const roundAliases = {
|
|
126
|
+
Semifinals: 'Semifinal'
|
|
67
127
|
};
|
|
68
128
|
|
|
69
|
-
const
|
|
129
|
+
const normalizeRoundName = roundName => roundAliases[roundName] || roundName;
|
|
130
|
+
const isConsolationRoundName = roundName => roundName.includes('Consolation');
|
|
131
|
+
const isPlacementRoundName = roundName => PLACEMENT_ROUND_NAMES.has(roundName);
|
|
132
|
+
const getRoundNumber = roundName => Number.parseInt(roundName.match(/\d+/)?.[0] || '0', 10);
|
|
133
|
+
const getBoutNodeId = bout => bout.DEBoutId ?? bout.BoutId;
|
|
134
|
+
|
|
135
|
+
const getRoundOrder = roundName => {
|
|
136
|
+
const mainRoundIndex = ROUND_ORDER.indexOf(normalizeRoundName(roundName));
|
|
137
|
+
if (mainRoundIndex >= 0) return mainRoundIndex;
|
|
138
|
+
if (isConsolationRoundName(roundName)) return ROUND_ORDER.length + getRoundNumber(roundName);
|
|
139
|
+
return ROUND_ORDER.length * 2;
|
|
140
|
+
};
|
|
70
141
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const priorityB = roundPriority[roundNameB] || 0;
|
|
142
|
+
const sortRoundEntries = ([roundNameA], [roundNameB]) => getRoundOrder(roundNameA) - getRoundOrder(roundNameB);
|
|
143
|
+
const sortBoutsByNodeId = (boutA, boutB) => Number(getBoutNodeId(boutA)) - Number(getBoutNodeId(boutB));
|
|
74
144
|
|
|
75
|
-
|
|
76
|
-
|
|
145
|
+
const addBoutToRound = (rounds, bout) => {
|
|
146
|
+
const roundName = bout.RoundLabel || 'Unknown Round';
|
|
147
|
+
return { ...rounds, [roundName]: [...(rounds[roundName] || []), bout] };
|
|
77
148
|
};
|
|
78
149
|
|
|
79
|
-
const
|
|
150
|
+
const groupBoutsByRound = bouts => (bouts || []).reduce(addBoutToRound, {});
|
|
151
|
+
|
|
152
|
+
const isVisibleListBout = bout =>
|
|
80
153
|
bout.Person1?.DisplayName !== 'BYE' && bout.Person2?.DisplayName !== 'BYE' && bout.Status !== 'Completed';
|
|
81
154
|
|
|
82
|
-
const createVisibleRoundEntry = ([roundName, bouts]) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
155
|
+
const createVisibleRoundEntry = ([roundName, bouts]) => [
|
|
156
|
+
roundName,
|
|
157
|
+
bouts.filter(isVisibleListBout).sort(sortBoutsByNodeId)
|
|
158
|
+
];
|
|
86
159
|
|
|
87
|
-
const
|
|
160
|
+
const hasBouts = ([, bouts]) => bouts.length > 0;
|
|
88
161
|
|
|
89
|
-
const
|
|
90
|
-
const roundName = bout.RoundLabel || 'Unknown Round';
|
|
91
|
-
const roundBouts = groupedBouts[roundName] || [];
|
|
162
|
+
const createListSection = ([label, bouts]) => ({ key: label, label, bouts });
|
|
92
163
|
|
|
164
|
+
const removeSuggestedBouts = (entries, suggestedBoutIds) =>
|
|
165
|
+
entries
|
|
166
|
+
.map(([roundName, bouts]) => [roundName, bouts.filter(bout => !suggestedBoutIds.has(String(bout.BoutId)))])
|
|
167
|
+
.filter(hasBouts);
|
|
168
|
+
|
|
169
|
+
const createRoundLayout = depth => {
|
|
170
|
+
const roundMultiplier = 2 ** Math.max(depth, 0);
|
|
93
171
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
172
|
+
gapPx: BASE_PITCH_PX * roundMultiplier - MATCH_HEIGHT_PX,
|
|
173
|
+
padTopPx: (BASE_PITCH_PX * (roundMultiplier - 1)) / 2
|
|
96
174
|
};
|
|
97
175
|
};
|
|
98
176
|
|
|
99
177
|
export default {
|
|
100
178
|
name: 'Tableau',
|
|
101
|
-
components: { BaseText, BoutCard,
|
|
179
|
+
components: { BaseText, BoutCard, TableauColumn, TableauSvgOverlay },
|
|
102
180
|
emits: ['action:editBout', 'action:directingBout'],
|
|
103
181
|
props: {
|
|
104
182
|
bouts: { type: Array, required: true },
|
|
105
183
|
bracketSize: { type: String, required: true },
|
|
106
184
|
hostingClubColors: { type: Object, required: true },
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
185
|
+
listView: { type: Boolean, default: false },
|
|
186
|
+
isCountingBackwardsMaxScore: { type: Number, default: 0 },
|
|
187
|
+
plannedDirectingStations: { type: Array, default: () => [] },
|
|
188
|
+
plannedBoutAssignments: { type: Array, default: () => [] },
|
|
189
|
+
viewerPersonId: { type: [Number, String], default: null }
|
|
190
|
+
},
|
|
191
|
+
data() {
|
|
192
|
+
return {
|
|
193
|
+
connectorPaths: [],
|
|
194
|
+
inputAnchors: new Map(),
|
|
195
|
+
outputAnchors: new Map(),
|
|
196
|
+
resizeObserver: null,
|
|
197
|
+
scheduledFrame: null,
|
|
198
|
+
svgSize: { width: 0, height: 0 }
|
|
199
|
+
};
|
|
110
200
|
},
|
|
111
201
|
computed: {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return { winnerBouts, loserBouts };
|
|
202
|
+
roundEntries() {
|
|
203
|
+
return Object.entries(groupBoutsByRound(this.bouts)).map(([roundName, bouts]) => [
|
|
204
|
+
roundName,
|
|
205
|
+
[...bouts].sort(sortBoutsByNodeId)
|
|
206
|
+
]);
|
|
119
207
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
208
|
+
winnerRoundEntries() {
|
|
209
|
+
return this.roundEntries
|
|
210
|
+
.filter(([roundName]) => !isConsolationRoundName(roundName) && !isPlacementRoundName(roundName))
|
|
211
|
+
.sort(sortRoundEntries);
|
|
212
|
+
},
|
|
213
|
+
placementRoundEntries() {
|
|
214
|
+
return this.roundEntries.filter(([roundName]) => isPlacementRoundName(roundName)).sort(sortRoundEntries);
|
|
215
|
+
},
|
|
216
|
+
loserRoundEntries() {
|
|
217
|
+
return this.roundEntries.filter(([roundName]) => isConsolationRoundName(roundName)).sort(sortRoundEntries);
|
|
131
218
|
},
|
|
132
|
-
|
|
133
219
|
filteredBouts() {
|
|
134
220
|
return Object.fromEntries(
|
|
135
|
-
|
|
136
|
-
.sort(([roundNameA], [roundNameB]) => sortRoundNames(roundNameA, roundNameB))
|
|
137
|
-
.map(createVisibleRoundEntry)
|
|
138
|
-
.filter(hasVisibleBouts)
|
|
221
|
+
[...this.roundEntries].sort(sortRoundEntries).map(createVisibleRoundEntry).filter(hasBouts)
|
|
139
222
|
);
|
|
223
|
+
},
|
|
224
|
+
listSections() {
|
|
225
|
+
const entries = Object.entries(this.filteredBouts);
|
|
226
|
+
const suggestedBouts = entries
|
|
227
|
+
.flatMap(([, bouts]) => bouts)
|
|
228
|
+
.filter(bout =>
|
|
229
|
+
isBoutSuggestedForViewer({
|
|
230
|
+
bout,
|
|
231
|
+
plannedStationByBout: this.plannedStationByBout,
|
|
232
|
+
viewerPersonId: this.viewerPersonId
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (!suggestedBouts.length) return entries.map(createListSection);
|
|
237
|
+
|
|
238
|
+
const suggestedBoutIds = new Set(suggestedBouts.map(bout => String(bout.BoutId)));
|
|
239
|
+
const remainingSections = removeSuggestedBouts(entries, suggestedBoutIds).map(createListSection);
|
|
240
|
+
return [{ key: 'suggested-for-viewer', label: 'Suggested for You', bouts: suggestedBouts }, ...remainingSections];
|
|
241
|
+
},
|
|
242
|
+
plannedStationByBout() {
|
|
243
|
+
return createPlannedStationByBout({
|
|
244
|
+
assignments: this.plannedBoutAssignments,
|
|
245
|
+
stations: this.plannedDirectingStations
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
placementLayout() {
|
|
249
|
+
return createRoundLayout(0);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
watch: {
|
|
253
|
+
bouts: {
|
|
254
|
+
deep: true,
|
|
255
|
+
handler() {
|
|
256
|
+
this.scheduleConnectorUpdate();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
plannedDirectingStations: {
|
|
260
|
+
deep: true,
|
|
261
|
+
handler() {
|
|
262
|
+
this.scheduleConnectorUpdate();
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
plannedBoutAssignments: {
|
|
266
|
+
deep: true,
|
|
267
|
+
handler() {
|
|
268
|
+
this.scheduleConnectorUpdate();
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
bracketSize() {
|
|
272
|
+
this.scheduleConnectorUpdate();
|
|
273
|
+
},
|
|
274
|
+
listView(value) {
|
|
275
|
+
if (!value) this.$nextTick(this.observeTableauSize);
|
|
276
|
+
this.scheduleConnectorUpdate();
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
mounted() {
|
|
280
|
+
this.observeTableauSize();
|
|
281
|
+
window.addEventListener('resize', this.scheduleConnectorUpdate);
|
|
282
|
+
document.fonts?.ready?.then(this.scheduleConnectorUpdate);
|
|
283
|
+
this.scheduleConnectorUpdate();
|
|
284
|
+
},
|
|
285
|
+
beforeUnmount() {
|
|
286
|
+
this.resizeObserver?.disconnect();
|
|
287
|
+
window.removeEventListener('resize', this.scheduleConnectorUpdate);
|
|
288
|
+
if (this.scheduledFrame !== null && typeof cancelAnimationFrame === 'function') {
|
|
289
|
+
cancelAnimationFrame(this.scheduledFrame);
|
|
140
290
|
}
|
|
141
291
|
},
|
|
142
|
-
|
|
143
292
|
methods: {
|
|
144
|
-
|
|
145
|
-
_findBoutByIds(idsOrBout) {
|
|
146
|
-
// BoutCard emits ids for start/resume/edit; normalize to an actual bout object.
|
|
293
|
+
findBoutByIds(idsOrBout) {
|
|
147
294
|
if (!idsOrBout) return null;
|
|
148
|
-
if (idsOrBout.BoutId && idsOrBout.Person1) return idsOrBout;
|
|
295
|
+
if (idsOrBout.BoutId && idsOrBout.Person1) return idsOrBout;
|
|
149
296
|
|
|
150
|
-
const boutId = idsOrBout
|
|
151
|
-
|
|
152
|
-
return (this.bouts || []).find(b => String(b.BoutId) === String(boutId)) || null;
|
|
297
|
+
const boutId = idsOrBout.BoutId ?? idsOrBout.boutId;
|
|
298
|
+
return this.bouts.find(bout => String(bout.BoutId) === String(boutId)) || null;
|
|
153
299
|
},
|
|
154
|
-
|
|
155
|
-
// Start/Resume from BoutCard
|
|
156
300
|
handleDirectingFromIds(ids) {
|
|
157
|
-
const bout = this.
|
|
158
|
-
if (
|
|
159
|
-
this.$emit('action:directingBout', bout);
|
|
301
|
+
const bout = this.findBoutByIds(ids);
|
|
302
|
+
if (bout) this.$emit('action:directingBout', bout);
|
|
160
303
|
},
|
|
161
|
-
|
|
162
|
-
// Edit from BoutCard
|
|
163
304
|
handleEditFromIds(ids) {
|
|
164
|
-
const bout = this.
|
|
165
|
-
if (
|
|
166
|
-
this.$emit('action:editBout', bout);
|
|
305
|
+
const bout = this.findBoutByIds(ids);
|
|
306
|
+
if (bout) this.$emit('action:editBout', bout);
|
|
167
307
|
},
|
|
168
|
-
|
|
169
|
-
// TableauColumn / non-list view already emits bout objects
|
|
170
308
|
handleEditBout(bout) {
|
|
171
309
|
this.$emit('action:editBout', bout);
|
|
172
310
|
},
|
|
173
311
|
handleDirectorBout(bout) {
|
|
174
312
|
this.$emit('action:directingBout', bout);
|
|
175
313
|
},
|
|
314
|
+
getRoundLayout(roundIndex) {
|
|
315
|
+
return createRoundLayout(roundIndex);
|
|
316
|
+
},
|
|
317
|
+
getPlannedStation(bout) {
|
|
318
|
+
return getPlannedStationForBout(this.plannedStationByBout, bout);
|
|
319
|
+
},
|
|
320
|
+
getPlannedStationLabel(bout) {
|
|
321
|
+
const station = this.getPlannedStation(bout);
|
|
322
|
+
if (!station) return '';
|
|
323
|
+
const directorName = station.Director?.DisplayName || 'Director TBD';
|
|
324
|
+
return `Suggested: ${station.RingName} / ${directorName}`;
|
|
325
|
+
},
|
|
326
|
+
registerNodeAnchor({ id, kind, el }) {
|
|
327
|
+
const anchors = kind === 'input' ? this.inputAnchors : this.outputAnchors;
|
|
328
|
+
const anchorId = String(id);
|
|
329
|
+
|
|
330
|
+
if (el) anchors.set(anchorId, el);
|
|
331
|
+
if (!el) anchors.delete(anchorId);
|
|
332
|
+
this.scheduleConnectorUpdate();
|
|
333
|
+
},
|
|
334
|
+
observeTableauSize() {
|
|
335
|
+
if (typeof ResizeObserver === 'undefined' || !this.$refs.tableauRoot) return;
|
|
336
|
+
this.resizeObserver?.disconnect();
|
|
337
|
+
this.resizeObserver = new ResizeObserver(this.scheduleConnectorUpdate);
|
|
338
|
+
this.resizeObserver.observe(this.$refs.tableauRoot);
|
|
339
|
+
},
|
|
340
|
+
scheduleConnectorUpdate() {
|
|
341
|
+
if (this.scheduledFrame !== null) return;
|
|
342
|
+
this.scheduledFrame = -1;
|
|
343
|
+
|
|
344
|
+
const update = () => {
|
|
345
|
+
this.scheduledFrame = null;
|
|
346
|
+
this.recomputeConnectors();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
this.$nextTick(() => {
|
|
350
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
351
|
+
this.scheduledFrame = requestAnimationFrame(update);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
update();
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
recomputeConnectors() {
|
|
358
|
+
const rootElement = this.$refs.tableauRoot;
|
|
359
|
+
if (this.listView || !rootElement) {
|
|
360
|
+
this.connectorPaths = [];
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const layout = createDomAnchorLayout({
|
|
365
|
+
rootElement,
|
|
366
|
+
getInputAnchor: id => this.inputAnchors.get(String(id)),
|
|
367
|
+
getOutputAnchor: id => this.outputAnchors.get(String(id))
|
|
368
|
+
});
|
|
176
369
|
|
|
177
|
-
|
|
178
|
-
|
|
370
|
+
this.svgSize = layout.size;
|
|
371
|
+
const paths = buildBracketPaths({
|
|
372
|
+
bouts: this.bouts,
|
|
373
|
+
getInputPoint: layout.getInputPoint,
|
|
374
|
+
getOutputPoint: layout.getOutputPoint
|
|
375
|
+
});
|
|
376
|
+
this.connectorPaths = addPlannedStrokesToConnectors(paths, this.plannedStationByBout);
|
|
179
377
|
}
|
|
180
378
|
}
|
|
181
379
|
};
|
package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js
ADDED
|
@@ -0,0 +1,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
|
+
});
|
package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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>
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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>
|