Upload app/webui/index.html
This commit is contained in:
@@ -0,0 +1,221 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>CRM – Anmeldung</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS via CDN (Dev) — nonced bundle required for Prod (v1.1) -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- App-specific overrides -->
|
||||||
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
|
|
||||||
|
<!-- Alpine.js (deferred so DOM is parsed first) -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<!-- App stores + components (ES modules) -->
|
||||||
|
<script type="module" src="/js/store.js"></script>
|
||||||
|
<script type="module" src="/js/components/notifications.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex items-center justify-center bg-slate-50 p-4">
|
||||||
|
|
||||||
|
<!-- Toast container (top-right) -->
|
||||||
|
<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="authForm()" x-cloak class="w-full max-w-md">
|
||||||
|
<div class="crm-card">
|
||||||
|
|
||||||
|
<!-- Logo / Title -->
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-slate-900">CRM System</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1" x-text="subtitle"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab switcher -->
|
||||||
|
<div class="flex border-b border-slate-200 mb-6" role="tablist">
|
||||||
|
<button type="button"
|
||||||
|
role="tab"
|
||||||
|
@click="switchTab('login')"
|
||||||
|
:class="tab === 'login'
|
||||||
|
? 'border-blue-600 text-blue-600'
|
||||||
|
: 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||||
|
class="flex-1 py-2 px-4 text-sm font-medium border-b-2 transition-colors">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
role="tab"
|
||||||
|
@click="switchTab('register')"
|
||||||
|
:class="tab === 'register'
|
||||||
|
? 'border-blue-600 text-blue-600'
|
||||||
|
: 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||||
|
class="flex-1 py-2 px-4 text-sm font-medium border-b-2 transition-colors">
|
||||||
|
Registrieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<div x-show="error" x-cloak
|
||||||
|
class="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 text-sm rounded"
|
||||||
|
x-text="error"></div>
|
||||||
|
|
||||||
|
<!-- LOGIN FORM -->
|
||||||
|
<form x-show="tab === 'login'" @submit.prevent="submitLogin()" novalidate>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="crm-label" for="login-email">E-Mail</label>
|
||||||
|
<input id="login-email"
|
||||||
|
type="email"
|
||||||
|
autocomplete="email"
|
||||||
|
required
|
||||||
|
x-model="loginEmail"
|
||||||
|
class="crm-input"
|
||||||
|
:disabled="loading" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="crm-label" for="login-password">Passwort</label>
|
||||||
|
<input id="login-password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
minlength="1"
|
||||||
|
x-model="loginPassword"
|
||||||
|
class="crm-input"
|
||||||
|
:disabled="loading" />
|
||||||
|
</div>
|
||||||
|
<button type="submit"
|
||||||
|
:disabled="loading || !loginEmail || !loginPassword"
|
||||||
|
class="btn-primary w-full flex items-center justify-center gap-2">
|
||||||
|
<span x-show="loading" class="crm-spinner"></span>
|
||||||
|
<span x-text="loading ? 'Anmelden …' : 'Anmelden'"></span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- REGISTER FORM -->
|
||||||
|
<form x-show="tab === 'register'" @submit.prevent="submitRegister()" novalidate>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="crm-label" for="reg-name">Name</label>
|
||||||
|
<input id="reg-name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
minlength="1"
|
||||||
|
maxlength="255"
|
||||||
|
x-model="regName"
|
||||||
|
class="crm-input"
|
||||||
|
:disabled="loading" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="crm-label" for="reg-email">E-Mail</label>
|
||||||
|
<input id="reg-email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
x-model="regEmail"
|
||||||
|
class="crm-input"
|
||||||
|
:disabled="loading" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="crm-label" for="reg-password">Passwort</label>
|
||||||
|
<input id="reg-password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
minlength="8"
|
||||||
|
maxlength="128"
|
||||||
|
autocomplete="new-password"
|
||||||
|
x-model="regPassword"
|
||||||
|
class="crm-input"
|
||||||
|
:disabled="loading" />
|
||||||
|
<p class="text-xs text-slate-500 mt-1">Mindestens 8 Zeichen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="crm-label" for="reg-role">Rolle (optional)</label>
|
||||||
|
<select id="reg-role" x-model="regRole" class="crm-input" :disabled="loading">
|
||||||
|
<option value="sales_rep">Sales-Rep (Standard)</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">
|
||||||
|
Hinweis: Bei der ersten Registrierung (Bootstrap) wird meistens ein
|
||||||
|
Admin-Konto angelegt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="submit"
|
||||||
|
:disabled="loading || !regEmail || !regPassword || !regName"
|
||||||
|
class="btn-primary w-full flex items-center justify-center gap-2">
|
||||||
|
<span x-show="loading" class="crm-spinner"></span>
|
||||||
|
<span x-text="loading ? 'Registrieren …' : 'Konto erstellen'"></span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-400 mt-6 text-center">
|
||||||
|
API-Endpoint: <code>/api/v1/auth/*</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page-level Alpine component -->
|
||||||
|
<script>
|
||||||
|
function authForm() {
|
||||||
|
return {
|
||||||
|
tab: 'login',
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
loginEmail: '',
|
||||||
|
loginPassword: '',
|
||||||
|
regName: '',
|
||||||
|
regEmail: '',
|
||||||
|
regPassword: '',
|
||||||
|
regRole: 'sales_rep',
|
||||||
|
|
||||||
|
get subtitle() {
|
||||||
|
return this.tab === 'login'
|
||||||
|
? 'Bitte mit E-Mail und Passwort anmelden.'
|
||||||
|
: 'Neues Konto anlegen.';
|
||||||
|
},
|
||||||
|
|
||||||
|
switchTab(t) {
|
||||||
|
this.tab = t;
|
||||||
|
this.error = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitLogin() {
|
||||||
|
this.error = '';
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const auth = Alpine.store('auth');
|
||||||
|
await auth.login(this.loginEmail.trim(), this.loginPassword);
|
||||||
|
// Erfolg → redirect
|
||||||
|
window.location.href = '/app.html';
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message || 'Anmeldung fehlgeschlagen.';
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitRegister() {
|
||||||
|
this.error = '';
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const auth = Alpine.store('auth');
|
||||||
|
await auth.register({
|
||||||
|
email: this.regEmail.trim(),
|
||||||
|
password: this.regPassword,
|
||||||
|
name: this.regName.trim(),
|
||||||
|
role: this.regRole,
|
||||||
|
});
|
||||||
|
window.location.href = '/app.html';
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.message || 'Registrierung fehlgeschlagen.';
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user