Implement User Details page at /core/users/[user_id]
- Added dynamic route for user details with manager-level access - Implemented permission management UI (Super, Manager, Admin, Verified) - Added account status toggles and timestamp visibility - Integrated with V3 User CRUD logic and editable field whitelist
This commit is contained in:
219
src/routes/core/users/[user_id]/+page.svelte
Normal file
219
src/routes/core/users/[user_id]/+page.svelte
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { update_ae_obj__user, delete_ae_obj_id__user } from '$lib/ae_core/ae_core__user';
|
||||||
|
import { editable_fields__user } from '$lib/ae_core/ae_core__user.editable_fields';
|
||||||
|
import {
|
||||||
|
ShieldCheck,
|
||||||
|
Mail,
|
||||||
|
User as UserIcon,
|
||||||
|
ArrowLeft,
|
||||||
|
Save,
|
||||||
|
Trash2,
|
||||||
|
Lock,
|
||||||
|
Clock,
|
||||||
|
Key,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
let user = $state(data.user);
|
||||||
|
let saving = $state(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!$ae_loc.manager_access) {
|
||||||
|
goto('/core');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handle_save() {
|
||||||
|
saving = true;
|
||||||
|
const data_kv: any = {};
|
||||||
|
editable_fields__user.forEach(field => {
|
||||||
|
if (user[field] !== undefined) {
|
||||||
|
data_kv[field] = user[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await update_ae_obj__user({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
user_id: user.user_id_random,
|
||||||
|
data_kv,
|
||||||
|
log_lvl: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
alert('User updated successfully');
|
||||||
|
}
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handle_delete() {
|
||||||
|
if (!confirm('Are you sure you want to disable this user account?')) return;
|
||||||
|
|
||||||
|
const result = await delete_ae_obj_id__user({
|
||||||
|
api_cfg: $ae_api,
|
||||||
|
user_id: user.user_id_random,
|
||||||
|
method: 'disable',
|
||||||
|
log_lvl: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
goto('/core/users');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-4 space-y-6">
|
||||||
|
<header class="flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a class="btn btn-sm variant-soft" href="/core/users">
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="avatar variant-filled-primary w-12 h-12 flex items-center justify-center rounded-full">
|
||||||
|
<UserIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="h2">{user.username}</h1>
|
||||||
|
<p class="text-sm opacity-60">ID: {user.user_id_random}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn variant-filled-error" onclick={handle_delete} disabled={saving}>
|
||||||
|
<Trash2 size={16} class="mr-2" /> Disable
|
||||||
|
</button>
|
||||||
|
<button class="btn variant-filled-primary" onclick={handle_save} disabled={saving}>
|
||||||
|
<Save size={16} class="mr-2" /> Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Main Info -->
|
||||||
|
<div class="lg:col-span-2 space-y-6">
|
||||||
|
<div class="card p-4 space-y-4">
|
||||||
|
<h3 class="h4 border-b border-surface-500/30 pb-2 flex items-center gap-2">
|
||||||
|
<UserIcon size={18} /> Basic Profile
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<label class="label">
|
||||||
|
<span>Display Name</span>
|
||||||
|
<input class="input" type="text" bind:value={user.name} placeholder="Full Name" />
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<span>Email Address</span>
|
||||||
|
<input class="input" type="email" bind:value={user.email} />
|
||||||
|
</label>
|
||||||
|
<label class="label md:col-span-2">
|
||||||
|
<span>Username</span>
|
||||||
|
<input class="input font-mono variant-soft" type="text" bind:value={user.username} disabled />
|
||||||
|
<p class="text-xs opacity-60 mt-1 italic">Username changes are currently restricted.</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-4 space-y-4">
|
||||||
|
<h3 class="h4 border-b border-surface-500/30 pb-2 flex items-center gap-2">
|
||||||
|
<ShieldCheck size={18} /> System Permissions
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4">
|
||||||
|
<label class="flex items-center space-x-3 card p-3 variant-soft-error cursor-pointer">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.super} />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Super User</span>
|
||||||
|
<span class="text-xs opacity-70">Full system access</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-3 card p-3 variant-soft-warning cursor-pointer">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.manager} />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Manager</span>
|
||||||
|
<span class="text-xs opacity-70">Global account & user mgmt</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-3 card p-3 variant-soft-primary cursor-pointer">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.administrator} />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Administrator</span>
|
||||||
|
<span class="text-xs opacity-70">Account-level admin access</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-3 card p-3 variant-soft-secondary cursor-pointer">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.verified} />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Verified User</span>
|
||||||
|
<span class="text-xs opacity-70">Identity confirmed</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar / Meta -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="card p-4 space-y-4">
|
||||||
|
<h3 class="h4 border-b border-surface-500/30 pb-2 flex items-center gap-2">
|
||||||
|
<Clock size={18} /> Account Status
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.enable} />
|
||||||
|
<p>Login Enabled</p>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.hide} />
|
||||||
|
<p>Hidden from Lists</p>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input class="checkbox" type="checkbox" bind:checked={user.allow_auth_key} />
|
||||||
|
<p>Allow Email Auth Key</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-4 space-y-4 variant-soft-surface">
|
||||||
|
<h3 class="h4 border-b border-surface-500/30 pb-2 flex items-center gap-2">
|
||||||
|
<Lock size={18} /> Linking Info
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="opacity-60">Linked Person:</span>
|
||||||
|
<span class="font-mono">
|
||||||
|
{#if user.person_id_random}
|
||||||
|
<a href="/core/people/{user.person_id_random}" class="text-primary-500 underline">
|
||||||
|
{user.person_id_random}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="italic text-error-500">Unlinked</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="opacity-60">Account ID:</span>
|
||||||
|
<span class="font-mono">{user.account_id_random || '--'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-4 space-y-4">
|
||||||
|
<h3 class="h4 border-b border-surface-500/30 pb-2 flex items-center gap-2">
|
||||||
|
<Clock size={18} /> Timestamps
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2 text-xs">
|
||||||
|
<p><span class="opacity-60">Created:</span> {new Date(user.created_on).toLocaleString()}</p>
|
||||||
|
{#if user.updated_on}
|
||||||
|
<p><span class="opacity-60">Updated:</span> {new Date(user.updated_on).toLocaleString()}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
28
src/routes/core/users/[user_id]/+page.ts
Normal file
28
src/routes/core/users/[user_id]/+page.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { PageLoad } from './$types';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { load_ae_obj_id__user } from '$lib/ae_core/ae_core__user';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { ae_api } from '$lib/stores/ae_stores';
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ params, parent }) => {
|
||||||
|
const parentData = await parent();
|
||||||
|
const { user_id } = params;
|
||||||
|
|
||||||
|
if (!user_id) {
|
||||||
|
error(404, 'User ID not specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await load_ae_obj_id__user({
|
||||||
|
api_cfg: get(ae_api),
|
||||||
|
user_id,
|
||||||
|
log_lvl: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
error(404, 'User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user