Add basic user edit page

This commit is contained in:
Eli Ribble 2026-03-28 16:31:29 -07:00
parent 4bfaaa72ce
commit 15371ec064
No known key found for this signature in database
5 changed files with 401 additions and 7 deletions

2
go.mod
View file

@ -48,6 +48,7 @@ require (
github.com/beevik/etree v1.1.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect
@ -89,6 +90,7 @@ require (
go.mongodb.org/mongo-driver v1.11.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect

4
go.sum
View file

@ -55,6 +55,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
@ -356,6 +358,8 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=

View file

@ -15,6 +15,7 @@ import ConfigurationUploadPool from "./view/configuration/UploadPool.vue";
import ConfigurationUploadPoolFlyover from "./view/configuration/UploadPoolFlyover.vue";
import ConfigurationUser from "./view/configuration/User.vue";
import ConfigurationUserAdd from "./view/configuration/UserAdd.vue";
import ConfigurationUserEdit from "./view/configuration/UserEdit.vue";
import Intelligence from "./view/Intelligence.vue";
import NotFound from "./view/NotFound.vue";
import OAuthRefreshArcgis from "./view/OAuthRefreshArcgis.vue";
@ -112,6 +113,13 @@ const routes: RouteRecordRaw[] = [
component: ConfigurationUserAdd,
meta: { requiresAuth: true, showSidebar: true },
},
{
component: ConfigurationUserEdit,
meta: { requiresAuth: true, showSidebar: true },
name: "User Edit",
path: "/_/configuration/user/:id",
props: true,
},
{
path: "/_/intelligence",
name: "Intelligence",

View file

@ -88,13 +88,11 @@
</span>
</td>
<td>
<button
class="btn btn-sm btn-warning"
title="Deactivate"
@click="deactivateUser(user.id)"
>
<i class="bi bi-person-x"></i>
</button>
<RouterLink :to="`/_/configuration${user.uri}`">
<button class="btn btn-sm btn-primary" title="Edit">
<i class="bi bi-person-x"></i>
</button>
</RouterLink>
</td>
</tr>
</tbody>

View file

@ -0,0 +1,382 @@
<style scoped>
.avatar-preview {
width: 120px;
height: 120px;
object-fit: cover;
border: 3px solid #dee2e6;
}
.btn-close-white {
opacity: 0.8;
}
.btn-close-white:hover {
opacity: 1;
}
.card {
border-radius: 0.5rem;
}
.badge {
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
display: inline-flex;
align-items: center;
}
pre {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
}
</style>
<template>
<div class="container mt-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">User Configuration</h4>
</div>
<div class="card-body">
<!-- Avatar Section -->
<div class="row mb-4">
<div class="col-md-12">
<label class="form-label fw-bold">Avatar</label>
<div class="d-flex align-items-center">
<div class="position-relative">
<img
:src="user.avatar || defaultAvatar"
alt="User Avatar"
class="rounded-circle avatar-preview"
/>
<button
class="btn btn-sm btn-primary position-absolute bottom-0 end-0 rounded-circle"
@click="triggerFileInput"
type="button"
>
<i class="bi bi-camera"></i>
</button>
</div>
<div class="ms-3">
<input
ref="fileInput"
type="file"
class="d-none"
accept="image/*"
@change="handleAvatarChange"
/>
<button
class="btn btn-outline-primary btn-sm me-2"
@click="triggerFileInput"
type="button"
>
Upload New
</button>
<button
class="btn btn-outline-danger btn-sm"
@click="removeAvatar"
type="button"
:disabled="!user.avatar"
>
Remove
</button>
</div>
</div>
</div>
</div>
<!-- Display Name -->
<div class="row mb-3">
<div class="col-md-6">
<label for="displayName" class="form-label fw-bold">
Display Name
</label>
<input
id="displayName"
v-model="user.displayName"
type="text"
class="form-control"
placeholder="Enter display name"
/>
</div>
<!-- Username (Read-only) -->
<div class="col-md-6">
<label for="username" class="form-label fw-bold">
Username
</label>
<input
id="username"
v-model="user.username"
type="text"
class="form-control"
readonly
disabled
/>
</div>
</div>
<!-- User Role -->
<div class="row mb-3">
<div class="col-md-6">
<label for="userRole" class="form-label fw-bold">
User Role
</label>
<select id="userRole" v-model="user.role" class="form-select">
<option value="">Select a role</option>
<option
v-for="role in availableRoles"
:key="role.value"
:value="role.value"
>
{{ role.label }}
</option>
</select>
</div>
<!-- User Status -->
<div class="col-md-6">
<label class="form-label fw-bold">User Status</label>
<div class="btn-group w-100" role="group">
<input
type="radio"
class="btn-check"
id="statusActive"
v-model="user.status"
value="active"
/>
<label class="btn btn-outline-success" for="statusActive">
<i class="bi bi-check-circle"></i> Active
</label>
<input
type="radio"
class="btn-check"
id="statusInactive"
v-model="user.status"
value="inactive"
/>
<label class="btn btn-outline-secondary" for="statusInactive">
<i class="bi bi-x-circle"></i> Inactive
</label>
</div>
</div>
</div>
<!-- User Tags -->
<div class="row mb-4">
<div class="col-md-12">
<label class="form-label fw-bold">User Tags</label>
<div class="mb-2">
<span
v-for="tag in user.tags"
:key="tag"
class="badge bg-info text-dark me-2 mb-2"
>
{{ tag }}
<button
type="button"
class="btn-close btn-close-white ms-2"
@click="removeTag(tag)"
style="font-size: 0.6rem"
></button>
</span>
<span
v-if="user.tags.length === 0"
class="text-muted fst-italic"
>
No tags added
</span>
</div>
<div class="input-group">
<select v-model="selectedTag" class="form-select">
<option value="">Select a tag</option>
<option
v-for="tag in availableTags"
:key="tag"
:value="tag"
:disabled="user.tags.includes(tag)"
>
{{ tag }}
</option>
</select>
<button
class="btn btn-outline-primary"
type="button"
@click="addTag"
:disabled="!selectedTag || user.tags.includes(selectedTag)"
>
<i class="bi bi-plus-lg"></i> Add Tag
</button>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row">
<div class="col-md-12">
<hr />
<div class="d-flex justify-content-end">
<button
class="btn btn-secondary me-2"
type="button"
@click="cancelChanges"
>
Cancel
</button>
<button
class="btn btn-primary"
type="button"
@click="saveChanges"
>
<i class="bi bi-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Preview Card -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Preview</h5>
</div>
<div class="card-body">
<pre class="mb-0">{{ user }}</pre>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from "vue";
interface User {
avatar: string;
displayName: string;
username: string;
role: string;
status: "active" | "inactive";
tags: string[];
}
interface Role {
value: string;
label: string;
}
export default defineComponent({
name: "UserConfiguration",
setup() {
const fileInput = ref<HTMLInputElement | null>(null);
const selectedTag = ref<string>("");
const defaultAvatar =
"https://via.placeholder.com/150/cccccc/666666?text=No+Avatar";
const user = reactive<User>({
avatar: "",
displayName: "John Doe",
username: "johndoe123",
role: "tech1",
status: "active",
tags: ["warrant"],
});
const availableRoles: Role[] = [
{ value: "manager", label: "Manager" },
{ value: "owner", label: "Owner" },
{ value: "tech1", label: "Tech 1" },
{ value: "tech2", label: "Tech 2" },
{ value: "tech3", label: "Tech 3" },
];
const availableTags: string[] = [
"warrant",
"drone pilot",
"certified",
"supervisor",
"field ops",
];
const triggerFileInput = () => {
fileInput.value?.click();
};
const handleAvatarChange = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
user.avatar = e.target?.result as string;
};
reader.readAsDataURL(file);
}
};
const removeAvatar = () => {
user.avatar = "";
if (fileInput.value) {
fileInput.value.value = "";
}
};
const addTag = () => {
if (selectedTag.value && !user.tags.includes(selectedTag.value)) {
user.tags.push(selectedTag.value);
selectedTag.value = "";
}
};
const removeTag = (tag: string) => {
const index = user.tags.indexOf(tag);
if (index > -1) {
user.tags.splice(index, 1);
}
};
const saveChanges = () => {
// Implement save logic here
console.log("Saving user configuration:", user);
alert("User configuration saved successfully!");
};
const cancelChanges = () => {
// Implement cancel/reset logic here
console.log("Canceling changes");
if (
confirm(
"Are you sure you want to cancel? All unsaved changes will be lost.",
)
) {
// Reset to original values or navigate away
window.history.back();
}
};
return {
user,
fileInput,
selectedTag,
defaultAvatar,
availableRoles,
availableTags,
triggerFileInput,
handleAvatarChange,
removeAvatar,
addTag,
removeTag,
saveChanges,
cancelChanges,
};
},
});
</script>