|
1 | 1 | <template>
|
2 |
| - <table class="table table-hover table-sm" data-testid="user-table"> |
| 2 | + <table class="table table-hover table-sm table-responsive-sm" data-testid="user-table"> |
3 | 3 | <thead>
|
4 | 4 | <tr>
|
5 | 5 | <th scope="col">Name</th>
|
6 | 6 | <th scope="col">Email</th>
|
7 | 7 | <th scope="col">Role</th>
|
| 8 | + <th scope="col">Managers</th> |
8 | 9 | <th scope="col">Actions</th>
|
9 | 10 | </tr>
|
10 | 11 | </thead>
|
11 | 12 | <tbody>
|
12 |
| - <tr v-for="user in users" :key="user._id"> |
| 13 | + <tr v-for="user in users" :key="user.immutable_id"> |
13 | 14 | <td align="left">
|
14 | 15 | {{ user.display_name }}
|
15 | 16 | <span v-if="user.account_status === 'active'" class="badge badge-success text-uppercase">
|
|
30 | 31 | </td>
|
31 | 32 | <td align="left">{{ user.contact_email }}</td>
|
32 | 33 | <td align="left">
|
33 |
| - <select |
| 34 | + <vSelect |
34 | 35 | v-model="user.role"
|
35 |
| - class="dropdown" |
36 |
| - @change="confirmUpdateUserRole(user._id.$oid, $event.target.value)" |
| 36 | + :options="roleOptions" |
| 37 | + :clearable="false" |
| 38 | + :searchable="false" |
| 39 | + class="form-control p-0 border-0" |
| 40 | + @update:model-value="(value) => confirmUpdateUserRole(user.immutable_id, value)" |
| 41 | + /> |
| 42 | + </td> |
| 43 | + <td align="left"> |
| 44 | + <vSelect |
| 45 | + v-model="user.managers" |
| 46 | + :options="potentialManagersMap[user.immutable_id]" |
| 47 | + label="display_name" |
| 48 | + multiple |
| 49 | + placeholder="No managers" |
| 50 | + :clearable="false" |
| 51 | + class="w-100" |
| 52 | + @update:model-value="(value) => handleManagersChange(user.immutable_id, value)" |
37 | 53 | >
|
38 |
| - <option value="user">User</option> |
39 |
| - <option value="admin">Admin</option> |
40 |
| - <option value="manager">Manager</option> |
41 |
| - </select> |
| 54 | + <template #option="option"> |
| 55 | + <div class="d-flex align-items-center"> |
| 56 | + <UserBubble :creator="option" :size="20" /> |
| 57 | + <span class="ml-2">{{ option.display_name }}</span> |
| 58 | + <span class="ml-auto badge badge-secondary small">{{ option.role }}</span> |
| 59 | + </div> |
| 60 | + </template> |
| 61 | + |
| 62 | + <template #selected-option="option"> |
| 63 | + <div class="d-flex align-items-center"> |
| 64 | + <UserBubble :creator="option" :size="18" /> |
| 65 | + <span class="ml-2 small">{{ option.display_name }}</span> |
| 66 | + </div> |
| 67 | + </template> |
| 68 | + </vSelect> |
42 | 69 | </td>
|
43 | 70 | <td align="left">
|
44 | 71 | <button
|
45 | 72 | v-if="user.account_status === 'active'"
|
46 | 73 | class="btn btn-outline-danger btn-sm text-uppercase text-monospace"
|
47 |
| - @click="confirmUpdateUserStatus(user._id.$oid, 'deactivated')" |
| 74 | + @click="confirmUpdateUserStatus(user.immutable_id, 'deactivated')" |
48 | 75 | >
|
49 | 76 | Deactivate
|
50 | 77 | </button>
|
51 | 78 | <button
|
52 | 79 | v-else-if="user.account_status === 'unverified'"
|
53 | 80 | class="btn btn-outline-success btn-sm text-uppercase text-monospace"
|
54 |
| - @click="confirmUpdateUserStatus(user._id.$oid, 'active')" |
| 81 | + @click="confirmUpdateUserStatus(user.immutable_id, 'active')" |
55 | 82 | >
|
56 | 83 | Activate
|
57 | 84 | </button>
|
58 | 85 | <button
|
59 | 86 | v-else-if="user.account_status === 'deactivated'"
|
60 | 87 | class="btn btn-outline-success btn-sm text-uppercase text-monospace"
|
61 |
| - @click="confirmUpdateUserStatus(user._id.$oid, 'active')" |
| 88 | + @click="confirmUpdateUserStatus(user.immutable_id, 'active')" |
62 | 89 | >
|
63 | 90 | Activate
|
64 | 91 | </button>
|
|
70 | 97 |
|
71 | 98 | <script>
|
72 | 99 | import { DialogService } from "@/services/DialogService";
|
73 |
| -
|
74 |
| -import { getUsersList, saveRole, saveUser } from "@/server_fetch_utils.js"; |
| 100 | +import vSelect from "vue-select"; |
| 101 | +import UserBubble from "@/components/UserBubble.vue"; |
| 102 | +import { getUsersList, saveRole, saveUser, saveUserManagers } from "@/server_fetch_utils.js"; |
75 | 103 |
|
76 | 104 | export default {
|
| 105 | + components: { |
| 106 | + vSelect, |
| 107 | + UserBubble, |
| 108 | + }, |
| 109 | +
|
77 | 110 | data() {
|
78 | 111 | return {
|
79 | 112 | users: null,
|
80 | 113 | original_users: null,
|
81 | 114 | tempRole: null,
|
| 115 | + roleOptions: ["user", "admin", "manager"], |
82 | 116 | };
|
83 | 117 | },
|
| 118 | + computed: { |
| 119 | + potentialManagersMap() { |
| 120 | + if (!this.users) return {}; |
| 121 | + const map = {}; |
| 122 | + this.users.forEach((u) => { |
| 123 | + map[u.immutable_id] = this.users.filter( |
| 124 | + (user) => |
| 125 | + user.immutable_id !== u.immutable_id && |
| 126 | + (user.role === "admin" || user.role === "manager"), |
| 127 | + ); |
| 128 | + }); |
| 129 | + return map; |
| 130 | + }, |
| 131 | + }, |
84 | 132 | created() {
|
85 | 133 | this.getUsers();
|
86 | 134 | },
|
87 | 135 | methods: {
|
88 | 136 | async getUsers() {
|
89 | 137 | let data = await getUsersList();
|
90 | 138 | if (data != null) {
|
| 139 | + const byId = {}; |
| 140 | + data.forEach((u) => { |
| 141 | + const id = u.immutable_id || u._id; |
| 142 | + byId[id] = u; |
| 143 | + }); |
| 144 | +
|
| 145 | + data.forEach((user) => { |
| 146 | + if (!user.managers) { |
| 147 | + user.managers = []; |
| 148 | + } |
| 149 | +
|
| 150 | + user.managers = user.managers |
| 151 | + .map((m) => { |
| 152 | + const mid = typeof m === "string" ? m : m.$oid || m.immutable_id; |
| 153 | + return byId[mid]; |
| 154 | + }) |
| 155 | + .filter(Boolean); |
| 156 | + }); |
| 157 | +
|
91 | 158 | this.users = JSON.parse(JSON.stringify(data));
|
92 | 159 | this.original_users = JSON.parse(JSON.stringify(data));
|
93 | 160 | }
|
94 | 161 | },
|
| 162 | + getPotentialManagers(userId) { |
| 163 | + if (!this.users) return []; |
| 164 | +
|
| 165 | + const potentials = this.users.filter((u) => { |
| 166 | + const isEligible = |
| 167 | + u.immutable_id !== userId && (u.role === "admin" || u.role === "manager"); |
| 168 | + return isEligible; |
| 169 | + }); |
| 170 | +
|
| 171 | + return potentials.sort((a, b) => (a.display_name || "").localeCompare(b.display_name || "")); |
| 172 | + }, |
| 173 | +
|
95 | 174 | async confirmUpdateUserRole(user_id, new_role) {
|
96 |
| - const originalCurrentUser = this.original_users.find((user) => user._id.$oid === user_id); |
| 175 | + const originalCurrentUser = this.original_users.find((user) => user.immutable_id === user_id); |
97 | 176 |
|
98 |
| - if (originalCurrentUser.role === "admin") { |
| 177 | + if (!originalCurrentUser) { |
99 | 178 | DialogService.error({
|
100 |
| - title: "Role Change Error", |
101 |
| - message: "You can't change an admin's role.", |
| 179 | + title: "Error", |
| 180 | + message: "Original user not found (id mismatch).", |
102 | 181 | });
|
103 |
| - this.users.find((user) => user._id.$oid === user_id).role = originalCurrentUser.role; |
| 182 | + const uiUser = this.users.find((u) => u.immutable_id === user_id); |
| 183 | + if (uiUser) uiUser.role = uiUser.role || "user"; |
104 | 184 | return;
|
105 | 185 | }
|
106 | 186 |
|
| 187 | + if (originalCurrentUser.role === "admin" && new_role !== "admin") { |
| 188 | + const confirmed = await DialogService.confirm({ |
| 189 | + title: "Change Admin Role", |
| 190 | + message: `Are you sure you want to remove admin privileges from ${originalCurrentUser.display_name}?`, |
| 191 | + type: "warning", |
| 192 | + }); |
| 193 | + if (!confirmed) { |
| 194 | + this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role; |
| 195 | + return; |
| 196 | + } |
| 197 | + } |
| 198 | +
|
107 | 199 | const confirmed = await DialogService.confirm({
|
108 | 200 | title: "Change User Role",
|
109 |
| - message: `Are you sure you want to change ${originalCurrentUser.display_name}'s role to ${new_role}?`, |
| 201 | + message: `Are you sure you want to change ${originalCurrentUser.display_name}'s role from "${originalCurrentUser.role}" to "${new_role}"?`, |
110 | 202 | type: "warning",
|
111 | 203 | });
|
| 204 | +
|
112 | 205 | if (confirmed) {
|
113 |
| - await this.updateUserRole(user_id, new_role); |
| 206 | + try { |
| 207 | + await this.updateUserRole(user_id, new_role); |
| 208 | + } catch (err) { |
| 209 | + this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role; |
| 210 | + } |
114 | 211 | } else {
|
115 |
| - this.users.find((user) => user._id.$oid === user_id).role = originalCurrentUser.role; |
| 212 | + this.users.find((user) => user.immutable_id === user_id).role = originalCurrentUser.role; |
| 213 | + } |
| 214 | + }, |
| 215 | +
|
| 216 | + async handleManagersChange(userId, managers) { |
| 217 | + if (!managers) managers = []; |
| 218 | +
|
| 219 | + const managerIds = managers.map((m) => m.immutable_id); |
| 220 | +
|
| 221 | + const userIndex = this.users.findIndex((u) => u.immutable_id === userId); |
| 222 | + const originalIndex = this.original_users.findIndex((u) => u.immutable_id === userId); |
| 223 | +
|
| 224 | + const originalUser = this.original_users[originalIndex]; |
| 225 | + const originalManagerIds = (originalUser?.managers || []).map((m) => m.immutable_id); |
| 226 | +
|
| 227 | + if (JSON.stringify(managerIds.sort()) === JSON.stringify(originalManagerIds.sort())) return; |
| 228 | +
|
| 229 | + const confirmed = await DialogService.confirm({ |
| 230 | + title: "Update Managers", |
| 231 | + message: `Are you sure you want to update managers for ${this.users[userIndex].display_name}?`, |
| 232 | + type: "info", |
| 233 | + }); |
| 234 | +
|
| 235 | + if (!confirmed) { |
| 236 | + this.users[userIndex].managers = originalUser.managers; |
| 237 | + return; |
| 238 | + } |
| 239 | +
|
| 240 | + try { |
| 241 | + await saveUserManagers(userId, managerIds); |
| 242 | +
|
| 243 | + const newManagers = this.potentialManagersMap[userId].filter((u) => |
| 244 | + managerIds.includes(u.immutable_id), |
| 245 | + ); |
| 246 | +
|
| 247 | + this.users[userIndex].managers = newManagers; |
| 248 | + this.original_users[originalIndex].managers = [...newManagers]; |
| 249 | + } catch (err) { |
| 250 | + this.users[userIndex].managers = originalUser.managers; |
| 251 | + DialogService.error({ |
| 252 | + title: "Error", |
| 253 | + message: "Failed to update managers.", |
| 254 | + }); |
116 | 255 | }
|
117 | 256 | },
|
118 | 257 | async confirmUpdateUserStatus(user_id, new_status) {
|
119 |
| - const originalCurrentUser = this.original_users.find((user) => user._id.$oid === user_id); |
| 258 | + const originalCurrentUser = this.original_users.find((user) => user.immutable_id === user_id); |
| 259 | +
|
| 260 | + if (!originalCurrentUser) { |
| 261 | + DialogService.error({ |
| 262 | + title: "Error", |
| 263 | + message: "Original user not found (id mismatch).", |
| 264 | + }); |
| 265 | + return; |
| 266 | + } |
120 | 267 |
|
121 | 268 | const confirmed = await DialogService.confirm({
|
122 | 269 | title: "Change User Status",
|
123 | 270 | message: `Are you sure you want to change ${originalCurrentUser.display_name}'s status from "${originalCurrentUser.account_status}" to "${new_status}"?`,
|
124 | 271 | type: "warning",
|
125 | 272 | });
|
126 | 273 | if (confirmed) {
|
127 |
| - this.users.find((user) => user._id.$oid == user_id).account_status = new_status; |
128 |
| - await this.updateUserStatus(user_id, new_status); |
| 274 | + this.users.find((user) => user.immutable_id == user_id).account_status = new_status; |
| 275 | + try { |
| 276 | + await this.updateUserStatus(user_id, new_status); |
| 277 | + } catch (err) { |
| 278 | + this.users.find((user) => user.immutable_id === user_id).account_status = |
| 279 | + originalCurrentUser.account_status; |
| 280 | + } |
129 | 281 | } else {
|
130 |
| - this.users.find((user) => user._id.$oid === user_id).account_status = |
| 282 | + this.users.find((user) => user.immutable_id === user_id).account_status = |
131 | 283 | originalCurrentUser.account_status;
|
132 | 284 | }
|
133 | 285 | },
|
| 286 | +
|
134 | 287 | async updateUserRole(user_id, user_role) {
|
135 | 288 | await saveRole(user_id, { role: user_role });
|
136 | 289 | this.original_users = JSON.parse(JSON.stringify(this.users));
|
137 | 290 | },
|
| 291 | +
|
138 | 292 | async updateUserStatus(user_id, status) {
|
139 | 293 | await saveUser(user_id, { account_status: status });
|
140 | 294 | this.original_users = JSON.parse(JSON.stringify(this.users));
|
|
0 commit comments