208 lines
10 KiB
JavaScript
208 lines
10 KiB
JavaScript
/**
|
|
* LoginScreen Component
|
|
*
|
|
* Displays the login form for the PWA.
|
|
* Handles username/password authentication with remember me option.
|
|
*/
|
|
|
|
export default {
|
|
name: 'LoginScreen',
|
|
emits: ['login', 'set-theme'],
|
|
props: {
|
|
theme: {
|
|
type: String,
|
|
default: 'system'
|
|
}
|
|
},
|
|
|
|
setup(props, { emit }) {
|
|
const { ref } = Vue;
|
|
|
|
// Form state
|
|
const username = ref('');
|
|
const password = ref('');
|
|
const rememberMe = ref(true);
|
|
const error = ref('');
|
|
const loading = ref(false);
|
|
const showPassword = ref(false);
|
|
|
|
// Theme picker (shown on first visit)
|
|
const showThemePicker = ref(!localStorage.getItem('theme'));
|
|
|
|
const handleSubmit = async () => {
|
|
if (!username.value || !password.value) {
|
|
error.value = 'Bitte Benutzername und Passwort eingeben';
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
error.value = '';
|
|
|
|
try {
|
|
const result = await new Promise((resolve) => {
|
|
// Emit returns undefined, we need to wait for parent to call back
|
|
const loginPromise = emit('login', {
|
|
username: username.value,
|
|
password: password.value,
|
|
rememberMe: rememberMe.value
|
|
});
|
|
|
|
// The parent will return the result
|
|
resolve(loginPromise);
|
|
});
|
|
|
|
if (result && !result.success) {
|
|
error.value = result.message || 'Login fehlgeschlagen';
|
|
if (result.requires2FA) {
|
|
error.value = 'Zwei-Faktor-Authentifizierung wird derzeit nicht unterstützt.';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
error.value = 'Ein Fehler ist aufgetreten';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const selectTheme = (newTheme) => {
|
|
emit('set-theme', newTheme);
|
|
showThemePicker.value = false;
|
|
};
|
|
|
|
return {
|
|
username,
|
|
password,
|
|
rememberMe,
|
|
error,
|
|
loading,
|
|
showPassword,
|
|
showThemePicker,
|
|
handleSubmit,
|
|
selectTheme
|
|
};
|
|
},
|
|
|
|
template: `
|
|
<div class="min-h-screen flex items-center justify-center bg-slate-100 dark:bg-slate-900 p-4">
|
|
<!-- Theme Picker Modal (First Visit) -->
|
|
<transition name="fade">
|
|
<div v-if="showThemePicker" class="fixed inset-0 bg-black bg-opacity-60 z-50 flex items-center justify-center p-4">
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-xs text-center shadow-2xl">
|
|
<h3 class="font-bold text-lg mb-2 text-slate-800 dark:text-white">Willkommen!</h3>
|
|
<p class="text-sm text-slate-600 dark:text-slate-300 mb-6">Wähle dein bevorzugtes Farbschema.</p>
|
|
<div class="flex flex-col space-y-3">
|
|
<button @click="selectTheme('light')" class="w-full px-4 py-3 bg-slate-200 text-slate-800 font-bold rounded-md hover:bg-slate-300 transition">
|
|
Hell
|
|
</button>
|
|
<button @click="selectTheme('dark')" class="w-full px-4 py-3 bg-slate-700 text-white font-bold rounded-md hover:bg-slate-600 transition">
|
|
Dunkel
|
|
</button>
|
|
<button @click="selectTheme('system')" class="w-full mt-2 text-sm text-slate-500 dark:text-slate-400 hover:underline">
|
|
Systemstandard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
|
|
<!-- Login Form -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6 w-full max-w-sm">
|
|
<!-- Logo -->
|
|
<div class="mb-8">
|
|
<img src="/assets/images/xinon-full-transparent.png" class="h-10 mx-auto dark:hidden" alt="Logo">
|
|
<img src="/assets/images/xinon-full-transparent-white.png" class="h-10 mx-auto hidden dark:block" alt="Logo">
|
|
</div>
|
|
|
|
<!-- Title -->
|
|
<h1 class="text-xl font-bold text-center text-slate-800 dark:text-white mb-6">
|
|
Lager Inventur
|
|
</h1>
|
|
|
|
<!-- Form -->
|
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
|
<!-- Username -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
Benutzername
|
|
</label>
|
|
<input
|
|
v-model="username"
|
|
type="text"
|
|
autocomplete="username"
|
|
autocapitalize="none"
|
|
class="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition dark:bg-slate-700 dark:border-slate-600 dark:text-white"
|
|
placeholder="Benutzername eingeben"
|
|
>
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
Passwort
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
v-model="password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
autocomplete="current-password"
|
|
class="w-full p-3 pr-12 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition dark:bg-slate-700 dark:border-slate-600 dark:text-white"
|
|
placeholder="Passwort eingeben"
|
|
>
|
|
<button
|
|
type="button"
|
|
@click="showPassword = !showPassword"
|
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
|
|
>
|
|
<svg v-if="!showPassword" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remember Me -->
|
|
<label class="flex items-center cursor-pointer">
|
|
<input
|
|
v-model="rememberMe"
|
|
type="checkbox"
|
|
class="w-4 h-4 rounded border-slate-300 text-primary focus:ring-primary dark:border-slate-600 dark:bg-slate-700"
|
|
>
|
|
<span class="ml-2 text-sm text-slate-600 dark:text-slate-300">
|
|
Angemeldet bleiben
|
|
</span>
|
|
</label>
|
|
|
|
<!-- Error Message -->
|
|
<div v-if="error" class="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<button
|
|
type="submit"
|
|
:disabled="loading"
|
|
class="w-full py-3 px-4 bg-primary text-white font-bold rounded-lg hover:bg-primary/90 focus:ring-4 focus:ring-primary/30 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
|
>
|
|
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
{{ loading ? 'Wird angemeldet...' : 'Anmelden' }}
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Footer -->
|
|
<div class="mt-8 text-center">
|
|
<p class="text-xs text-slate-400 dark:text-slate-500">
|
|
powered by XINON GmbH
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
};
|