807 lines
33 KiB
HTML
807 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Geteilte Einkaufsliste</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="icon" type="image/png" href="/favicon.png">
|
|
<style>
|
|
body {
|
|
background: linear-gradient(to right, #ece9e6, #ffffff);
|
|
}
|
|
.container {
|
|
max-width: 800px;
|
|
}
|
|
.input-card-sticky {
|
|
position: -webkit-sticky;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1020;
|
|
}
|
|
#page-title {
|
|
font-weight: 300;
|
|
}
|
|
.list-group-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.delete-btn {
|
|
cursor: pointer;
|
|
color: #dc3545;
|
|
font-weight: bold;
|
|
}
|
|
#status-bar {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #f8f9fa;
|
|
padding: 5px;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
border-top: 1px solid #dee2e6;
|
|
}
|
|
#suggestion-info {
|
|
font-size: 0.9rem;
|
|
color: #6c757d;
|
|
margin-top: 10px;
|
|
}
|
|
#suggestion-box {
|
|
margin-top: 5px;
|
|
}
|
|
.suggestion-btn {
|
|
margin-right: 5px;
|
|
margin-bottom: 5px;
|
|
}
|
|
#login-view, #app-view {
|
|
display: none; /* Hidden by default */
|
|
}
|
|
#login-view, #app-view {
|
|
display: none; /* Hidden by default */
|
|
}
|
|
</style> </style>
|
|
<body>
|
|
|
|
<!-- Login View -->
|
|
<div id="login-view" class="container mt-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-6">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<h3 id="login-title" class="card-title text-center"></h3>
|
|
<form id="login-form">
|
|
<div class="mb-3">
|
|
<label id="login-username-label" for="username" class="form-label"></label>
|
|
<input type="text" class="form-control" id="username" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label id="login-password-label" for="password" class="form-label"></label>
|
|
<input type="password" class="form-control" id="password" required>
|
|
</div>
|
|
<div class="d-grid">
|
|
<button id="login-button" type="submit" class="btn btn-primary"></button>
|
|
</div>
|
|
<div id="login-error" class="text-danger mt-2"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main App View -->
|
|
<div id="app-view" class="container mt-5">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h1 id="page-title" class="text-center mb-4"></h1>
|
|
<button id="logout-btn" class="btn btn-sm btn-outline-secondary">Logout</button>
|
|
</div>
|
|
|
|
<div id="online-users-container" class="mb-3" style="display: none;">
|
|
<strong>Online:</strong> <span id="online-users-list"></span>
|
|
</div>
|
|
|
|
<div id="user-view-content">
|
|
<div class="card shadow-sm mb-4 input-card-sticky">
|
|
<div class="card-body">
|
|
<form id="add-item-form" class="input-group">
|
|
<input type="text" id="item-name" class="form-control" autocomplete="off" required>
|
|
<button type="submit" id="add-button" class="btn btn-primary"></button>
|
|
<button type="button" id="add-one-button" class="btn btn-secondary"></button>
|
|
</form>
|
|
<div id="suggestion-info">
|
|
<div class="d-flex align-items-center mb-1">
|
|
<button id="example-button-existing" type="button" class="btn btn-primary btn-sm me-2" disabled></button>
|
|
<span id="info-text-existing"></span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<button id="example-button-new" type="button" class="btn btn-outline-primary btn-sm me-2" disabled></button>
|
|
<span id="info-text-new"></span>
|
|
</div>
|
|
</div>
|
|
<div id="suggestion-box"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-grid gap-2 mb-3">
|
|
<button id="notify-btn" class="btn btn-success"></button>
|
|
</div>
|
|
|
|
<ul id="item-list" class="list-group shadow-sm"></ul>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Admin Section -->
|
|
<div id="admin-section" class="card shadow-sm mt-5" style="display: none;">
|
|
<div id="admin-panel-title" class="card-header"></div>
|
|
<div class="card-body">
|
|
|
|
<h5 id="create-user-title" class="card-title"></h5>
|
|
<form id="create-user-form">
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label id="username-label" for="new-username" class="form-label"></label>
|
|
<input type="text" id="new-username" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label id="password-label" for="new-password" class="form-label"></label>
|
|
<input type="password" id="new-password" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3 d-flex align-items-end">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="new-user-is-admin">
|
|
<label id="is-admin-label" class="form-check-label" for="new-user-is-admin"></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button id="create-user-button" type="submit" class="btn btn-success"></button>
|
|
<div id="create-user-status" class="mt-2"></div>
|
|
</form>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5 id="change-password-title" class="card-title"></h5>
|
|
<form id="change-password-form">
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label id="current-password-label" for="current-password" class="form-label"></label>
|
|
<input type="password" id="current-password" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label id="new-password-label" for="new-password-change" class="form-label"></label>
|
|
<input type="password" id="new-password-change" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label id="confirm-password-label" for="confirm-password" class="form-label"></label>
|
|
<input type="password" id="confirm-password" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<button id="change-password-button" type="submit" class="btn btn-primary"></button>
|
|
<div id="change-password-status" class="mt-2"></div>
|
|
</form>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5 id="manage-users-title" class="card-title"></h5>
|
|
<ul id="user-list" class="list-group mb-3">
|
|
<!-- Users will be populated by JavaScript -->
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div id="status-bar"></div>
|
|
|
|
<script>
|
|
// Views
|
|
const loginView = document.getElementById('login-view');
|
|
const appView = document.getElementById('app-view');
|
|
const userViewContent = document.getElementById('user-view-content');
|
|
|
|
// Login elements
|
|
const loginForm = document.getElementById('login-form');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const loginError = document.getElementById('login-error');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
|
|
const adminSection = document.getElementById('admin-section');
|
|
const createUserForm = document.getElementById('create-user-form');
|
|
const newUsernameInput = document.getElementById('new-username');
|
|
const newPasswordInput = document.getElementById('new-password');
|
|
const newUserIsAdminCheckbox = document.getElementById('new-user-is-admin');
|
|
const createUserStatus = document.getElementById('create-user-status');
|
|
|
|
// App elements
|
|
const pageTitle = document.getElementById('page-title');
|
|
const itemList = document.getElementById('item-list');
|
|
const addItemForm = document.getElementById('add-item-form');
|
|
const itemNameInput = document.getElementById('item-name');
|
|
const addButton = document.getElementById('add-button');
|
|
const addOneButton = document.getElementById('add-one-button');
|
|
const suggestionInfo = document.getElementById('suggestion-info');
|
|
const suggestionBox = document.getElementById('suggestion-box');
|
|
const notifyBtn = document.getElementById('notify-btn');
|
|
const exampleButtonExisting = document.getElementById('example-button-existing');
|
|
const infoTextExisting = document.getElementById('info-text-existing');
|
|
const exampleButtonNew = document.getElementById('example-button-new');
|
|
const infoTextNew = document.getElementById('info-text-new');
|
|
const statusBar = document.getElementById('status-bar');
|
|
|
|
const onlineUsersContainer = document.getElementById('online-users-container');
|
|
const onlineUsersList = document.getElementById('online-users-list');
|
|
|
|
let standardItems = [];
|
|
let translations = {};
|
|
let currentShoppingList = [];
|
|
let currentUser = {};
|
|
let socket;
|
|
|
|
// --- WebSocket Functions ---
|
|
function connectWebSocket() {
|
|
const token = localStorage.getItem('access_token');
|
|
if (!token) return;
|
|
|
|
// Close existing socket if any
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.close();
|
|
}
|
|
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
socket = new WebSocket(`${wsProtocol}://${window.location.host}/ws/${token}`);
|
|
|
|
socket.onopen = () => {
|
|
console.log("WebSocket connection established.");
|
|
};
|
|
|
|
socket.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'user_list') {
|
|
if (data.users && data.users.length > 1) {
|
|
onlineUsersList.textContent = data.users.join(', ');
|
|
onlineUsersContainer.style.display = 'block';
|
|
} else {
|
|
onlineUsersContainer.style.display = 'none';
|
|
}
|
|
}
|
|
};
|
|
|
|
socket.onclose = () => {
|
|
console.log("WebSocket connection closed. Attempting to reconnect...");
|
|
setTimeout(connectWebSocket, 5000); // Reconnect after 5 seconds
|
|
};
|
|
|
|
socket.onerror = (error) => {
|
|
console.error("WebSocket error:", error);
|
|
socket.close();
|
|
};
|
|
}
|
|
|
|
// --- Auth Functions ---
|
|
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('access_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + token
|
|
};
|
|
}
|
|
|
|
async function handleLogin(event) {
|
|
event.preventDefault();
|
|
loginError.textContent = '';
|
|
|
|
const formData = new FormData();
|
|
formData.append('username', usernameInput.value);
|
|
formData.append('password', passwordInput.value);
|
|
|
|
try {
|
|
const response = await fetch('/token', {
|
|
method: 'POST',
|
|
body: new URLSearchParams(formData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
localStorage.setItem('access_token', data.access_token);
|
|
await showAppView();
|
|
} else {
|
|
loginError.textContent = translations.login_error_incorrect || 'Incorrect username or password.';
|
|
}
|
|
} catch (error) {
|
|
loginError.textContent = translations.login_error_generic || 'An error occurred. Please try again.';
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.close();
|
|
}
|
|
localStorage.removeItem('access_token');
|
|
location.reload();
|
|
}
|
|
|
|
async function handleCreateUser(event) {
|
|
event.preventDefault();
|
|
createUserStatus.textContent = '';
|
|
|
|
const newUser = {
|
|
username: newUsernameInput.value,
|
|
password: newPasswordInput.value,
|
|
is_admin: newUserIsAdminCheckbox.checked
|
|
};
|
|
|
|
const response = await fetch('/api/users', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(newUser)
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
const successMessage = (translations.user_created_success || "User '{username}' created successfully.").replace('{username}', newUser.username);
|
|
createUserStatus.textContent = successMessage;
|
|
createUserStatus.className = 'mt-2 text-success';
|
|
createUserForm.reset();
|
|
await fetchAndRenderUsers(); // Refresh the user list
|
|
} else {
|
|
createUserStatus.textContent = `Error: ${data.detail}`;
|
|
createUserStatus.className = 'mt-2 text-danger';
|
|
}
|
|
}
|
|
|
|
// --- View Management ---
|
|
|
|
function showLoginView() {
|
|
appView.style.display = 'none';
|
|
loginView.style.display = 'block';
|
|
adminSection.style.display = 'none';
|
|
if (userViewContent) userViewContent.style.display = 'none';
|
|
}
|
|
|
|
async function showAppView() {
|
|
loginView.style.display = 'none';
|
|
appView.style.display = 'block';
|
|
adminSection.style.display = 'none';
|
|
if (userViewContent) userViewContent.style.display = 'none';
|
|
connectWebSocket();
|
|
await initApp();
|
|
}
|
|
|
|
// --- App Functions ---
|
|
async function fetchCurrentUser() {
|
|
const response = await fetch(`/api/users/me?_=${new Date().getTime()}`, { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
currentUser = await response.json();
|
|
|
|
if (currentUser.is_admin) {
|
|
adminSection.style.display = 'block';
|
|
userViewContent.style.display = 'none';
|
|
pageTitle.textContent = 'Admin Panel';
|
|
} else {
|
|
adminSection.style.display = 'none';
|
|
userViewContent.style.display = 'block';
|
|
// Title will be set by fetchTranslations for regular users
|
|
}
|
|
}
|
|
|
|
async function fetchTranslations() {
|
|
const lang = navigator.language.startsWith('de') ? 'de' : 'en';
|
|
const response = await fetch(`/api/translations/${lang}?_=${new Date().getTime()}`);
|
|
translations = await response.json();
|
|
updateUIWithTranslations();
|
|
}
|
|
|
|
|
|
function updateUIWithTranslations() {
|
|
document.title = translations.title;
|
|
|
|
// Login View
|
|
if(document.getElementById('login-title')) {
|
|
document.getElementById('login-title').textContent = translations.login_title;
|
|
document.getElementById('login-username-label').textContent = translations.login_username_label;
|
|
document.getElementById('login-password-label').textContent = translations.login_password_label;
|
|
document.getElementById('login-button').textContent = translations.login_button;
|
|
}
|
|
|
|
// User View
|
|
pageTitle.textContent = translations.title;
|
|
itemNameInput.placeholder = translations.placeholder;
|
|
addButton.textContent = translations.add_button;
|
|
addOneButton.textContent = translations.add_one_button;
|
|
notifyBtn.textContent = translations.notify_button;
|
|
exampleButtonExisting.textContent = translations.example_button_text || 'Example';
|
|
exampleButtonNew.textContent = translations.example_button_text || 'Example';
|
|
infoTextExisting.textContent = translations.info_text_existing || '';
|
|
infoTextNew.textContent = translations.info_text_new || '';
|
|
|
|
// Admin View
|
|
if (document.getElementById('admin-panel-title')) {
|
|
document.getElementById('admin-panel-title').textContent = translations.admin_panel_title;
|
|
document.getElementById('create-user-title').textContent = translations.create_user_title;
|
|
document.getElementById('username-label').textContent = translations.username_label;
|
|
document.getElementById('password-label').textContent = translations.password_label;
|
|
document.getElementById('is-admin-label').textContent = translations.is_admin_label;
|
|
document.getElementById('create-user-button').textContent = translations.create_user_button;
|
|
document.getElementById('change-password-title').textContent = translations.change_password_title;
|
|
document.getElementById('current-password-label').textContent = translations.current_password_label;
|
|
document.getElementById('new-password-label').textContent = translations.new_password_label;
|
|
document.getElementById('confirm-password-label').textContent = translations.confirm_password_label;
|
|
document.getElementById('change-password-button').textContent = translations.change_password_button;
|
|
document.getElementById('manage-users-title').textContent = translations.manage_users_title;
|
|
}
|
|
|
|
// Common elements
|
|
if (document.getElementById('logout-btn')) {
|
|
document.getElementById('logout-btn').textContent = translations.logout_button;
|
|
}
|
|
}
|
|
|
|
async function fetchStandardItems() {
|
|
const lang = navigator.language.startsWith('de') ? 'de' : 'en';
|
|
const response = await fetch(`/api/standard-items/${lang}`);
|
|
standardItems = await response.json();
|
|
}
|
|
|
|
async function fetchItems() {
|
|
const response = await fetch('/api/items', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
const data = await response.json();
|
|
currentShoppingList = data.items;
|
|
itemList.innerHTML = '';
|
|
data.items.forEach(item => {
|
|
const li = document.createElement('li');
|
|
li.className = 'list-group-item';
|
|
li.textContent = item.name;
|
|
|
|
if (currentUser.is_admin) {
|
|
const deleteBtn = document.createElement('span');
|
|
deleteBtn.textContent = 'X';
|
|
deleteBtn.className = 'delete-btn';
|
|
deleteBtn.onclick = () => deleteItem(item.id);
|
|
li.appendChild(deleteBtn);
|
|
}
|
|
|
|
itemList.appendChild(li);
|
|
});
|
|
handleInputChange();
|
|
}
|
|
|
|
async function addItemsFromInput(inputString, split) {
|
|
if (!inputString.trim()) return;
|
|
|
|
let itemsToAdd = [];
|
|
if (split) {
|
|
itemsToAdd = inputString.split(/[ ,]+/).map(item => item.trim()).filter(item => item);
|
|
} else {
|
|
itemsToAdd.push(inputString.trim());
|
|
}
|
|
|
|
if (itemsToAdd.length === 0) return;
|
|
|
|
const response = await fetch('/api/items', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ names: itemsToAdd })
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
const result = await response.json();
|
|
if (result.status === 'exists' && result.added === 0) {
|
|
console.log('All items already exist.');
|
|
}
|
|
|
|
await fetchItems();
|
|
handleInputChange();
|
|
}
|
|
|
|
async function deleteItem(id) {
|
|
const response = await fetch(`/api/items/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (response.status === 401) return handleLogout();
|
|
if (response.status === 403) {
|
|
alert("Only admins can delete items."); // Simple feedback for non-admins
|
|
return;
|
|
}
|
|
await fetchItems();
|
|
}
|
|
|
|
function handleInputChange() {
|
|
const value = itemNameInput.value.trim().toLowerCase();
|
|
const currentShoppingListNames = currentShoppingList.map(item => item.name.toLowerCase());
|
|
|
|
if (value === '') {
|
|
suggestionBox.innerHTML = '';
|
|
suggestionInfo.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
if (suggestionInfo) suggestionInfo.style.display = 'block';
|
|
|
|
const searchTerms = value.split(/[ ,]+/).map(term => term.trim()).filter(term => term);
|
|
const lastTerm = searchTerms[searchTerms.length - 1];
|
|
|
|
if (!lastTerm) {
|
|
suggestionBox.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const newSuggestions = standardItems.filter(item =>
|
|
item.toLowerCase().includes(lastTerm) &&
|
|
!currentShoppingListNames.includes(item.toLowerCase())
|
|
);
|
|
|
|
const existingItems = currentShoppingList
|
|
.filter(item => item.name.toLowerCase().includes(lastTerm))
|
|
.map(item => item.name);
|
|
|
|
const existingStandardSuggestions = standardItems.filter(item =>
|
|
item.toLowerCase().includes(lastTerm) &&
|
|
currentShoppingListNames.includes(item.toLowerCase())
|
|
);
|
|
|
|
const allExistingItems = [...new Set([...existingItems, ...existingStandardSuggestions])];
|
|
|
|
displaySuggestions(allExistingItems, newSuggestions);
|
|
}
|
|
|
|
function displaySuggestions(existing, news) {
|
|
suggestionBox.innerHTML = '';
|
|
|
|
existing.slice(0, 10).forEach(suggestion => {
|
|
const btn = createSuggestionButton(suggestion, true);
|
|
suggestionBox.appendChild(btn);
|
|
});
|
|
|
|
news.slice(0, 10).forEach(suggestion => {
|
|
const btn = createSuggestionButton(suggestion, false);
|
|
suggestionBox.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
function createSuggestionButton(suggestion, isExisting) {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = isExisting
|
|
? 'btn btn-primary btn-sm suggestion-btn'
|
|
: 'btn btn-outline-primary btn-sm suggestion-btn';
|
|
btn.textContent = suggestion;
|
|
btn.disabled = isExisting;
|
|
if (!isExisting) {
|
|
btn.onclick = () => onSuggestionClick(suggestion);
|
|
}
|
|
return btn;
|
|
}
|
|
|
|
async function onSuggestionClick(suggestion) {
|
|
const value = itemNameInput.value;
|
|
await addItemsFromInput(suggestion, false);
|
|
const lastSeparatorIndex = Math.max(value.lastIndexOf(','), value.lastIndexOf(' '));
|
|
let newValue = '';
|
|
if (lastSeparatorIndex !== -1) {
|
|
newValue = value.substring(0, lastSeparatorIndex + 1);
|
|
}
|
|
itemNameInput.value = newValue;
|
|
suggestionBox.innerHTML = '';
|
|
itemNameInput.focus();
|
|
}
|
|
|
|
async function sendNotification() {
|
|
const lang = navigator.language.startsWith('de') ? 'de' : 'en';
|
|
notifyBtn.disabled = true;
|
|
notifyBtn.textContent = translations.sending_notification || 'Sending...';
|
|
|
|
try {
|
|
const response = await fetch(`/api/notify?lang=${lang}`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (response.status === 401) return handleLogout();
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.status.includes("success")) {
|
|
notifyBtn.textContent = translations.notification_sent || 'Notification sent!';
|
|
} else {
|
|
notifyBtn.textContent = translations.generic_notification_error || 'Error sending notification.';
|
|
}
|
|
} catch (error) {
|
|
notifyBtn.textContent = translations.generic_notification_error || 'Error sending notification.';
|
|
}
|
|
|
|
setTimeout(() => {
|
|
notifyBtn.disabled = false;
|
|
notifyBtn.textContent = translations.notify_button || 'Finalize Changes & Notify';
|
|
}, 3000);
|
|
}
|
|
|
|
async function fetchDbStatus() {
|
|
try {
|
|
const response = await fetch('/api/db-status', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
const data = await response.json();
|
|
if (data.error) {
|
|
statusBar.textContent = (translations.db_error || "Database error: {error}").replace("{error}", data.error);
|
|
} else {
|
|
statusBar.textContent = (translations.db_version || "Database version: {version}").replace("{version}", data.version);
|
|
}
|
|
} catch (error) {
|
|
statusBar.textContent = (translations.db_error || "Database error: {error}").replace("{error}", "Connection failed");
|
|
}
|
|
}
|
|
|
|
// --- Event Listeners ---
|
|
loginForm.addEventListener('submit', handleLogin);
|
|
logoutBtn.addEventListener('click', handleLogout);
|
|
createUserForm.addEventListener('submit', handleCreateUser);
|
|
addItemForm.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
await addItemsFromInput(itemNameInput.value, true);
|
|
itemNameInput.value = '';
|
|
suggestionBox.innerHTML = '';
|
|
});
|
|
|
|
addOneButton.addEventListener('click', async () => {
|
|
await addItemsFromInput(itemNameInput.value, false);
|
|
itemNameInput.value = '';
|
|
suggestionBox.innerHTML = '';
|
|
});
|
|
|
|
itemNameInput.addEventListener('input', handleInputChange);
|
|
|
|
notifyBtn.addEventListener('click', sendNotification);
|
|
|
|
// --- Initialisierung ---
|
|
async function initApp() {
|
|
// NOTE: Translations are now fetched before this function is called.
|
|
const promises = [
|
|
fetchCurrentUser(),
|
|
fetchStandardItems(),
|
|
fetchItems(),
|
|
fetchAndRenderUsers(),
|
|
fetchDbStatus()
|
|
];
|
|
const results = await Promise.allSettled(promises);
|
|
results.forEach((result, i) => {
|
|
if (result.status === 'rejected') {
|
|
console.error(`Error in initApp data fetch step ${i}:`, result.reason);
|
|
}
|
|
});
|
|
|
|
if (suggestionInfo) suggestionInfo.style.display = 'none';
|
|
}
|
|
|
|
|
|
// --- User Management Logic ---
|
|
const userList = document.getElementById('user-list');
|
|
|
|
async function unlockUser(userId, username) {
|
|
// TODO: Add translation for the confirmation message
|
|
if (!confirm(`Are you sure you want to unlock the user "${username}"?`)) {
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/users/${userId}/unlock`, {
|
|
method: 'PUT',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
if (response.ok) {
|
|
await fetchAndRenderUsers();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(`Error: ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function deleteUser(userId, username) {
|
|
if (!confirm(`Are you sure you want to delete the user "${username}"?`)) {
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`/api/users/${userId}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
if (response.ok) {
|
|
await fetchAndRenderUsers();
|
|
} else {
|
|
const data = await response.json();
|
|
alert(`Error: ${data.detail}`);
|
|
}
|
|
}
|
|
|
|
async function fetchAndRenderUsers() {
|
|
if (!userList) return;
|
|
|
|
const response = await fetch('/api/users', { headers: getAuthHeaders() });
|
|
if (response.status === 401) return handleLogout();
|
|
|
|
const users = await response.json();
|
|
userList.innerHTML = '';
|
|
|
|
users.forEach(user => {
|
|
const li = document.createElement('li');
|
|
li.className = 'list-group-item d-flex justify-content-between align-items-center';
|
|
|
|
const userInfo = document.createElement('span');
|
|
userInfo.textContent = `${user.username}`;
|
|
|
|
if (user.is_admin) {
|
|
const adminBadge = document.createElement('span');
|
|
adminBadge.className = 'badge bg-primary rounded-pill ms-2';
|
|
adminBadge.textContent = 'Admin';
|
|
userInfo.appendChild(adminBadge);
|
|
}
|
|
|
|
if (user.is_locked) {
|
|
const lockedBadge = document.createElement('span');
|
|
// TODO: Add translation for "Locked"
|
|
lockedBadge.className = 'badge bg-warning text-dark rounded-pill ms-2';
|
|
lockedBadge.textContent = 'Locked';
|
|
userInfo.appendChild(lockedBadge);
|
|
}
|
|
|
|
li.appendChild(userInfo);
|
|
|
|
const buttonContainer = document.createElement('div');
|
|
|
|
if (currentUser && currentUser.id !== user.id) {
|
|
if (user.is_locked) {
|
|
const unlockBtn = document.createElement('button');
|
|
unlockBtn.className = 'btn btn-success btn-sm';
|
|
// TODO: Add translation for "Unlock"
|
|
unlockBtn.textContent = 'Unlock';
|
|
unlockBtn.onclick = () => unlockUser(user.id, user.username);
|
|
buttonContainer.appendChild(unlockBtn);
|
|
} else {
|
|
const deleteBtn = document.createElement('button');
|
|
deleteBtn.className = 'btn btn-danger btn-sm ms-2';
|
|
deleteBtn.textContent = translations.delete_button || 'Delete';
|
|
deleteBtn.onclick = () => deleteUser(user.id, user.username);
|
|
buttonContainer.appendChild(deleteBtn);
|
|
}
|
|
}
|
|
|
|
li.appendChild(buttonContainer);
|
|
userList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
// --- Change Password Logic ---
|
|
// --- Main Application Entry Point ---
|
|
async function main() {
|
|
// Always fetch translations first, so all UI is translated before showing.
|
|
await fetchTranslations();
|
|
|
|
// Now decide which view to show
|
|
if (localStorage.getItem('access_token')) {
|
|
showAppView();
|
|
} else {
|
|
showLoginView();
|
|
}
|
|
}
|
|
|
|
main(); // Run the app
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
</html>
|