@dcrackel/hematournamentui 1.0.714 → 1.0.716

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 (25) hide show
  1. package/README.md +2 -0
  2. package/dist/HemaTournamentUI-lib.es.js +4367 -3850
  3. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  4. package/dist/hematournamentui-tailwind.css +267 -9
  5. package/package.json +1 -1
  6. package/src/index.js +45 -43
  7. package/src/stories/Organisms/Cards/RingBoutCard/RingBoutCard.vue +158 -0
  8. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +10 -0
  9. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +140 -80
  10. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.test.js +94 -0
  11. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +180 -153
  12. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +19 -3
  13. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +59 -22
  14. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +13 -0
  15. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +26 -0
  16. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +6 -2
  17. package/src/stories/Templates/EventManagement/EventAttendance/EventAttendance.stories.js +35 -3
  18. package/src/stories/Templates/EventManagement/EventAttendance/EventAttendance.test.js +73 -0
  19. package/src/stories/Templates/EventManagement/EventAttendance/EventAttendance.vue +417 -403
  20. package/src/stories/Templates/EventManagement/RingBoard/README.md +29 -0
  21. package/src/stories/Templates/EventManagement/RingBoard/RingBoard.stories.js +118 -0
  22. package/src/stories/Templates/EventManagement/RingBoard/RingBoard.test.js +208 -0
  23. package/src/stories/Templates/EventManagement/RingBoard/RingBoard.vue +170 -0
  24. package/src/stories/Util/ringBoard.js +103 -0
  25. package/src/stories/Util/waitlistAttendance.js +8 -0
