@dcrackel/hematournamentui 1.0.686 → 1.0.688

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.
@@ -0,0 +1,242 @@
1
+ <template>
2
+ <section class="w-full">
3
+ <div class="flex w-full ">
4
+ <!-- LEFT: Merge into this account -->
5
+ <section class="border border-dropdownSelect rounded-lg p-4 bg-white w-1/2 mr-10">
6
+ <BaseText
7
+ :text="`Merge into this account: ${person?.DisplayName || 'Root Account'}`"
8
+ size="md"
9
+ color="primary"
10
+ />
11
+
12
+ <div class="mt-3 space-y-2">
13
+ <div class="text-sm text-quaternary">
14
+ <span class="font-semibold text-primary">PersonId:</span> {{ rootPersonId }}
15
+ </div>
16
+ <div class="text-sm text-quaternary">
17
+ <span class="font-semibold text-primary">Legal Name:</span> {{ legalName }}
18
+ </div>
19
+ <div class="text-sm text-quaternary">
20
+ <span class="font-semibold text-primary">Club:</span> {{ clubName }}
21
+ </div>
22
+ <div class="text-sm text-quaternary">
23
+ <span class="font-semibold text-primary">Email:</span> {{ primaryEmail }}
24
+ </div>
25
+ </div>
26
+
27
+ <div class="mt-4 flex flex-col justify-between h-64">
28
+ <div>
29
+ <BaseText text="Accounts to merge in" size="sm" color="quaternary" weight="bold" />
30
+ <div class="border-b border-dropdownSelect h-2"></div>
31
+ <div v-if="selected.length === 0" class="mt-2 text-sm text-quaternary">
32
+ None selected yet.
33
+ </div>
34
+ <div v-else class="mt-2 border border-dropdownSelect rounded-lg">
35
+ <div
36
+ v-for="p in selected"
37
+ :key="p.PersonId"
38
+ class="flex items-center justify-between px-3 py-2 border-b border-dropdownSelect last:border-b-0"
39
+ >
40
+ <div class="min-w-0">
41
+ <div class="text-sm text-primary font-semibold truncate">
42
+ {{ p.DisplayName }}
43
+ <span class="text-xs text-quaternary font-normal">(#{{ p.PersonId }})</span>
44
+ </div>
45
+ <div class="text-xs text-quaternary truncate">
46
+ {{ p.FirstName }} {{ p.LastName }}
47
+ <span v-if="p?.Club?.Name"> • {{ p.Club.Name }}</span>
48
+ </div>
49
+ </div>
50
+
51
+ <BaseButton
52
+ color="warn"
53
+ label="Remove"
54
+ size="xs"
55
+ type="primary"
56
+ @buttonClick="removeSelected(p.PersonId)"
57
+ />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div>
62
+ <div class="flex justify-end">
63
+ <BaseButton
64
+ color="warn"
65
+ label="Merge Accounts Together"
66
+ size="sm"
67
+ type="primary"
68
+ :disabled="selected.length === 0"
69
+ @buttonClick="doMerge"
70
+ />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </section>
75
+
76
+ <!-- RIGHT: Filter + Results -->
77
+ <section class="border border-dropdownSelect rounded-lg p-4 bg-white bg-white w-1/2">
78
+ <BaseText text="Results" size="md" color="primary" />
79
+
80
+ <div class="mt-4">
81
+ <TitledInput
82
+ title="Filter people"
83
+ placeholder="Type name or club..."
84
+ :inputValue="filterText"
85
+ @update:value="filterText = $event"
86
+ />
87
+ <div class="text-xs text-quaternary mt-1">
88
+ Filters Display Name, First/Last Name, or Club Name.
89
+ </div>
90
+ </div>
91
+
92
+ <div class="mt-4 flex items-center justify-between text-xs text-quaternary">
93
+ <span>
94
+ Showing {{ filteredPeople.length }}{{ totalMatches > resultLimit ? '+' : '' }} results
95
+ </span>
96
+ <span v-if="totalMatches > resultLimit">
97
+ (limited to first {{ resultLimit }})
98
+ </span>
99
+ </div>
100
+
101
+ <!-- Scrollable results list -->
102
+ <div class="mt-2 border border-dropdownSelect rounded-lg h-64 overflow-y-auto ">
103
+ <div
104
+ v-for="p in filteredPeople"
105
+ :key="p.PersonId"
106
+ class="flex items-center justify-between px-3 py-2 border-b border-dropdownSelect last:border-b-0"
107
+ >
108
+ <div class="min-w-0">
109
+ <div class="text-sm text-primary font-semibold truncate">
110
+ <span class="text-xs text-quaternary font-normal">(#{{ p.PersonId }})</span>
111
+ {{ p.DisplayName }} - {{ getPrimaryEmail(p) }}
112
+ </div>
113
+ <div class="text-xs text-quaternary truncate">
114
+ {{ p.FirstName }} {{ p.LastName }}
115
+ <span v-if="p?.Club?.Name"> • {{ p.Club.Name }}</span>
116
+ </div>
117
+ </div>
118
+
119
+ <BaseButton
120
+ color="neutral"
121
+ label="Add"
122
+ size="xs"
123
+ type="primary"
124
+ @buttonClick="addSelected(p)"
125
+ />
126
+ </div>
127
+
128
+ <div v-if="filteredPeople.length === 0" class="px-3 py-3 text-sm text-quaternary">
129
+ No matches.
130
+ </div>
131
+ </div>
132
+ </section>
133
+ </div>
134
+ </section>
135
+ </template>
136
+
137
+ <script>
138
+ import BaseText from "../../../Atoms/Text/BaseText.vue";
139
+ import TitledInput from "../../../Molecules/CombinationInputs/TitledInput/TitledInput.vue";
140
+ import BaseButton from "../../../Molecules/Buttons/BaseButton/BaseButton.vue";
141
+
142
+ export default {
143
+ name: "MergeAccounts",
144
+ components: { BaseText, TitledInput, BaseButton },
145
+ emits: ["merge:merge"],
146
+ props: {
147
+ person: { type: Object, required: true }, // root person
148
+ people: { type: Array, required: true },
149
+ },
150
+ data() {
151
+ return {
152
+ filterText: "",
153
+ selected: [],
154
+ resultLimit: 200, // "high limit" but still prevents crazy DOM
155
+ };
156
+ },
157
+ computed: {
158
+ getPrimaryEmail(p) {
159
+ const emails = p?.PersonEmails || [];
160
+ return emails.find(e => e.IsPrimary)?.EmailAddress || "";
161
+ },
162
+ rootPersonId() {
163
+ return this.person?.PersonId ?? "";
164
+ },
165
+ legalName() {
166
+ const first = this.person?.FirstName || "";
167
+ const last = this.person?.LastName || "";
168
+ return `${first} ${last}`.trim() || "-";
169
+ },
170
+ clubName() {
171
+ return this.person?.Club?.Name || "-";
172
+ },
173
+ primaryEmail() {
174
+ const emails = this.person?.PersonEmails || [];
175
+ const primary = emails.find((e) => e.IsPrimary);
176
+ return primary?.EmailAddress || "-";
177
+ },
178
+ selectedIds() {
179
+ return new Set(this.selected.map((p) => p.PersonId));
180
+ },
181
+ totalMatches() {
182
+ const q = (this.filterText || "").trim().toLowerCase();
183
+ const base = (this.people || []).filter(
184
+ (p) => p && p.PersonId !== this.rootPersonId && !this.selectedIds.has(p.PersonId)
185
+ );
186
+
187
+ if (!q) return base.length;
188
+
189
+ return base.reduce((acc, p) => (this.matchesQuery(p, q) ? acc + 1 : acc), 0);
190
+ },
191
+ filteredPeople() {
192
+ const q = (this.filterText || "").trim().toLowerCase();
193
+
194
+ const base = (this.people || []).filter(
195
+ (p) => p && p.PersonId !== this.rootPersonId && !this.selectedIds.has(p.PersonId)
196
+ );
197
+
198
+ if (!q) return base.slice(0, this.resultLimit);
199
+
200
+ const results = [];
201
+ for (const p of base) {
202
+ if (this.matchesQuery(p, q)) {
203
+ results.push(p);
204
+ if (results.length >= this.resultLimit) break;
205
+ }
206
+ }
207
+ return results;
208
+ },
209
+ },
210
+ methods: {
211
+ matchesQuery(p, q) {
212
+ const display = (p.DisplayName || "").toLowerCase();
213
+ const first = (p.FirstName || "").toLowerCase();
214
+ const last = (p.LastName || "").toLowerCase();
215
+ const club = (p?.Club?.Name || "").toLowerCase();
216
+ const clubShort = (p?.Club?.ShortName || "").toLowerCase();
217
+
218
+ return (
219
+ display.includes(q) ||
220
+ first.includes(q) ||
221
+ last.includes(q) ||
222
+ club.includes(q) ||
223
+ clubShort.includes(q)
224
+ );
225
+ },
226
+ addSelected(p) {
227
+ if (!p || p.PersonId === this.rootPersonId) return;
228
+ if (this.selectedIds.has(p.PersonId)) return;
229
+ this.selected = [...this.selected, p];
230
+ },
231
+ removeSelected(personId) {
232
+ this.selected = this.selected.filter((p) => p.PersonId !== personId);
233
+ },
234
+ doMerge() {
235
+ this.$emit("merge:merge", {
236
+ rootPersonId: this.rootPersonId,
237
+ mergePersonIds: this.selected.map((p) => p.PersonId),
238
+ });
239
+ },
240
+ },
241
+ };
242
+ </script>
@@ -2,6 +2,7 @@ import PersonManagement from './PersonManagement.vue';
2
2
  import clubGetAllMock from "../../../mocks/clubGetAllMock.js";
3
3
  import personsMock from "../../../mocks/personsMock.js";
4
4
  import attendance from "../../../mocks/getAttendance.js";
5
+ import allPersonsMock from "../../../mocks/personGetAllMock.js"
5
6
 
6
7
  export default {
7
8
  title: 'Templates/PersonManagement/PersonManagement',
@@ -44,6 +45,7 @@ export const admin = {
44
45
  personProfile: personsMock[1],
45
46
  fencingClubs: clubGetAllMock,
46
47
  attendance: attendance,
48
+ people: allPersonsMock,
47
49
  uploadImageName: 'profile-test',
48
50
  uploadServer: 'http://localhost:3000/api/upload/',
49
51
  imageServer: 'http://localhost:3000/uploads/',
@@ -55,6 +57,7 @@ export const clubAdmin = {
55
57
  args: {
56
58
  personProfile: personsMock[1],
57
59
  fencingClubs: clubGetAllMock,
60
+ people: allPersonsMock,
58
61
  attendance: attendance,
59
62
  uploadImageName: 'profile-test',
60
63
  uploadServer: 'http://localhost:3000/api/upload/',
@@ -69,6 +72,7 @@ export const adminWithLoading = {
69
72
  personProfile: personsMock,
70
73
  fencingClubs: clubGetAllMock,
71
74
  attendance: attendance,
75
+ people: allPersonsMock,
72
76
  uploadImageName: 'profile-test',
73
77
  uploadServer: 'http://localhost:3000/api/upload/',
74
78
  imageServer: 'http://localhost:3000/uploads/',
@@ -91,6 +95,7 @@ export const emptyProfile = {
91
95
  },
92
96
  fencingClubs: clubGetAllMock,
93
97
  attendance: attendance,
98
+ people: allPersonsMock,
94
99
  uploadImageName: 'profile-test',
95
100
  uploadServer: 'http://localhost:3000/api/upload/',
96
101
  imageServer: 'http://localhost:3000/uploads/',
@@ -27,7 +27,13 @@
27
27
 
28
28
  <EditEventsTopMenu :tabs="personProfileTabs(userLevel)" :currentTab="currentTab" @tabMenuClick="handleTab" />
29
29
  <BasicEdit v-if="currentTab === 'Basic'" :person="person" :fencingClubs="fencingClubs" :validation="validation" @update:person="updatePerson" />
30
- <FencerAdmin v-if="currentTab === 'Admin'" :person="person" :userLevel="userLevel" @update:person="updatePerson"/>
30
+ <FencerAdmin v-if="currentTab === 'Admin'"
31
+ :person="person"
32
+ :people="people"
33
+ :userLevel="userLevel"
34
+ @update:person="updatePerson"
35
+ @merge:merge="doMerge"
36
+ />
31
37
  <Attendance v-if="currentTab === 'Stats'" :attendance="attendance" />
32
38
  <Attendance v-if="currentTab === 'Attendance'" :attendance="attendance" />
33
39
 
@@ -53,7 +59,7 @@ import { validateField } from "../../Util/Validations/PersonManagementValidation
53
59
 
54
60
  export default {
55
61
  name: "PersonManagement",
56
- emits: ["submit:person","delete:person","update:image"],
62
+ emits: ["submit:person","delete:person","update:image","merge:merge"],
57
63
  components: {
58
64
  FencerAdmin,
59
65
  Attendance,
@@ -73,6 +79,10 @@ export default {
73
79
  type: Array,
74
80
  default: () => [],
75
81
  },
82
+ people: {
83
+ type: Array,
84
+ default: () => [],
85
+ },
76
86
  attendance: {
77
87
  type: Array,
78
88
  default: () => [],
@@ -170,6 +180,9 @@ export default {
170
180
  handleTab(tabId) {
171
181
  this.currentTab = tabId;
172
182
  },
183
+ doMerge(data) {
184
+ this.$emit("merge:merge", data);
185
+ }
173
186
  },
174
187
  };
175
188
  </script>