Files
crm-system/app/webui/settings-users.html
T

232 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>