@@ -1,153 +1,180 @@
1
- <template>
2
- <div
3
- v-if="person"
4
- class="flex items-center border border-dropdownSelect rounded-lg shadow-lg bg-poolBox hover:border-bright"
5
- >
6
- <div class="hidden md:block mr-4 border-r border-dropdownSelect h-20 w-10 items-center text-center pt-7">
7
- <BaseText :text="queueLabel" size="md" color="quaternary" weight="bold" />
8
- </div>
9
- <div class="block md:hidden mr-4 border-r border-dropdownSelect h-16 w-5 items-center text-center pt-5">
10
- <BaseText :text="queueLabel" size="sm" color="quaternary" weight="bold" />
11
- </div>
12
-
13
- <div class="hidden md:block mr-4">
14
- <div v-if="!portraitURL" class="w-14 h-14 rounded-xl bg-dropdownSelect flex items-center justify-center">
15
- <BaseText text="" size="sm" color="neutral" weight="bold" />
16
- </div>
17
- <img v-if="portraitURL" :src="portraitURL" alt="Portrait" class="w-14 h-14 rounded-xl" />
18
- </div>
19
-
20
- <div class="flex flex-col w-3/4 md:w-4/12 justify-start">
21
- <div class="flex items-center justify-start gap-2 min-w-0">
22
- <button class="min-w-0 text-left" type="button" @click="handleForwardToProfile">
23
- <BaseText
24
- :text="displayName"
25
- size="md"
26
- color="quaternary"
27
- weight="bold"
28
- class="text-sm md:text-xl truncate"
29
- />
30
- </button>
31
- <MissingHemaRatingsId :person="person.Person" />
32
- </div>
33
- <span class="w-full flex justify-start">
34
- <BaseText :text="clubName" size="md" color="quinary" weight="normal" class="hidden md:block" />
35
- <BaseText :text="clubShortName" size="xs" color="quinary" weight="normal" class="block md:hidden" />
36
- </span>
37
-
38
- <div class="grid md:hidden grid-cols-2 gap-x-4 mr-10">
39
- <div class="flex">
40
- <BaseText text="M2: " size="xs" color="quaternary" weight="normal" class="mr-1" />
41
- <BaseText :text="M2Rating" size="xs" color="quinary" weight="bold" class="ml-1" />
42
- </div>
43
- <div class="flex">
44
- <BaseText text="HR: " size="xs" color="quaternary" weight="normal" class="mr-1" />
45
- <BaseText :text="HEMARating" size="xs" color="quinary" weight="bold" class="ml-1" />
46
- </div>
47
- </div>
48
- </div>
49
-
50
- <div class="hidden md:grid grid-cols-1 gap-x-4 mr-4 w-36">
51
- <div class="flex">
52
- <BaseText text="M2Rating:" size="sm" color="quaternary" weight="normal" class="mr-2" />
53
- <BaseText :text="M2Rating" size="sm" color="quinary" weight="bold" class="pt-0.5" />
54
- </div>
55
- <div class="flex">
56
- <BaseText text="HemaRating:" size="sm" color="quaternary" weight="normal" class="mr-2" />
57
- <BaseText :text="HEMARating" size="sm" color="quinary" weight="bold" />
58
- </div>
59
- </div>
60
-
61
- <div class="mr-2 md:mr-4 md:ml-auto flex flex-row">
62
- <div class="flex justify-center w-20">
63
- <div class="hidden md:flex border border-dropdownSelect shadow-md rounded-lg bg-natural h-9 px-3 items-center">
64
- <BaseText text="Unpaid" size="xs" color="quinary" weight="bold" />
65
- </div>
66
- </div>
67
- <div class="w-44 flex justify-end">
68
- <BaseButton
69
- label="Move to Active"
70
- size="xs"
71
- type="primary"
72
- icon-name="fa-arrow-up-from-bracket"
73
- class="border border-dropdownSelect shadow-md rounded-l-lg bg-natural w-40 m-0! p-0! h-10"
74
- @buttonClick="handleMoveToActive"
75
- />
76
- </div>
77
- </div>
78
-
79
- <div
80
- class="block md:ml-4 border-l border-dropdownSelect h-16 w-9 md:h-20 md:w-10 items-center text-center hover:bg-eventBoxRed rounded-tr-lg rounded-br-lg pt-5 md:pt-6"
81
- >
82
- <BaseIcon
83
- icon-name="fa-circle-xmark"
84
- size=""
85
- color="quinary"
86
- hover="alarm"
87
- @click="handleRemoveFencer"
88
- class="text-md md:text-2xl"
89
- />
90
- </div>
91
- </div>
92
- </template>
93
-
94
- <script>
95
- import BaseIcon from '../../../Atoms/Icon/BaseIcon.vue';
96
- import BaseText from '../../../Atoms/Text/BaseText.vue';
97
- import BaseButton from '../../../Molecules/Buttons/BaseButton/BaseButton.vue';
98
- import MissingHemaRatingsId from '../../../Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue';
99
-
100
- export default {
101
- name: 'WaitlistFencerCard',
102
- components: { BaseButton, BaseIcon, BaseText, MissingHemaRatingsId },
103
- emits: ['forward:toProfile', 'move:active', 'remove:fencer'],
104
- props: {
105
- person: {
106
- type: Object,
107
- required: true
108
- },
109
- queuePosition: {
110
- type: Number,
111
- required: true
112
- }
113
- },
114
- computed: {
115
- queueLabel() {
116
- return `#${this.queuePosition}`;
117
- },
118
- displayName() {
119
- return this.person?.Person?.DisplayName || 'Unknown fencer';
120
- },
121
- clubName() {
122
- return this.person?.Person?.Club?.Name || 'No club listed';
123
- },
124
- clubShortName() {
125
- return this.person?.Person?.Club?.ShortName || this.clubName;
126
- },
127
- portraitURL() {
128
- const images = this.person?.Person?.Images || [];
129
- const portrait = images.find(image => image.Type === 'Portrait');
130
- return portrait ? portrait.URL : '';
131
- },
132
- HEMARating() {
133
- const rating = this.person?.Person?.HEMARatings?.[0]?.Rating;
134
- return rating || 0;
135
- },
136
- M2Rating() {
137
- const rating = this.person?.Person?.M2Ratings?.[0]?.Rating;
138
- return rating || 'U';
139
- }
140
- },
141
- methods: {
142
- handleMoveToActive() {
143
- this.$emit('move:active', { ...this.person });
144
- },
145
- handleRemoveFencer() {
146
- this.$emit('remove:fencer', { ...this.person });
147
- },
148
- handleForwardToProfile() {
149
- this.$emit('forward:toProfile', this.person.PersonId);
150
- }
151
- }
152
- };
153
- </script>
1
+ <template>
2
+ <div
3
+ v-if="person"
4
+ class="flex flex-wrap items-center gap-3 rounded-lg border border-dropdownSelect bg-poolBox py-3 pl-3 shadow-lg hover:border-bright md:flex-nowrap"
5
+ >
6
+ <div class="w-8 shrink-0 border-r border-dropdownSelect pr-3 text-center">
7
+ <BaseText :text="queueLabel" size="sm" color="quaternary" weight="bold" />
8
+ </div>
9
+ <div class="hidden shrink-0 lg:block">
10
+ <div v-if="!portraitURL" class="flex h-14 w-14 items-center justify-center rounded-xl bg-dropdownSelect"></div>
11
+ <img v-else :src="portraitURL" alt="Portrait" class="h-14 w-14 rounded-xl" />
12
+ </div>
13
+
14
+ <div class="min-w-0 flex-1 basis-40 text-left">
15
+ <div class="flex min-w-0 items-center gap-2">
16
+ <button class="min-w-0 text-left" type="button" @click="handleForwardToProfile">
17
+ <BaseText
18
+ :text="displayName"
19
+ size="md"
20
+ color="quaternary"
21
+ weight="bold"
22
+ class="break-words text-sm md:text-xl"
23
+ />
24
+ </button>
25
+ <MissingHemaRatingsId :person="person.Person" />
26
+ </div>
27
+ <BaseText :text="clubName" size="sm" color="quinary" class="hidden md:block" />
28
+ <BaseText :text="clubShortName" size="xs" color="quinary" class="md:hidden" />
29
+ <div class="mt-2 flex flex-wrap items-center gap-2 text-xs" aria-live="polite">
30
+ <span class="rounded px-2 py-1 font-semibold" :class="checkInClass">{{ checkInStatus }}</span>
31
+ <span
32
+ v-if="isNextCheckedIn && checkedIn"
33
+ class="rounded bg-eventBoxBlueTwo px-2 py-1 font-semibold text-primary"
34
+ >Next checked in</span
35
+ >
36
+ <span class="text-primaryHighlight">{{ waiverStatus }}</span>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="hidden shrink-0 text-left xl:block">
41
+ <BaseText :text="`M2: ${M2Rating}`" size="xs" color="quinary" />
42
+ <BaseText :text="`HR: ${HEMARating}`" size="xs" color="quinary" />
43
+ </div>
44
+
45
+ <div class="flex w-full flex-wrap items-center justify-end gap-y-2 md:ml-auto md:w-auto md:shrink-0">
46
+ <div class="mr-2 flex items-center gap-2">
47
+ <span
48
+ class="hidden rounded border border-dropdownSelect px-2 py-1 text-xs font-bold text-quinary lg:inline-flex"
49
+ >Unpaid</span
50
+ >
51
+ <BaseButton
52
+ label="Move to Active"
53
+ size="xs"
54
+ type="primary"
55
+ icon-name="fa-arrow-up-from-bracket"
56
+ class="m-0! min-h-10"
57
+ @buttonClick="handleMoveToActive"
58
+ />
59
+ </div>
60
+ <div class="mr-2 flex w-44 shrink-0 justify-end md:mr-4">
61
+ <BaseButton
62
+ v-if="needsWaiver"
63
+ label="Sign Waiver"
64
+ size="xs"
65
+ type="primary"
66
+ class="m-0! h-10 w-40 p-0!"
67
+ @buttonClick="requestWeaverSign"
68
+ />
69
+ <ButtonBar
70
+ v-else
71
+ :buttons="attendanceButtons"
72
+ :selected="selectedAttendance"
73
+ class="mr-1 min-h-10"
74
+ @update:selected="handleAttendance"
75
+ />
76
+ </div>
77
+ <button
78
+ type="button"
79
+ :aria-label="`Remove ${displayName} from waitlist`"
80
+ class="flex min-h-10 w-9 shrink-0 items-center justify-center border-l border-dropdownSelect text-quinary hover:bg-eventBoxRed focus-visible:outline-2 focus-visible:outline-bright md:ml-4 md:w-10"
81
+ @click="handleRemoveFencer"
82
+ >
83
+ <BaseIcon icon-name="fa-circle-xmark" size="lg" color="quinary" hover="alarm" />
84
+ </button>
85
+ </div>
86
+ </div>
87
+ </template>
88
+
89
+ <script>
90
+ import BaseIcon from '../../../Atoms/Icon/BaseIcon.vue';
91
+ import BaseText from '../../../Atoms/Text/BaseText.vue';
92
+ import BaseButton from '../../../Molecules/Buttons/BaseButton/BaseButton.vue';
93
+ import ButtonBar from '../../../Molecules/Buttons/ButtonBar/ButtonBar.vue';
94
+ import MissingHemaRatingsId from '../../../Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue';
95
+ import { isWaitlistCheckedIn, needsAttendanceWaiver } from '../../../Util/waitlistAttendance.js';
96
+
97
+ export default {
98
+ name: 'WaitlistFencerCard',
99
+ components: { BaseButton, ButtonBar, BaseIcon, BaseText, MissingHemaRatingsId },
100
+ emits: ['forward:toProfile', 'move:active', 'remove:fencer', 'update:check-in', 'request:weaver-sign'],
101
+ props: {
102
+ person: { type: Object, required: true },
103
+ queuePosition: { type: Number, required: true },
104
+ isNextCheckedIn: { type: Boolean, default: false }
105
+ },
106
+ computed: {
107
+ queueLabel() {
108
+ return `#${this.queuePosition}`;
109
+ },
110
+ displayName() {
111
+ return this.person?.Person?.DisplayName || 'Unknown fencer';
112
+ },
113
+ clubName() {
114
+ return this.person?.Person?.Club?.Name || 'No club listed';
115
+ },
116
+ clubShortName() {
117
+ return this.person?.Person?.Club?.ShortName || this.clubName;
118
+ },
119
+ portraitURL() {
120
+ const images = this.person?.Person?.Images || [];
121
+ return images.find(image => image.Type === 'Portrait')?.URL || '';
122
+ },
123
+ HEMARating() {
124
+ return this.person?.Person?.HEMARatings?.[0]?.Rating || 0;
125
+ },
126
+ M2Rating() {
127
+ return this.person?.Person?.M2Ratings?.[0]?.Rating || 'U';
128
+ },
129
+ checkedIn() {
130
+ return isWaitlistCheckedIn(this.person);
131
+ },
132
+ needsWaiver() {
133
+ return needsAttendanceWaiver(this.person);
134
+ },
135
+ checkInStatus() {
136
+ return this.checkedIn ? 'Checked in' : 'Not checked in';
137
+ },
138
+ attendanceButtons() {
139
+ return [
140
+ { id: 'Absent', label: 'Absent', defaultBgColor: 'bg-natural', selectedBgColor: 'bg-eventBoxBlueTwo' },
141
+ { id: 'Present', label: 'Present', defaultBgColor: 'bg-natural', selectedBgColor: 'bg-eventBoxGreenTwo' }
142
+ ];
143
+ },
144
+ selectedAttendance() {
145
+ return this.checkedIn ? 'Present' : 'Absent';
146
+ },
147
+ checkInClass() {
148
+ return this.checkedIn ? 'bg-acceptedHighlight text-accepted' : 'bg-dropdownSelect text-primaryHighlight';
149
+ },
150
+ waiverStatus() {
151
+ if (this.needsWaiver) return 'Waiver needed';
152
+ return this.person?.Person?.Weaver?.Required ? 'Waiver signed' : 'No waiver required';
153
+ }
154
+ },
155
+ methods: {
156
+ clonePerson() {
157
+ return JSON.parse(JSON.stringify(this.person));
158
+ },
159
+ requestWeaverSign() {
160
+ this.$emit('request:weaver-sign', this.clonePerson());
161
+ },
162
+ handleAttendance(selected) {
163
+ if (selected === 'Present' && this.needsWaiver) {
164
+ this.requestWeaverSign();
165
+ return;
166
+ }
167
+ this.$emit('update:check-in', { ...this.clonePerson(), CheckedIn: selected === 'Present' });
168
+ },
169
+ handleMoveToActive() {
170
+ this.$emit('move:active', this.clonePerson());
171
+ },
172
+ handleRemoveFencer() {
173
+ this.$emit('remove:fencer', this.clonePerson());
174
+ },
175
+ handleForwardToProfile() {
176
+ this.$emit('forward:toProfile', this.person.PersonId ?? this.person.Person?.PersonId);
177
+ }
178
+ }
179
+ };
180
+ </script>
@@ -41,11 +41,27 @@ describe('BracketDirectorAssignmentPanel', () => {
41
41
  ).toEqual([stations[1]]);
42
42
  });
