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:
tajniak81andClaude Opus 5 committed 2026-08-17 17:38:54 +02:00
1 parent cd16d4383f
commit e373497958
10 files changed
+387 -151

No files matched your search

+8 -13
View File
@@ -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>