Settings: fold Users and Organization into the Settings tabs
The left rail is back to the three places you actually go — Garage, Charging, Settings — and user management moves inside Settings as an admin-only tab, next to a new Organization tab that used to be a card buried in the personal settings. Tab order is Personal settings, Users, Organization, Integrations. /admin redirects to /settings?tab=users so old links keep working, and ?tab= picks the starting tab in general. AdminUsers moves from views/ to components/ since it is a panel now, not a route, and its page header becomes a section header like its neighbours. The personal panel was split in two around the integrations markup, which left no gap between the Profile and Privacy cards; it is one block again. Creating a user gets an organization picker for superadmins, defaulting to "no organization" so an org-less account stays a deliberate choice. Admins see no picker: the server pins their members to their own org regardless, which users_test.go now covers along with both superadmin paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd16d4383f
commit
e373497958
10 files changed
+387
-151
No files matched your search
+8
-13
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, computed, ref } from "vue";
|
||||
import { RouterView, RouterLink, useRouter, useRoute } from "vue-router";
|
||||
import { state, isAuthenticated, isAdmin, logout, refreshProfile } from "./auth";
|
||||
import { state, isAuthenticated, logout, refreshProfile } from "./auth";
|
||||
import { prefs, applyProfilePrefs } from "./prefs";
|
||||
import { api } from "./api";
|
||||
import { t } from "./i18n";
|
||||
@@ -32,15 +32,12 @@ const userInitial = computed(() =>
|
||||
(state.user?.name || state.user?.email || "?").charAt(0).toUpperCase()
|
||||
);
|
||||
|
||||
// Sidebar nav. Admin item is filtered out for non-admins.
|
||||
const nav = computed(() =>
|
||||
[
|
||||
{ to: "/", label: t("nav.garage"), icon: "grid", exact: true },
|
||||
{ to: "/charging", label: t("nav.charging"), icon: "bolt" },
|
||||
{ to: "/settings", label: t("nav.settings"), icon: "gear" },
|
||||
isAdmin.value ? { to: "/admin", label: t("nav.users"), icon: "users" } : null,
|
||||
].filter(Boolean)
|
||||
);
|
||||
// Sidebar nav. User management is a tab inside Settings, not a rail item.
|
||||
const nav = computed(() => [
|
||||
{ to: "/", label: t("nav.garage"), icon: "grid", exact: true },
|
||||
{ to: "/charging", label: t("nav.charging"), icon: "bolt" },
|
||||
{ to: "/settings", label: t("nav.settings"), icon: "gear" },
|
||||
]);
|
||||
|
||||
function active(item) {
|
||||
return item.exact ? route.path === item.to : route.path.startsWith(item.to);
|
||||
@@ -88,9 +85,7 @@ onBeforeUnmount(() => themeObserver?.disconnect());
|
||||
<!-- charging -->
|
||||
<svg v-else-if="item.icon === 'bolt'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="h-5 w-5 shrink-0" :style="active(item) ? 'color:#60A5FA' : ''"><path stroke-linecap="round" stroke-linejoin="round" d="M13 2 4.5 13.5H11l-1 8.5 8.5-11.5H12z"/></svg>
|
||||
<!-- settings -->
|
||||
<svg v-else-if="item.icon === 'gear'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="h-5 w-5 shrink-0" :style="active(item) ? 'color:#60A5FA' : ''"><path stroke-linecap="round" stroke-linejoin="round" d="M9.6 3.6 9 6a7.5 7.5 0 0 0-1.7 1L5 6.3l-2 3.4 2 1.5a7.6 7.6 0 0 0 0 2l-2 1.5 2 3.4 2.3-.7c.5.4 1.1.8 1.7 1l.6 2.4h4l.6-2.4c.6-.2 1.2-.6 1.7-1l2.3.7 2-3.4-2-1.5a7.6 7.6 0 0 0 0-2l2-1.5-2-3.4-2.3.7A7.5 7.5 0 0 0 15 6l-.6-2.4z"/><circle cx="12" cy="12" r="2.6"/></svg>
|
||||
<!-- users -->
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="h-5 w-5 shrink-0" :style="active(item) ? 'color:#60A5FA' : ''"><path stroke-linecap="round" stroke-linejoin="round" d="M16 19v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 9a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm13 10v-2a4 4 0 0 0-3-3.9M16 2.1A4 4 0 0 1 16 9.9"/></svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="h-5 w-5 shrink-0" :style="active(item) ? 'color:#60A5FA' : ''"><path stroke-linecap="round" stroke-linejoin="round" d="M9.6 3.6 9 6a7.5 7.5 0 0 0-1.7 1L5 6.3l-2 3.4 2 1.5a7.6 7.6 0 0 0 0 2l-2 1.5 2 3.4 2.3-.7c.5.4 1.1.8 1.7 1l.6 2.4h4l.6-2.4c.6-.2 1.2-.6 1.7-1l2.3.7 2-3.4-2-1.5a7.6 7.6 0 0 0 0-2l2-1.5-2-3.4-2.3.7A7.5 7.5 0 0 0 15 6l-.6-2.4z"/><circle cx="12" cy="12" r="2.6"/></svg>
|
||||
<span class="hidden md:inline">{{ item.label }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
Reference in new issue
Block a user