43
43
 
44
- it('opens director management', async () => {
44
+ it.each([
45
+ ['Manage directors', 'request:manage-planned-directors'],
46
+ ['Redistribute scheduled bouts', 'request:redistribute-scheduled-bouts'],
47
+ ['Refresh assignments', 'request:refresh-assignments']
48
+ ])('emits %s intent without changing the station list', async (label, event) => {
45
49
  const wrapper = shallowMount(BracketDirectorAssignmentPanel, {
46
50
  props: { stations, canManageDirectors: true }
47
51
  });
48
- await wrapper.get('[data-testid="manage-bracket-directors"]').trigger('click');
49
- expect(wrapper.emitted('request:manage-planned-directors')).toEqual([[]]);
52
+ const button = wrapper.get('button[aria-label="' + label + '"]');
53
+ expect(button.attributes('title')).toBe(label);
54
+ await button.trigger('click');
55
+
56
+ expect(wrapper.emitted(event)).toEqual([[]]);
57
+ expect(Object.keys(wrapper.emitted()).filter(name => name.startsWith('request:'))).toEqual([event]);
58
+ expect(wrapper.props('stations')).toEqual(stations);
59
+ wrapper.unmount();
60
+ });
61
+
62
+ it('hides the toolbar when management is unavailable', () => {
63
+ const wrapper = shallowMount(BracketDirectorAssignmentPanel, { props: { stations } });
64
+ expect(wrapper.find('[role="group"]').exists()).toBe(false);
65
+ wrapper.unmount();
50
66
  });
51
67
  });
