Upload app/webui/settings-users.html
This commit is contained in:
@@ -0,0 +1,231 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>CRM – User-Verwaltung</title>
|
||||||
|
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<script type="module" src="/js/store.js"></script>
|
||||||
|
<script type="module" src="/js/components/notifications.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-50">
|
||||||
|
|
||||||
|
<div x-data="toastContainer()" class="crm-toast-container" x-cloak>
|
||||||
|
<template x-for="n in items" :key="n.id">
|
||||||
|
<div class="crm-toast" :class="'toast-' + n.type"
|
||||||
|
@click="dismiss(n.id)" x-text="n.message"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-data="{ init: () => Alpine.store('auth').init() }" x-init="init()" class="flex min-h-screen">
|
||||||
|
|
||||||
|
<aside class="crm-sidebar">
|
||||||
|
<div class="px-6 py-4 text-white text-lg font-bold border-b border-slate-700">
|
||||||
|
<a href="/app.html" class="!text-white !border-0">CRM</a>
|
||||||
|
</div>
|
||||||
|
<nav class="mt-2">
|
||||||
|
<a href="/app.html">Dashboard</a>
|
||||||
|
<a href="/accounts.html">Accounts</a>
|
||||||
|
<a href="/contacts.html">Contacts</a>
|
||||||
|
<a href="/pipeline.html">Pipeline</a>
|
||||||
|
<a href="/activities.html">Activities</a>
|
||||||
|
<a href="/settings-profile.html">Profil</a>
|
||||||
|
<a href="/settings-users.html" class="active" x-show="$store.auth.isAdmin">Users (Admin)</a>
|
||||||
|
<a href="/settings-org.html" x-show="$store.auth.isAdmin">Org (Admin)</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
<header class="bg-white border-b border-slate-200 px-6 py-3 flex items-center justify-between">
|
||||||
|
<h1 class="text-xl font-semibold text-slate-800">User-Verwaltung (Admin)</h1>
|
||||||
|
<div class="flex items-center gap-3 text-sm">
|
||||||
|
<span class="text-slate-600">
|
||||||
|
<strong class="text-slate-900"
|
||||||
|
x-text="$store.auth.user ? $store.auth.user.name : '…'"></strong>
|
||||||
|
</span>
|
||||||
|
<button class="btn-secondary" @click="$store.auth.logout()">Logout</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 p-6" x-data="userAdmin()" x-init="init()">
|
||||||
|
|
||||||
|
<!-- Admin-Gate -->
|
||||||
|
<div x-show="checked && !$store.auth.isAdmin" class="crm-card text-center py-8">
|
||||||
|
<p class="text-red-600 font-medium">Zugriff verweigert.</p>
|
||||||
|
<p class="text-slate-500 text-sm mt-1">Diese Seite ist nur für Admins.</p>
|
||||||
|
<a href="/app.html" class="btn-primary inline-block mt-4">Zum Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="!checked" class="crm-card text-center py-8">
|
||||||
|
<div class="crm-spinner mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="checked && $store.auth.isAdmin">
|
||||||
|
<div class="flex justify-end mb-4">
|
||||||
|
<button @click="openCreate()" class="btn-primary">+ New User</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="loading" class="crm-card text-center py-6">
|
||||||
|
<div class="crm-spinner mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="!loading" class="overflow-x-auto">
|
||||||
|
<table class="crm-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>ID</th><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Aktionen</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="u in users" :key="u.id">
|
||||||
|
<tr>
|
||||||
|
<td x-text="u.id"></td>
|
||||||
|
<td x-text="u.name"></td>
|
||||||
|
<td x-text="u.email"></td>
|
||||||
|
<td><span class="crm-badge badge-blue" x-text="u.role"></span></td>
|
||||||
|
<td class="space-x-1">
|
||||||
|
<button @click="openEdit(u)" class="btn-secondary text-xs py-1 px-2">Edit</button>
|
||||||
|
<button @click="confirmDelete(u)" class="btn-danger text-xs py-1 px-2">Del</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr x-show="!loading && users.length === 0">
|
||||||
|
<td colspan="5" class="text-center text-slate-500 py-4">Keine User.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create / Edit modal -->
|
||||||
|
<div x-show="showForm" x-cloak class="crm-modal-backdrop" @click.self="closeForm()">
|
||||||
|
<div class="crm-modal p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4" x-text="formTitle"></h2>
|
||||||
|
<form @submit.prevent="submitForm()">
|
||||||
|
<div class="mb-3" x-show="!editing">
|
||||||
|
<label class="crm-label">E-Mail *</label>
|
||||||
|
<input type="email" required :disabled="editing"
|
||||||
|
x-model="form.email" class="crm-input" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" x-show="!editing">
|
||||||
|
<label class="crm-label">Passwort * (min. 8 Zeichen)</label>
|
||||||
|
<input type="password" required minlength="8" maxlength="128"
|
||||||
|
:disabled="editing"
|
||||||
|
x-model="form.password" class="crm-input" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="crm-label">Name *</label>
|
||||||
|
<input type="text" required maxlength="255"
|
||||||
|
x-model="form.name" class="crm-input" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="crm-label">Rolle</label>
|
||||||
|
<select x-model="form.role" class="crm-input">
|
||||||
|
<option value="sales_rep">Sales-Rep</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div x-show="formError" class="mb-3 p-2 bg-red-50 text-red-700 text-sm rounded"
|
||||||
|
x-text="formError"></div>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button type="button" @click="closeForm()" class="btn-secondary">Abbrechen</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary"
|
||||||
|
x-text="saving ? 'Speichere …' : 'Speichern'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function userAdmin() {
|
||||||
|
return {
|
||||||
|
checked: false,
|
||||||
|
users: [],
|
||||||
|
loading: false,
|
||||||
|
showForm: false,
|
||||||
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
formError: '',
|
||||||
|
form: { id: null, email: '', password: '', name: '', role: 'sales_rep' },
|
||||||
|
|
||||||
|
get formTitle() { return this.editing ? 'User bearbeiten' : 'Neuen User anlegen'; },
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
// wait for auth
|
||||||
|
const auth = Alpine.store('auth');
|
||||||
|
if (!auth.user) { try { await auth.fetchUser(); } catch {} }
|
||||||
|
this.checked = true;
|
||||||
|
if (!auth.isAdmin) return; // admin-gate shows message
|
||||||
|
await this.load();
|
||||||
|
},
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const { api } = await import('/js/api.js');
|
||||||
|
const r = await api.get('/users/?limit=200');
|
||||||
|
this.users = Array.isArray(r) ? r : (r.items || []);
|
||||||
|
} catch (err) {
|
||||||
|
Alpine.store('notifications').error(err.message || 'Lade-Fehler.');
|
||||||
|
} finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreate() {
|
||||||
|
this.editing = false;
|
||||||
|
this.form = { id: null, email: '', password: '', name: '', role: 'sales_rep' };
|
||||||
|
this.formError = '';
|
||||||
|
this.showForm = true;
|
||||||
|
},
|
||||||
|
openEdit(u) {
|
||||||
|
this.editing = true;
|
||||||
|
this.form = { id: u.id, email: u.email, password: '', name: u.name, role: u.role };
|
||||||
|
this.formError = '';
|
||||||
|
this.showForm = true;
|
||||||
|
},
|
||||||
|
closeForm() { this.showForm = false; },
|
||||||
|
|
||||||
|
async submitForm() {
|
||||||
|
this.saving = true; this.formError = '';
|
||||||
|
try {
|
||||||
|
const { api } = await import('/js/api.js');
|
||||||
|
if (this.editing) {
|
||||||
|
const p = { name: this.form.name, role: this.form.role };
|
||||||
|
await api.patch(`/users/${this.form.id}`, p);
|
||||||
|
Alpine.store('notifications').success('User aktualisiert.');
|
||||||
|
} else {
|
||||||
|
await api.post('/users/', {
|
||||||
|
email: this.form.email,
|
||||||
|
password: this.form.password,
|
||||||
|
name: this.form.name,
|
||||||
|
role: this.form.role,
|
||||||
|
});
|
||||||
|
Alpine.store('notifications').success('User angelegt.');
|
||||||
|
}
|
||||||
|
this.showForm = false;
|
||||||
|
await this.load();
|
||||||
|
} catch (err) {
|
||||||
|
this.formError = err.message || 'Fehler.';
|
||||||
|
} finally { this.saving = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
async confirmDelete(u) {
|
||||||
|
if (!confirm(`User "${u.name}" wirklich löschen?`)) return;
|
||||||
|
try {
|
||||||
|
const { api } = await import('/js/api.js');
|
||||||
|
await api.del(`/users/${u.id}`);
|
||||||
|
Alpine.store('notifications').success('User gelöscht.');
|
||||||
|
await this.load();
|
||||||
|
} catch (err) {
|
||||||
|
Alpine.store('notifications').error(err.message || 'Fehler.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user