Files
noteshop-webapp/static/index.html
Désiré Werner Menrath 9033ac4054 fix(delete): Ensure delete password prompt shows for all users
The password prompt for deleting items was not appearing for non-admin
users because the backend endpoint to check for the password's
existence was incorrectly restricted to admins, and the frontend logic
did not properly handle the check.

This commit fixes the issue by:
- Allowing all authenticated users to check if a deletion password is set.
- Updating the frontend to correctly show the prompt based on this check.
2025-11-05 12:12:16 +01:00

1233 lines
54 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">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<link rel="icon" type="image/png" href="/favicon.png">
<style>
:root {
--background-gradient-start: #ece9e6;
--background-gradient-end: #ffffff;
--text-color: #212529;
--card-background-color: #ffffff;
--card-shadow-color: rgba(0, 0, 0, 0.05);
--input-background-color: #ffffff;
--input-text-color: #212529;
--status-bar-background-color: #f8f9fa;
--status-bar-border-color: #dee2e6;
--list-group-item-background-color: #ffffff;
}
[data-theme="dark"] {
--background-gradient-start: #212529;
--background-gradient-end: #343a40;
--text-color: #f8f9fa;
--card-background-color: #343a40;
--card-shadow-color: rgba(255, 255, 255, 0.05);
--input-background-color: #495057;
--input-text-color: #f8f9fa;
--status-bar-background-color: #343a40;
--status-bar-border-color: #495057;
--list-group-item-background-color: #343a40;
}
[data-theme="dark"] .text-dark {
color: var(--text-color) !important;
}
[data-theme="dark"] .modal-content {
background-color: var(--card-background-color);
}
[data-theme="dark"] .form-label,
[data-theme="dark"] .card-title,
[data-theme="dark"] #create-user-status,
[data-theme="dark"] #change-password-status,
[data-theme="dark"] #set-deletion-password-status {
color: var(--text-color);
}
[data-theme="dark"] .btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
[data-theme="dark"] .btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
}
[data-theme="dark"] .btn-outline-primary:hover {
background-color: #0d6efd;
color: #fff;
}
[data-theme="dark"] .btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
[data-theme="dark"] .btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
[data-theme="dark"] .btn-outline-secondary:hover {
background-color: #6c757d;
color: #fff;
}
[data-theme="dark"] #admin-panel-title,
[data-theme="dark"] #logout-btn,
[data-theme="dark"] #item-name,
[data-theme="dark"] #info-text-existing,
[data-theme="dark"] #info-text-new {
color: var(--text-color) !important;
}
[data-theme="dark"] #is-admin-label {
color: var(--text-color) !important;
}
[data-theme="dark"] #item-name::placeholder,
[data-theme="dark"] #deletion-password::placeholder {
color: var(--text-color) !important;
opacity: 0.5;
}
[data-theme="dark"] .form-check-input {
background-color: var(--input-background-color);
border-color: var(--status-bar-border-color);
}
[data-theme="dark"] #password-set-message p {
color: var(--text-color);
}
[data-theme="dark"] hr {
border: 0;
border-top: 1px solid var(--status-bar-border-color);
opacity: 1;
}
[data-theme="dark"] #toggle-deletion-password svg {
fill: #fff;
}
body {
background: linear-gradient(to right, var(--background-gradient-start), var(--background-gradient-end));
color: var(--text-color);
}
.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;
background-color: var(--list-group-item-background-color);
color: var(--text-color);
}
.delete-btn {
cursor: pointer;
color: #dc3545;
font-weight: bold;
}
#status-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--status-bar-background-color);
padding: 5px;
text-align: center;
font-size: 12px;
border-top: 1px solid var(--status-bar-border-color);
}
#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 */
}
.card {
background-color: var(--card-background-color);
box-shadow: 0 0.125rem 0.25rem var(--card-shadow-color);
}
.form-control {
background-color: var(--input-background-color);
color: var(--input-text-color);
}
.form-control:focus {
background-color: var(--input-background-color);
color: var(--input-text-color);
}
</style>
</head>
<body class="pb-5">
<script>
(function() {
const theme = localStorage.getItem('theme');
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
} else {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}
})();
</script>
<!-- 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>
<div class="d-flex align-items-center">
<button id="delete-marked-btn" class="btn btn-sm btn-outline-danger me-2" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
<button id="theme-switcher" class="btn btn-sm btn-outline-secondary me-2">☀️</button>
<button id="logout-btn" class="btn btn-sm btn-outline-secondary">Logout</button>
</div>
</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="row g-2 mb-3">
<div class="col">
<div class="d-grid">
<button id="notify-btn" class="btn btn-success"></button>
</div>
</div>
<div class="col-auto">
<div class="d-grid">
<button id="select-all-btn" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-square" viewBox="0 0 16 16">
<path d="M3 14.5A1.5 1.5 0 0 1 1.5 13V3A1.5 1.5 0 0 1 3 1.5h8A1.5 1.5 0 0 1 12.5 3v1.5a.5.5 0 0 1-1 0V3a.5.5 0 0 0-.5-.5H3a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V10a.5.5 0 0 1 1 0v3a1.5 1.5 0 0 1-1.5 1.5H3z"/>
<path d="m8.354 10.354 7-7a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0z"/>
</svg>
</button>
</div>
</div>
</div>
<ul id="item-list" class="list-group shadow-sm"></ul>
</div>
<!-- Admin Section -->
<div id="admin-section" class="card shadow-sm mt-3" 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>
<hr class="my-4">
<h5 id="set-deletion-password-title" class="card-title"></h5>
<div id="password-set-message" style="display: none;">
<p>Passwort ist gesetzt.</p>
<button id="change-password-btn" class="btn btn-primary">Passwort ändern</button>
<button id="delete-password-btn" class="btn btn-danger">Passwort löschen</button>
</div>
<form id="set-deletion-password-form">
<div class="row">
<div class="col-md-6 mb-3">
<div class="input-group">
<input type="password" id="deletion-password" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" id="toggle-deletion-password">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.12 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16" style="display: none;">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.94 5.94 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.707z"/>
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822zm-2.943-1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822zm-2.943-1.288.822.822.073.073.026.026a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829l.822.822z"/>
<path d="M.5 8a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z"/>
</svg>
</button>
</div>
</div>
</div>
<button id="set-deletion-password-button" type="submit" class="btn btn-primary"></button>
<div id="set-deletion-password-status" class="mt-2"></div>
</form>
</div>
</div>
</div>
<div id="status-bar"></div>
<!-- Deletion Password Modal -->
<div class="modal fade" id="delete-password-modal" tabindex="-1" aria-labelledby="delete-password-modal-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="delete-password-modal-label"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="delete-password-form">
<div class="mb-3">
<label id="delete-password-label" for="delete-password-input" class="form-label"></label>
<input type="password" class="form-control" id="delete-password-input" required>
</div>
<div id="delete-password-error" class="text-danger"></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>
<button type="button" id="confirm-delete-btn" class="btn btn-danger"></button>
</div>
</div>
</div>
</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');
const deleteMarkedBtn = document.getElementById('delete-marked-btn');
const deletePasswordModal = new bootstrap.Modal(document.getElementById('delete-password-modal'));
const deletePasswordForm = document.getElementById('delete-password-form');
const deletePasswordInput = document.getElementById('delete-password-input');
const deletePasswordError = document.getElementById('delete-password-error');
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
const setDeletionPasswordForm = document.getElementById('set-deletion-password-form');
const deletionPasswordInput = document.getElementById('deletion-password');
const setDeletionPasswordStatus = document.getElementById('set-deletion-password-status');
const selectAllBtn = document.getElementById('select-all-btn');
let standardItems = [];
let translations = {};
let currentShoppingList = [];
let currentUser = {};
let socket;
let isDeletionPasswordSet = false; // New global variable
// --- 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 = document.documentElement.lang;
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;
document.getElementById('set-deletion-password-title').textContent = translations.set_deletion_password_title;
document.getElementById('deletion-password').placeholder = translations.deletion_password_placeholder;
document.getElementById('set-deletion-password-button').textContent = translations.set_deletion_password_button;
}
// Deletion Modal
if (document.getElementById('delete-password-modal-label')) {
document.getElementById('delete-password-modal-label').textContent = translations.delete_password_modal_title;
document.getElementById('delete-password-label').textContent = translations.delete_password_label;
document.querySelector('#delete-password-modal .btn-secondary').textContent = translations.cancel_button;
document.getElementById('confirm-delete-btn').textContent = translations.delete_button;
}
// Common elements
if (document.getElementById('logout-btn')) {
document.getElementById('logout-btn').textContent = translations.logout_button;
}
}
async function fetchStandardItems() {
const lang = document.documentElement.lang;
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 = '';
let markedItemsCount = 0;
data.items.forEach(item => {
const li = document.createElement('li');
li.className = 'list-group-item';
const itemName = document.createElement('span');
itemName.textContent = item.name;
li.appendChild(itemName);
const switchContainer = document.createElement('div');
switchContainer.className = 'form-check form-switch';
const switchInput = document.createElement('input');
switchInput.className = 'form-check-input';
switchInput.type = 'checkbox';
switchInput.checked = item.marked;
switchInput.addEventListener('change', () => toggleMarkItem(item.id));
switchContainer.appendChild(switchInput);
li.appendChild(switchContainer);
if (item.marked) {
markedItemsCount++;
}
itemList.appendChild(li);
});
if (markedItemsCount > 0) {
deleteMarkedBtn.style.display = 'block';
} else {
deleteMarkedBtn.style.display = 'none';
}
handleInputChange();
}
async function toggleMarkItem(id) {
const response = await fetch(`/api/items/${id}/mark`, {
method: 'PUT',
headers: getAuthHeaders()
});
if (response.status === 401) return handleLogout();
await fetchItems();
}
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 = '';
suggestionInfo.style.display = 'none';
itemNameInput.focus();
}
async function sendNotification() {
const lang = document.documentElement.lang;
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");
}
}
// New function to call the delete marked items API
async function callDeleteMarkedItemsApi(password) {
const response = await fetch('/api/items/delete-marked', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ password: password })
});
if (response.status === 401) {
const errorData = await response.json(); // Get the error detail from the response
if (errorData.detail === "Incorrect password") {
deletePasswordError.textContent = translations.incorrect_password;
} else {
handleLogout(); // This is a session issue
}
} else if (response.ok) {
deletePasswordModal.hide();
await fetchItems();
} else {
deletePasswordError.textContent = translations.generic_error;
}
}
deleteMarkedBtn.addEventListener('click', () => {
if (isDeletionPasswordSet) {
deletePasswordModal.show();
} else {
// If no password is set, delete directly without showing the modal.
callDeleteMarkedItemsApi('');
}
});
deletePasswordForm.addEventListener('submit', (event) => {
event.preventDefault();
confirmDeleteBtn.click();
});
confirmDeleteBtn.addEventListener('click', async () => {
const password = deletePasswordInput.value;
// Always send the password (can be empty) to the backend.
// The backend will determine if a password is required.
await callDeleteMarkedItemsApi(password);
});
// Clear password input on modal close
deletePasswordModal._element.addEventListener('hidden.bs.modal', () => {
deletePasswordInput.value = '';
deletePasswordError.textContent = '';
});
async function getDeletionPassword() {
const response = await fetch('/api/settings/deletion-password', { headers: getAuthHeaders() });
if (response.status === 401) return handleLogout();
const data = await response.json();
isDeletionPasswordSet = data.is_set; // Update the global variable
if (data.is_set) {
document.getElementById('password-set-message').style.display = 'block';
document.getElementById('set-deletion-password-form').style.display = 'none';
} else {
document.getElementById('password-set-message').style.display = 'none';
document.getElementById('set-deletion-password-form').style.display = 'block';
}
}
document.getElementById('change-password-btn').addEventListener('click', () => {
document.getElementById('password-set-message').style.display = 'none';
document.getElementById('set-deletion-password-form').style.display = 'block';
});
document.getElementById('delete-password-btn').addEventListener('click', async () => {
const response = await fetch('/api/settings/deletion-password', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ password: '' })
});
if (response.ok) {
await getDeletionPassword();
}
});
async function setDeletionPassword(event) {
event.preventDefault();
const password = deletionPasswordInput.value;
const response = await fetch('/api/settings/deletion-password', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ password: password })
});
if (response.status === 401) return handleLogout();
if (response.ok) {
setDeletionPasswordStatus.textContent = translations.password_set_success;
setDeletionPasswordStatus.className = 'mt-2 text-success';
await getDeletionPassword();
} else {
const data = await response.json();
setDeletionPasswordStatus.textContent = `Error: ${data.detail}`;
setDeletionPasswordStatus.className = 'mt-2 text-danger';
}
}
async function selectAllItems() {
const allMarked = currentShoppingList.every(item => item.marked);
for (const item of currentShoppingList) {
if (allMarked || !item.marked) {
await toggleMarkItem(item.id);
}
}
}
// --- 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 = '';
suggestionInfo.style.display = 'none';
});
addOneButton.addEventListener('click', async () => {
await addItemsFromInput(itemNameInput.value, false);
itemNameInput.value = '';
suggestionBox.innerHTML = '';
suggestionInfo.style.display = 'none';
});
itemNameInput.addEventListener('input', handleInputChange);
notifyBtn.addEventListener('click', sendNotification);
selectAllBtn.addEventListener('click', selectAllItems);
setDeletionPasswordForm.addEventListener('submit', setDeletionPassword);
const toggleDeletionPassword = document.getElementById('toggle-deletion-password');
toggleDeletionPassword.addEventListener('click', () => {
const passwordInput = document.getElementById('deletion-password');
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
const eyeIcon = toggleDeletionPassword.querySelector('.bi-eye');
const eyeSlashIcon = toggleDeletionPassword.querySelector('.bi-eye-slash');
if (type === 'password') {
eyeIcon.style.display = 'block';
eyeSlashIcon.style.display = 'none';
} else {
eyeIcon.style.display = 'none';
eyeSlashIcon.style.display = 'block';
}
});
// --- Initialisierung ---
async function initApp() {
// NOTE: Translations are now fetched before this function is called.
await fetchCurrentUser();
const promises = [
fetchStandardItems(),
fetchItems(),
fetchDbStatus(),
getDeletionPassword()
];
if (currentUser.is_admin) {
promises.push(fetchAndRenderUsers());
}
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 || !currentUser.is_admin) return;
const response = await fetch('/api/users', { headers: getAuthHeaders() });
if (response.status === 401) return handleLogout();
if (response.status === 403) return; // Non-admin users will get a 403 error
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
const themeSwitcher = document.getElementById('theme-switcher');
themeSwitcher.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
themeSwitcher.textContent = newTheme === 'dark' ? '☀️' : '🌙';
});
// Set initial theme switcher icon
const currentTheme = document.documentElement.getAttribute('data-theme');
if (currentTheme === 'dark') {
themeSwitcher.textContent = '☀️';
} else {
themeSwitcher.textContent = '🌙';
}
</script>
</body>
</html>