@@ -1,29 +1,33 @@
1
1
  <template>
2
2
  <aside
3
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"
4
+ aria-label="Director assignments"
5
+ class="w-full border-b border-dropdownSelect py-4 xl:w-72 xl:border-b-0 xl:border-l xl:pl-4"
5
6
  >
6
- <div class="mb-4 flex items-start justify-between gap-3">
7
- <div>
8
- <BaseText color="primaryHighlight" size="md" text="Director Assignments" weight="bold" />
9
- <BaseText
10
- class="mt-1"
11
- color="quaternary"
12
- size="xs"
13
- text="Plan ring coverage. Scheduled bouts may still be moved between rings."
14
- />
7
+ <header class="mb-4 space-y-3 border-b border-dropdownSelect pb-4">
8
+ <h2 class="text-md font-bold text-primaryHighlight">Director Assignments</h2>
9
+ <div v-if="canManageDirectors" class="flex flex-wrap gap-1" role="group" aria-label="Director assignment actions">
10
+ <button
11
+ v-for="action in directorActions"
12
+ :key="action.event"
13
+ type="button"
14
+ :data-testid="action.testId"
15
+ :aria-label="action.description"
16
+ :title="action.description"
17
+ class="flex min-h-9 flex-auto items-center justify-center gap-1 whitespace-nowrap rounded border border-dropdownSelect bg-white px-1 py-2 text-xs text-primaryHighlight transition hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
18
+ @click="$emit(action.event)"
19
+ >
20
+ <BaseIcon :iconName="action.icon" iconStyle="fa" color="primaryHighlight" size="xs" aria-hidden="true" />
21
+ <span>{{ action.label }}</span>
22
+ </button>
15
23
  </div>
16
- <button
17
- v-if="canManageDirectors"
18
- type="button"
19
- data-testid="manage-bracket-directors"
20
- class="rounded border border-dropdownSelect bg-white px-2 py-1 text-xs text-primaryHighlight hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
21
- @click="$emit('request:manage-planned-directors')"
22
- >
23
- Manage
24
- </button>
25
- </div>
26
24
 
25
+ <BaseText
26
+ color="quaternary"
27
+ size="xs"
28
+ text="Plan ring coverage. Scheduled bouts may still be moved between rings."
29
+ />
30
+ </header>
27
31
  <div class="space-y-3">
28
32
  <BracketDirectorAssignmentCard
29
33
  v-for="station in stations"
@@ -42,12 +46,45 @@
42
46
 
43
47
  <script>
44
48
  import BaseText from '../../../../Atoms/Text/BaseText.vue';
49
+ import BaseIcon from '../../../../Atoms/Icon/BaseIcon.vue';
45
50
  import BracketDirectorAssignmentCard from '../../../Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
46
51
 
52
+ const directorActions = [
53
+ {
54
+ label: 'Manage',
55
+ description: 'Manage directors',
56
+ icon: 'fa-users',
57
+ event: 'request:manage-planned-directors',
58
+ testId: 'manage-bracket-directors'
59
+ },
60
+ {
61
+ label: 'Redistribute',
62
+ description: 'Redistribute scheduled bouts',
63
+ icon: 'fa-random',
64
+ event: 'request:redistribute-scheduled-bouts',
65
+ testId: 'redistribute-scheduled-bouts'
66
+ },
67
+ {
68
+ label: 'Refresh',
69
+ description: 'Refresh assignments',
70
+ icon: 'fa-refresh',
71
+ event: 'request:refresh-assignments',
72
+ testId: 'refresh-bracket-assignments'
73
+ }
74
+ ];
47
75
  export default {
48
76
  name: 'BracketDirectorAssignmentPanel',
49
- components: { BaseText, BracketDirectorAssignmentCard },
50
- emits: ['request:add-planned-director', 'request:remove-planned-director', 'request:manage-planned-directors'],
77
+ components: { BaseIcon, BaseText, BracketDirectorAssignmentCard },
78
+ emits: [
79
+ 'request:add-planned-director',
80
+ 'request:remove-planned-director',
81
+ 'request:manage-planned-directors',
82
+ 'request:redistribute-scheduled-bouts',
83
+ 'request:refresh-assignments'
84
+ ],
85
+ data() {
86
+ return { directorActions };
87
+ },
51
88
  props: {
52
89
  canManageDirectors: { type: Boolean, default: false },
53
90
  stations: {
@@ -135,6 +135,11 @@ const renderInteractiveBracket = args => ({
135
135
  requestBoutDirector,
136
136
  assignPlannedDirector,
137
137
  handleAddPlannedDirector,
138
+ redistributeAssignments,
139
+ refreshAssignments() {
140
+ storyStations.value = cloneStations(args.plannedDirectingStations);
141
+ applyProvidedAssignments(args.plannedBoutAssignments);
142
+ },
138
143
  handleRemovePlannedDirector(station) {
139
144
  const remainingStations = storyStations.value.filter(item => String(item.RingId) !== String(station?.RingId));
140
145
  if (remainingStations.length === storyStations.value.length) return;
@@ -174,6 +179,8 @@ const renderInteractiveBracket = args => ({
174
179
  :plannedBoutAssignments="storyAssignments"
175
180
  :showAddPlannedDirectorCard="args.showAddPlannedDirectorCard && canAddDirector"
176
181
  @request:bout-director="requestBoutDirector"
182
+ @request:redistribute-scheduled-bouts="redistributeAssignments"
183
+ @request:refresh-assignments="refreshAssignments"
177
184
  @close:directing-bout="handleCloseDirectingBout"
178
185
  @request:add-planned-director="handleAddPlannedDirector"
179
186
  @request:remove-planned-director="handleRemovePlannedDirector"
@@ -208,6 +215,12 @@ export default {
208
215
  showBoutDirectorAssignment: true
209
216
  },
210
217
  argTypes: {
218
+ 'request:redistribute-scheduled-bouts': {
219
+ description: 'Parent intent to redistribute scheduled coverage without changing active or completed bouts.'
220
+ },
221
+ 'request:refresh-assignments': {
222
+ description: 'Parent intent to reload stations and assignments. This story reloads the supplied args fixtures.'
223
+ },
211
224
  availableDirectors: {
212
225
  control: 'object',
213
226
  description: 'Eligible staff, including DE-only directors, supplied by the parent.'
@@ -216,3 +216,29 @@ describe('Bracket assignment defaults', () => {
216
216
  wrapper.unmount();
217
217
  });
218
218
  });
219
+
220
+ describe('Bracket assignment toolbar', () => {
221
+ it.each(['request:redistribute-scheduled-bouts', 'request:refresh-assignments'])(
222
+ 'forwards %s without changing bouts or planned coverage',
223
+ async event => {
224
+ const bouts = [createBout(), createBout({ BoutId: 43, Status: 'Active', RefereeId: 99 })];
225
+ const stations = [{ RingId: 1, Director: { PersonId: 99, DisplayName: 'Dave Smith' } }];
226
+ const assignments = [{ DEBoutId: 1, RingId: 1 }];
227
+ const wrapper = mountBracket(bouts, {
228
+ availableDirectors: [stations[0].Director],
229
+ rings: [{ RingId: 1 }],
230
+ plannedDirectingStations: stations,
231
+ plannedBoutAssignments: assignments
232
+ });
233
+
234
+ wrapper.findComponent(BracketDirectorAssignmentPanel).vm.$emit(event);
235
+ await wrapper.vm.$nextTick();
236
+
237
+ expect(wrapper.emitted()).toEqual({ [event]: [[]] });
238
+ expect(wrapper.vm.boutsData).toEqual(bouts);
239
+ expect(wrapper.props('plannedDirectingStations')).toEqual(stations);
240
+ expect(wrapper.props('plannedBoutAssignments')).toEqual(assignments);
241
+ wrapper.unmount();
242
+ }
243
+ );
244
+ });
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <section>
2
+ <section class="flex min-h-full flex-col xl:min-h-[calc(100dvh-2rem)]">
3
3
  <EditEventsTopMenu :currentTab="'Bracket'" :tabs="tabs(role, isMobile)" @tabMenuClick="handleTabMenuClick" />
4
4
  <ToggleHeader
5
5
  :currentSelection="viewName"
@@ -17,7 +17,7 @@
17
17
  :statusText="connectedToServer ? 'Connected' : 'Disconnected'"
18
18
  />
19
19
 
20
- <div class="flex flex-col gap-4 xl:flex-row">
20
+ <div class="flex flex-1 flex-col gap-4 xl:flex-row">
21
21
  <div class="min-w-0 flex-1">
22
22
  <Tableau
23
23
  :bouts="sortedBouts"
@@ -42,6 +42,8 @@
42
42
  :showAddCard="showAddPlannedDirectorCard"
43
43
  :canManageDirectors="canManagePlannedDirectors"
44
44
  @request:manage-planned-directors="handleManagePlannedDirectors"
45
+ @request:redistribute-scheduled-bouts="$emit('request:redistribute-scheduled-bouts')"
46
+ @request:refresh-assignments="$emit('request:refresh-assignments')"
45
47
  @request:add-planned-director="handleAddPlannedDirector"
46
48
  @request:remove-planned-director="$emit('request:remove-planned-director', $event)"
47
49
  />
@@ -136,6 +138,8 @@ export default {
136
138
  'penalty:update',
137
139
  'request:add-planned-director',
138
140
  'request:remove-planned-director',
141
+ 'request:redistribute-scheduled-bouts',
142
+ 'request:refresh-assignments',
139
143
  'update:planned-directing-stations',
140
144
  'request:bout-active',
141
145
  'request:bout-director',
@@ -43,7 +43,7 @@ export default {
43
43
  },
44
44
  argTypes: {
45
45
  personsInTournament: {
46
- control: { type: 'array' },
46
+ control: { type: 'object' },
47
47
  description: 'List of persons in the tournament',
48
48
  table: {
49
49
  type: { summary: 'Array' },
@@ -51,7 +51,7 @@ export default {
51
51
  }
52
52
  },
53
53
  personsAll: {
54
- control: { type: 'array' },
54
+ control: { type: 'object' },
55
55
  description: 'List of all persons',
56
56
  table: {
57
57
  type: { summary: 'Array' },
@@ -59,7 +59,7 @@ export default {
59
59
  }
60
60
  },
61
61
  fencingClubs: {
62
- control: { type: 'array' },
62
+ control: { type: 'object' },
63
63
  description: 'List of clubs',
64
64
  table: {
65
65
  type: { summary: 'Array' },
@@ -117,6 +117,38 @@ export const WithWaitlist = {
117
117
  }
118
118
  };
119
119
 
120
+ export const WaitlistCheckIn = {
121
+ parameters: {
122
+ docs: {
123
+ description: {
124
+ story:
125
+ 'Waitlisted fencers sign the waiver, then use Absent/Present to check in without joining the active roster. Move to Active remains available to fill a no-show spot. The parent handles update:waitlist-check-in ({ ...person, CheckedIn }), weaver:sign, and move:waitlist-to-active, then supplies updated personsInTournament. Status stays Waitlist until the parent promotes the fencer.'
126
+ }
127
+ }
128
+ },
129
+ args: {
130
+ ...WithWaitlist.args,
131
+ personsInTournament: [
132
+ {
133
+ ...activeRoster[0],
134
+ Status: 'Absent',
135
+ Person: {
136
+ ...activeRoster[0].Person,
137
+ Weaver: { Required: true, Scope: 'Tournament', Signed: true }
138
+ }
139
+ },
140
+ ...waitlistedPersons.map((person, index) => ({
141
+ ...person,
142
+ CheckedIn: index === 2,
143
+ Person: {
144
+ ...person.Person,
145
+ Weaver: { Required: true, Scope: 'Tournament', Signed: index !== 0 }
146
+ }
147
+ }))
148
+ ]
149
+ }
150
+ };
151
+
120
152
  export const RegistrationOpen = {
121
153
  args: {
122
154
  personsInTournament: eventPersonGetEventById.persons,