new system prototype
This commit is contained in:
668
index.html
668
index.html
@ -9,66 +9,129 @@
|
||||
.card { border: 1px solid #ddd; border-radius: 12px; padding: 16px; margin-bottom: 16px; width: 100%; box-sizing: border-box; }
|
||||
.hidden { display: none; }
|
||||
.actions { margin: 12px 0; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||
button, a.button { padding:8px 12px; border:1px solid #ccc; border-radius:8px; background:#fff; cursor:pointer; text-decoration:none; }
|
||||
button, a.button { padding:8px 12px; border:1px solid #ccc; border-radius:8px; background:#fff; cursor:pointer; text-decoration:none; font-size:14px; }
|
||||
button:hover, a.button:hover { background:#f7f7f7; }
|
||||
.muted { color:#666; }
|
||||
|
||||
/* Nur der Tabellenbereich scrollt vertikal */
|
||||
.role-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:600; margin-left:6px; }
|
||||
.role-admin { background:#e3f2fd; color:#1565c0; }
|
||||
.role-supervisor { background:#fff3e0; color:#e65100; }
|
||||
.role-coach { background:#e8f5e9; color:#2e7d32; }
|
||||
.table-wrap { overflow-x: auto; border-radius: 8px; }
|
||||
|
||||
table { border-collapse: collapse; width: 100%; margin: 8px 0 24px; }
|
||||
th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
|
||||
|
||||
/* BEIDE Kopfzeilen fixieren */
|
||||
#qdb-table thead tr:nth-child(1) th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
#qdb-table thead tr:nth-child(2) th {
|
||||
position: sticky;
|
||||
top: var(--head1h, 34px); /* wird per JS exakt gesetzt */
|
||||
z-index: 2;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
#qdb-table thead tr:nth-child(1) th { position: sticky; top: 0; z-index: 3; background: #f7f7f7; }
|
||||
#qdb-table thead tr:nth-child(2) th { position: sticky; top: var(--head1h, 34px); z-index: 2; background: #f7f7f7; }
|
||||
.row-highlight { animation: hi 1.2s ease-in-out 0s 1; }
|
||||
@keyframes hi { 0% { background:#fff9c4; } 100% { background:transparent; } }
|
||||
|
||||
.find-wrap { margin-left:auto; display:flex; gap:6px; align-items:center; }
|
||||
.find-wrap input[type="text"] { padding:6px 8px; border:1px solid #ccc; border-radius:8px; min-width:160px; }
|
||||
#qdb-table th:nth-child(3), #qdb-table td:nth-child(3) { max-width:260px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
input[type="text"], input[type="password"], select { padding:8px; border:1px solid #ccc; border-radius:8px; font-size:14px; }
|
||||
.form-group { margin-bottom: 12px; }
|
||||
.form-group label { display:block; margin-bottom:4px; font-weight:500; }
|
||||
.msg { padding:8px 12px; border-radius:6px; margin:8px 0; }
|
||||
.msg-error { background:#ffebee; color:#c62828; }
|
||||
.msg-success { background:#e8f5e9; color:#2e7d32; }
|
||||
|
||||
/* kompaktere „lange“ Spalte (2. Daten-Spalte nach Checkbox) */
|
||||
#qdb-table th:nth-child(3),
|
||||
#qdb-table td:nth-child(3) {
|
||||
max-width: 260px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Assignment panel */
|
||||
.assign-grid { display:grid; grid-template-columns:1fr auto 1fr; gap:12px; align-items:start; margin-top:12px; }
|
||||
.assign-list { border:1px solid #ddd; border-radius:8px; padding:8px; max-height:300px; overflow-y:auto; }
|
||||
.assign-list h4 { margin:0 0 8px; }
|
||||
.assign-item { padding:6px 8px; border-radius:4px; cursor:pointer; display:flex; justify-content:space-between; }
|
||||
.assign-item:hover { background:#f5f5f5; }
|
||||
.assign-item.selected { background:#e3f2fd; }
|
||||
.assign-controls { display:flex; flex-direction:column; gap:8px; justify-content:center; align-items:center; padding-top:40px; }
|
||||
.unassigned { color:#e65100; font-style:italic; }
|
||||
|
||||
/* User management panel */
|
||||
.panel-cols { display:grid; grid-template-columns:1fr 340px; gap:20px; align-items:start; margin-top:12px; }
|
||||
.users-table-wrap { overflow-x:auto; }
|
||||
.users-table-wrap table { margin:0; }
|
||||
.create-user-form { border:1px solid #ddd; border-radius:8px; padding:16px; }
|
||||
.create-user-form h4 { margin:0 0 14px; }
|
||||
.form-row { display:flex; gap:8px; }
|
||||
.form-row .form-group { flex:1; }
|
||||
.btn-danger { background:#fff; border-color:#f44336; color:#c62828; }
|
||||
.btn-danger:hover { background:#ffebee; }
|
||||
.btn-primary { background:#1565c0; border-color:#1565c0; color:#fff; }
|
||||
.btn-primary:hover { background:#1976d2; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Questionnaire Database</h1>
|
||||
|
||||
<!-- LOGIN CARD -->
|
||||
<div id="loginCard" class="card">
|
||||
<h2>Login</h2>
|
||||
<form id="loginForm" autocomplete="on">
|
||||
<label>Username<br><input id="username" required autocomplete="username" /></label><br><br>
|
||||
<label>Password<br><input id="password" type="password" required autocomplete="current-password" /></label><br><br>
|
||||
<button>Sign in</button>
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" type="text" required autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" required autocomplete="current-password" />
|
||||
</div>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
<p id="loginMsg"></p>
|
||||
</div>
|
||||
|
||||
<!-- FORCED PASSWORD CHANGE CARD -->
|
||||
<div id="changePwCard" class="card hidden">
|
||||
<h2>Change Password</h2>
|
||||
<p>You must change your password before continuing.</p>
|
||||
<form id="changePwForm">
|
||||
<div class="form-group">
|
||||
<label for="cpOld">Current password</label>
|
||||
<input id="cpOld" type="password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cpNew">New password (min 6 characters)</label>
|
||||
<input id="cpNew" type="password" required minlength="6" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cpConfirm">Confirm new password</label>
|
||||
<input id="cpConfirm" type="password" required minlength="6" />
|
||||
</div>
|
||||
<button type="submit">Change Password</button>
|
||||
</form>
|
||||
<p id="changePwMsg"></p>
|
||||
</div>
|
||||
|
||||
<!-- VOLUNTARY PASSWORD CHANGE MODAL -->
|
||||
<div id="volChangePwCard" class="card hidden">
|
||||
<h2>Change Password</h2>
|
||||
<form id="volChangePwForm">
|
||||
<div class="form-group">
|
||||
<label for="vcpOld">Current password</label>
|
||||
<input id="vcpOld" type="password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vcpNew">New password (min 6 characters)</label>
|
||||
<input id="vcpNew" type="password" required minlength="6" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vcpConfirm">Confirm new password</label>
|
||||
<input id="vcpConfirm" type="password" required minlength="6" />
|
||||
</div>
|
||||
<button type="submit">Change Password</button>
|
||||
<button type="button" id="vcpCancel">Cancel</button>
|
||||
</form>
|
||||
<p id="volChangePwMsg"></p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN APP CARD -->
|
||||
<div id="appCard" class="card hidden">
|
||||
<div class="actions">
|
||||
<span>Signed in as <b id="who"></b>.</span>
|
||||
<span>Signed in as <b id="who"></b><span id="roleBadge" class="role-badge"></span></span>
|
||||
<button id="logoutBtn">Logout</button>
|
||||
<button id="changePwBtn">Change Password</button>
|
||||
<button id="exportSelectedBtn">Download</button>
|
||||
<span id="selInfo" class="muted">(0 selected)</span>
|
||||
<button id="assignBtn" class="hidden">Manage Assignments</button>
|
||||
<button id="manageUsersBtn" class="hidden">Manage Users</button>
|
||||
|
||||
<div class="find-wrap">
|
||||
<label for="findClient">Find client:</label>
|
||||
@ -79,33 +142,82 @@
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" id="tableWrap">
|
||||
<div id="dbContainer"><em>Loading…</em></div>
|
||||
<div id="dbContainer"><em>Loading...</em></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ASSIGNMENT PANEL -->
|
||||
<div id="assignCard" class="card hidden">
|
||||
<h2>Manage Client Assignments</h2>
|
||||
<button id="assignBackBtn">Back to Data View</button>
|
||||
<div id="assignContent"><em>Loading...</em></div>
|
||||
</div>
|
||||
|
||||
<!-- USER MANAGEMENT PANEL -->
|
||||
<div id="manageUsersCard" class="card hidden">
|
||||
<h2>Manage Users</h2>
|
||||
<button id="manageUsersBackBtn">Back to Data View</button>
|
||||
<div id="manageUsersContent"><em>Loading...</em></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = sel => document.querySelector(sel);
|
||||
let _pendingUser = null;
|
||||
|
||||
function setLoggedIn(user, token) {
|
||||
function roleBadgeClass(role) {
|
||||
if (role === 'admin') return 'role-admin';
|
||||
if (role === 'supervisor') return 'role-supervisor';
|
||||
return 'role-coach';
|
||||
}
|
||||
|
||||
function setLoggedIn(user, token, role) {
|
||||
localStorage.setItem('qdb_user', user);
|
||||
localStorage.setItem('qdb_token', token);
|
||||
localStorage.setItem('qdb_role', role || '');
|
||||
$('#who').textContent = user;
|
||||
const badge = $('#roleBadge');
|
||||
badge.textContent = (role || '').charAt(0).toUpperCase() + (role || '').slice(1);
|
||||
badge.className = 'role-badge ' + roleBadgeClass(role);
|
||||
$('#loginCard').classList.add('hidden');
|
||||
$('#changePwCard').classList.add('hidden');
|
||||
$('#volChangePwCard').classList.add('hidden');
|
||||
$('#assignCard').classList.add('hidden');
|
||||
$('#manageUsersCard').classList.add('hidden');
|
||||
$('#appCard').classList.remove('hidden');
|
||||
|
||||
if (role === 'admin' || role === 'supervisor') {
|
||||
$('#assignBtn').classList.remove('hidden');
|
||||
} else {
|
||||
$('#assignBtn').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (role === 'admin') {
|
||||
$('#manageUsersBtn').classList.remove('hidden');
|
||||
} else {
|
||||
$('#manageUsersBtn').classList.add('hidden');
|
||||
}
|
||||
|
||||
loadDbView();
|
||||
}
|
||||
|
||||
function setLoggedOut() {
|
||||
localStorage.removeItem('qdb_user');
|
||||
localStorage.removeItem('qdb_token');
|
||||
localStorage.removeItem('qdb_role');
|
||||
_pendingUser = null;
|
||||
$('#dbContainer').innerHTML = '';
|
||||
$('#appCard').classList.add('hidden');
|
||||
$('#changePwCard').classList.add('hidden');
|
||||
$('#volChangePwCard').classList.add('hidden');
|
||||
$('#assignCard').classList.add('hidden');
|
||||
$('#manageUsersCard').classList.add('hidden');
|
||||
$('#loginCard').classList.remove('hidden');
|
||||
}
|
||||
|
||||
/* ---------- LOGIN ---------- */
|
||||
async function login(evt) {
|
||||
evt.preventDefault();
|
||||
$('#loginMsg').textContent = 'Signing in…';
|
||||
$('#loginMsg').textContent = 'Signing in...';
|
||||
try {
|
||||
const res = await fetch('login.php', {
|
||||
method: 'POST',
|
||||
@ -120,19 +232,114 @@ async function login(evt) {
|
||||
$('#loginMsg').textContent = data.message || 'Login failed';
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.must_change_password) {
|
||||
_pendingUser = data.user;
|
||||
$('#loginCard').classList.add('hidden');
|
||||
$('#changePwCard').classList.remove('hidden');
|
||||
$('#changePwMsg').textContent = '';
|
||||
$('#cpOld').value = $('#password').value;
|
||||
$('#cpNew').value = '';
|
||||
$('#cpConfirm').value = '';
|
||||
$('#cpNew').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#loginMsg').textContent = '';
|
||||
setLoggedIn(data.user, data.token);
|
||||
setLoggedIn(data.user, data.token, data.role);
|
||||
} catch (e) {
|
||||
$('#loginMsg').textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- FORCED PASSWORD CHANGE ---------- */
|
||||
async function handleForcedPwChange(evt) {
|
||||
evt.preventDefault();
|
||||
const msg = $('#changePwMsg');
|
||||
const newPw = $('#cpNew').value;
|
||||
const confirm = $('#cpConfirm').value;
|
||||
|
||||
if (newPw.length < 6) { msg.textContent = 'New password must be at least 6 characters.'; return; }
|
||||
if (newPw !== confirm) { msg.textContent = 'Passwords do not match.'; return; }
|
||||
|
||||
msg.textContent = 'Changing password...';
|
||||
try {
|
||||
const res = await fetch('change_password.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: _pendingUser,
|
||||
old_password: $('#cpOld').value,
|
||||
new_password: newPw
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) {
|
||||
msg.textContent = data.message || 'Password change failed';
|
||||
return;
|
||||
}
|
||||
setLoggedIn(data.user, data.token, data.role);
|
||||
} catch (e) {
|
||||
msg.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- VOLUNTARY PASSWORD CHANGE ---------- */
|
||||
function showVoluntaryPwChange() {
|
||||
$('#appCard').classList.add('hidden');
|
||||
$('#volChangePwCard').classList.remove('hidden');
|
||||
$('#volChangePwMsg').textContent = '';
|
||||
$('#vcpOld').value = '';
|
||||
$('#vcpNew').value = '';
|
||||
$('#vcpConfirm').value = '';
|
||||
$('#vcpOld').focus();
|
||||
}
|
||||
|
||||
function hideVoluntaryPwChange() {
|
||||
$('#volChangePwCard').classList.add('hidden');
|
||||
$('#appCard').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function handleVoluntaryPwChange(evt) {
|
||||
evt.preventDefault();
|
||||
const msg = $('#volChangePwMsg');
|
||||
const newPw = $('#vcpNew').value;
|
||||
const confirm = $('#vcpConfirm').value;
|
||||
|
||||
if (newPw.length < 6) { msg.textContent = 'New password must be at least 6 characters.'; return; }
|
||||
if (newPw !== confirm) { msg.textContent = 'Passwords do not match.'; return; }
|
||||
|
||||
msg.textContent = 'Changing password...';
|
||||
try {
|
||||
const res = await fetch('change_password.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: localStorage.getItem('qdb_user'),
|
||||
old_password: $('#vcpOld').value,
|
||||
new_password: newPw
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) {
|
||||
msg.textContent = data.message || 'Password change failed';
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('qdb_token', data.token);
|
||||
msg.textContent = '';
|
||||
hideVoluntaryPwChange();
|
||||
alert('Password changed successfully.');
|
||||
} catch (e) {
|
||||
msg.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- DB VIEW ---------- */
|
||||
function sizeTableArea() {
|
||||
const wrap = document.getElementById('tableWrap');
|
||||
if (!wrap) return;
|
||||
const rect = wrap.getBoundingClientRect();
|
||||
const top = rect.top;
|
||||
const avail = window.innerHeight - top - 16;
|
||||
const avail = window.innerHeight - rect.top - 16;
|
||||
wrap.style.maxHeight = (avail > 200 ? avail : 200) + 'px';
|
||||
wrap.style.overflowY = 'auto';
|
||||
}
|
||||
@ -140,7 +347,7 @@ function sizeTableArea() {
|
||||
async function loadDbView() {
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
if (!token) return setLoggedOut();
|
||||
$('#dbContainer').innerHTML = '<em>Loading…</em>';
|
||||
$('#dbContainer').innerHTML = '<em>Loading...</em>';
|
||||
try {
|
||||
const res = await fetch('db_view_ordered.php', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
@ -154,6 +361,7 @@ async function loadDbView() {
|
||||
$('#dbContainer').innerHTML = html;
|
||||
|
||||
const table = document.querySelector('#qdb-table');
|
||||
if (!table) return;
|
||||
|
||||
const selInfo = $('#selInfo');
|
||||
function updateCount(){
|
||||
@ -171,8 +379,8 @@ async function loadDbView() {
|
||||
updateCount();
|
||||
|
||||
$('#exportSelectedBtn').onclick = () => exportSelectedToXLSX();
|
||||
|
||||
buildClientSearchList();
|
||||
|
||||
const findInput = $('#findClient');
|
||||
const findBtn = $('#findBtn');
|
||||
findBtn.onclick = () => gotoClientRow(findInput.value.trim());
|
||||
@ -180,21 +388,18 @@ async function loadDbView() {
|
||||
if (e.key === 'Enter') { e.preventDefault(); findBtn.click(); }
|
||||
});
|
||||
|
||||
/* Höhe der 1. Kopfzeile messen und als CSS-Variable setzen,
|
||||
damit die 2. Kopfzeile darunter sticky stehen bleibt */
|
||||
if (table && table.tHead && table.tHead.rows.length >= 1) {
|
||||
if (table.tHead && table.tHead.rows.length >= 1) {
|
||||
const h1 = table.tHead.rows[0].getBoundingClientRect().height || 34;
|
||||
table.style.setProperty('--head1h', `${Math.round(h1)}px`);
|
||||
}
|
||||
|
||||
sizeTableArea();
|
||||
} catch (e) {
|
||||
$('#dbContainer').innerHTML = '<b>Error:</b> ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', sizeTableArea);
|
||||
|
||||
/* ---------- EXPORT ---------- */
|
||||
function exportSelectedToXLSX() {
|
||||
const table = document.querySelector('#qdb-table');
|
||||
if (!table) return alert('Table not found.');
|
||||
@ -204,29 +409,20 @@ function exportSelectedToXLSX() {
|
||||
const ids = [];
|
||||
for (let i=1; i<headRows[0].cells.length; i++) {
|
||||
const th = headRows[0].cells[i];
|
||||
const id = th.getAttribute('data-id') || th.textContent.trim();
|
||||
ids.push(id);
|
||||
ids.push(th.getAttribute('data-id') || th.textContent.trim());
|
||||
}
|
||||
const labels = [];
|
||||
for (let i=1; i<headRows[1].cells.length; i++) {
|
||||
labels.push(headRows[1].cells[i].textContent.trim());
|
||||
}
|
||||
|
||||
const rows = Array.from(table.tBodies[0].rows)
|
||||
.filter(tr => tr.querySelector('.rowchk')?.checked);
|
||||
if (rows.length === 0) {
|
||||
alert('Please select at least one row.');
|
||||
return;
|
||||
}
|
||||
|
||||
const aoa = [];
|
||||
aoa.push([...ids]);
|
||||
aoa.push([...labels]);
|
||||
const rows = Array.from(table.tBodies[0].rows).filter(tr => tr.querySelector('.rowchk')?.checked);
|
||||
if (rows.length === 0) { alert('Please select at least one row.'); return; }
|
||||
|
||||
const aoa = [[...ids], [...labels]];
|
||||
rows.forEach(tr => {
|
||||
const tds = tr.cells;
|
||||
const arr = [];
|
||||
for (let i=1; i<tds.length; i++) arr.push(tds[i].textContent.trim());
|
||||
for (let i=1; i<tr.cells.length; i++) arr.push(tr.cells[i].textContent.trim());
|
||||
aoa.push(arr);
|
||||
});
|
||||
|
||||
@ -237,11 +433,10 @@ function exportSelectedToXLSX() {
|
||||
XLSX.writeFile(wb, 'SelectedClients.xlsx');
|
||||
}
|
||||
|
||||
/* ======= Suche (nur Client-Code) ======= */
|
||||
/* ---------- CLIENT SEARCH ---------- */
|
||||
function buildClientSearchList() {
|
||||
const table = document.querySelector('#qdb-table');
|
||||
if (!table) return;
|
||||
|
||||
let clientCol = -1;
|
||||
const thead = table.tHead;
|
||||
const rows = thead ? thead.rows : [];
|
||||
@ -257,28 +452,19 @@ function buildClientSearchList() {
|
||||
|
||||
const dl = document.querySelector('#clientList');
|
||||
dl.innerHTML = '';
|
||||
|
||||
const codes = new Set();
|
||||
table.tBodies[0].querySelectorAll('tr').forEach(tr => {
|
||||
const td = tr.cells[clientCol];
|
||||
if (!td) return;
|
||||
const val = td.textContent.trim();
|
||||
if (val) codes.add(val);
|
||||
if (td) { const val = td.textContent.trim(); if (val) codes.add(val); }
|
||||
});
|
||||
|
||||
Array.from(codes).sort((a,b)=> (''+a).localeCompare(''+b, undefined, {numeric:true}))
|
||||
.forEach(code => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = code;
|
||||
dl.appendChild(opt);
|
||||
});
|
||||
Array.from(codes).sort((a,b)=> a.localeCompare(b, undefined, {numeric:true}))
|
||||
.forEach(code => { const opt = document.createElement('option'); opt.value = code; dl.appendChild(opt); });
|
||||
}
|
||||
|
||||
function gotoClientRow(query) {
|
||||
if (!query) return;
|
||||
const table = document.querySelector('#qdb-table');
|
||||
if (!table) return;
|
||||
|
||||
let clientCol = -1;
|
||||
const thead = table.tHead;
|
||||
const rows = thead ? thead.rows : [];
|
||||
@ -293,22 +479,17 @@ function gotoClientRow(query) {
|
||||
if (clientCol < 0) return;
|
||||
|
||||
const tryCols = [clientCol, clientCol + 1, Math.max(0, clientCol - 1)];
|
||||
|
||||
const bodyRows = Array.from(table.tBodies[0].rows);
|
||||
let target = null;
|
||||
|
||||
outer:
|
||||
for (const tr of bodyRows) {
|
||||
for (const col of tryCols) {
|
||||
const txt = (tr.cells[col]?.textContent || '').trim();
|
||||
if (!txt) continue;
|
||||
if (txt === query || txt.toLowerCase().includes(query.toLowerCase())) {
|
||||
target = tr;
|
||||
break outer;
|
||||
if (txt && (txt === query || txt.toLowerCase().includes(query.toLowerCase()))) {
|
||||
target = tr; break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
target.classList.add('row-highlight');
|
||||
@ -317,14 +498,337 @@ function gotoClientRow(query) {
|
||||
alert('No matching client found.');
|
||||
}
|
||||
}
|
||||
/* ======= Ende Suche ======= */
|
||||
|
||||
/* ---------- ASSIGNMENT PANEL ---------- */
|
||||
async function loadAssignments() {
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
if (!token) return;
|
||||
const content = $('#assignContent');
|
||||
content.innerHTML = '<em>Loading...</em>';
|
||||
try {
|
||||
const res = await fetch('assign_client.php', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
content.innerHTML = '<b>Error:</b> ' + (err.error || res.statusText);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
renderAssignments(data);
|
||||
} catch (e) {
|
||||
content.innerHTML = '<b>Error:</b> ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAssignments(data) {
|
||||
const { coaches, clients } = data;
|
||||
const content = $('#assignContent');
|
||||
|
||||
let html = '<div class="assign-grid">';
|
||||
|
||||
// Left: Clients
|
||||
html += '<div class="assign-list"><h4>Clients</h4>';
|
||||
if (clients.length === 0) {
|
||||
html += '<p class="muted">No clients found.</p>';
|
||||
} else {
|
||||
clients.forEach(c => {
|
||||
const coachLabel = c.coachUsername || c.coachID || '';
|
||||
const cls = coachLabel ? '' : ' unassigned';
|
||||
html += `<div class="assign-item" data-client="${c.clientCode}">
|
||||
<span>${c.clientCode}</span>
|
||||
<span class="muted${cls}">${coachLabel || 'Unassigned'}</span>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// Center: Controls
|
||||
html += '<div class="assign-controls">';
|
||||
html += '<label for="assignCoachSelect">Assign to coach:</label>';
|
||||
html += '<select id="assignCoachSelect">';
|
||||
html += '<option value="">-- Select coach --</option>';
|
||||
coaches.forEach(c => {
|
||||
html += `<option value="${c.coachID}">${c.username} (${c.coachID})</option>`;
|
||||
});
|
||||
html += '</select>';
|
||||
html += '<button id="doAssignBtn">Assign Selected</button>';
|
||||
html += '<p id="assignMsg" class="muted"></p>';
|
||||
html += '</div>';
|
||||
|
||||
// Right: empty placeholder for future use
|
||||
html += '<div></div>';
|
||||
html += '</div>';
|
||||
|
||||
content.innerHTML = html;
|
||||
|
||||
// Selection handling
|
||||
let selectedClients = new Set();
|
||||
content.querySelectorAll('.assign-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const cc = el.dataset.client;
|
||||
if (selectedClients.has(cc)) {
|
||||
selectedClients.delete(cc);
|
||||
el.classList.remove('selected');
|
||||
} else {
|
||||
selectedClients.add(cc);
|
||||
el.classList.add('selected');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#doAssignBtn').addEventListener('click', async () => {
|
||||
const coachID = $('#assignCoachSelect').value;
|
||||
const msg = $('#assignMsg');
|
||||
if (!coachID) { msg.textContent = 'Please select a coach.'; return; }
|
||||
if (selectedClients.size === 0) { msg.textContent = 'Please select at least one client.'; return; }
|
||||
|
||||
msg.textContent = 'Assigning...';
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
try {
|
||||
const res = await fetch('assign_client.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientCodes: Array.from(selectedClients),
|
||||
coachID: coachID
|
||||
})
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok || !result.success) {
|
||||
msg.textContent = result.error || result.message || 'Assignment failed';
|
||||
return;
|
||||
}
|
||||
msg.textContent = 'Assigned successfully!';
|
||||
setTimeout(() => loadAssignments(), 800);
|
||||
} catch (e) {
|
||||
msg.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showAssignPanel() {
|
||||
$('#appCard').classList.add('hidden');
|
||||
$('#assignCard').classList.remove('hidden');
|
||||
loadAssignments();
|
||||
}
|
||||
|
||||
function hideAssignPanel() {
|
||||
$('#assignCard').classList.add('hidden');
|
||||
$('#appCard').classList.remove('hidden');
|
||||
loadDbView();
|
||||
}
|
||||
|
||||
/* ---------- USER MANAGEMENT PANEL ---------- */
|
||||
function showManageUsersPanel() {
|
||||
$('#appCard').classList.add('hidden');
|
||||
$('#manageUsersCard').classList.remove('hidden');
|
||||
loadManageUsers();
|
||||
}
|
||||
|
||||
function hideManageUsersPanel() {
|
||||
$('#manageUsersCard').classList.add('hidden');
|
||||
$('#appCard').classList.remove('hidden');
|
||||
loadDbView();
|
||||
}
|
||||
|
||||
async function loadManageUsers() {
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
if (!token) return;
|
||||
const content = $('#manageUsersContent');
|
||||
content.innerHTML = '<em>Loading...</em>';
|
||||
try {
|
||||
const res = await fetch('manage_users.php', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
content.innerHTML = '<b>Error:</b> ' + (err.error || res.statusText);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
renderManageUsers(data);
|
||||
} catch (e) {
|
||||
content.innerHTML = '<b>Error:</b> ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderManageUsers(data) {
|
||||
const { users, supervisors } = data;
|
||||
const content = $('#manageUsersContent');
|
||||
const me = localStorage.getItem('qdb_user');
|
||||
|
||||
// ------ Users table ------
|
||||
let tableHtml = '<div class="users-table-wrap">';
|
||||
tableHtml += '<table><thead><tr><th>Username</th><th>Role</th><th>Location / Supervisor</th><th>Must change pw?</th><th></th></tr></thead><tbody>';
|
||||
|
||||
if (users.length === 0) {
|
||||
tableHtml += '<tr><td colspan="5"><em class="muted">No users found.</em></td></tr>';
|
||||
} else {
|
||||
users.forEach(u => {
|
||||
const detail = u.role === 'coach'
|
||||
? (u.supervisorUsername ? `${u.supervisorUsername}` : u.supervisorID || '—')
|
||||
: (u.location || '—');
|
||||
const detailLabel = u.role === 'coach' ? 'Supervisor: ' : 'Location: ';
|
||||
const mustChg = u.mustChangePassword == 1 ? '⚠ Yes' : 'No';
|
||||
const isSelf = u.username === me;
|
||||
const delBtn = isSelf
|
||||
? '<em class="muted">you</em>'
|
||||
: `<button class="btn-danger del-user-btn" data-uid="${u.userID}" data-uname="${u.username}">Delete</button>`;
|
||||
tableHtml += `<tr>
|
||||
<td><strong>${u.username}</strong></td>
|
||||
<td><span class="role-badge ${roleBadgeClass(u.role)}">${u.role}</span></td>
|
||||
<td class="muted">${detailLabel}${detail}</td>
|
||||
<td>${mustChg}</td>
|
||||
<td>${delBtn}</td>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
tableHtml += '</tbody></table></div>';
|
||||
|
||||
// ------ Create user form ------
|
||||
let svOptions = '<option value="">-- Select supervisor --</option>';
|
||||
supervisors.forEach(s => {
|
||||
svOptions += `<option value="${s.supervisorID}">${s.username} (${s.location || 'no location'})</option>`;
|
||||
});
|
||||
|
||||
const formHtml = `
|
||||
<div class="create-user-form">
|
||||
<h4>Create New User</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="nu_username" placeholder="username" autocomplete="off" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" id="nu_password" placeholder="min 6 chars" autocomplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Role</label>
|
||||
<select id="nu_role">
|
||||
<option value="">-- Select role --</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="supervisor">Supervisor</option>
|
||||
<option value="coach">Coach</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="nu_location_group" style="display:none">
|
||||
<label>Location <span class="muted">(optional)</span></label>
|
||||
<input type="text" id="nu_location" placeholder="e.g. Berlin" />
|
||||
</div>
|
||||
<div class="form-group" id="nu_supervisor_group" style="display:none">
|
||||
<label>Supervisor</label>
|
||||
<select id="nu_supervisor">${svOptions}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display:flex;gap:8px;align-items:center;cursor:pointer;">
|
||||
<input type="checkbox" id="nu_mustchange" checked />
|
||||
Require password change on first login
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn-primary" id="nu_submitBtn">Create User</button>
|
||||
<p id="nu_msg" class="muted" style="margin-top:8px;"></p>
|
||||
</div>`;
|
||||
|
||||
content.innerHTML = `<div class="panel-cols">${tableHtml}${formHtml}</div>`;
|
||||
|
||||
// Role select show/hide conditional fields
|
||||
const roleSelect = content.querySelector('#nu_role');
|
||||
roleSelect.addEventListener('change', () => {
|
||||
const r = roleSelect.value;
|
||||
content.querySelector('#nu_location_group').style.display = (r === 'admin' || r === 'supervisor') ? '' : 'none';
|
||||
content.querySelector('#nu_supervisor_group').style.display = r === 'coach' ? '' : 'none';
|
||||
});
|
||||
|
||||
// Delete buttons
|
||||
content.querySelectorAll('.del-user-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const uname = btn.dataset.uname;
|
||||
if (!confirm(`Delete user "${uname}"? This cannot be undone.`)) return;
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
try {
|
||||
const res = await fetch('manage_users.php', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ userID: btn.dataset.uid })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok || !result.success) {
|
||||
alert(result.error || 'Delete failed');
|
||||
return;
|
||||
}
|
||||
loadManageUsers();
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Create user submit
|
||||
content.querySelector('#nu_submitBtn').addEventListener('click', async () => {
|
||||
const msg = content.querySelector('#nu_msg');
|
||||
const username = content.querySelector('#nu_username').value.trim();
|
||||
const password = content.querySelector('#nu_password').value;
|
||||
const role = content.querySelector('#nu_role').value;
|
||||
const location = content.querySelector('#nu_location').value.trim();
|
||||
const supervisorID = content.querySelector('#nu_supervisor').value;
|
||||
const mustChange = content.querySelector('#nu_mustchange').checked;
|
||||
|
||||
if (!username) { msg.textContent = 'Username is required.'; return; }
|
||||
if (password.length < 6) { msg.textContent = 'Password must be at least 6 characters.'; return; }
|
||||
if (!role) { msg.textContent = 'Please select a role.'; return; }
|
||||
if (role === 'coach' && !supervisorID) { msg.textContent = 'Please select a supervisor for the coach.'; return; }
|
||||
|
||||
msg.textContent = 'Creating...';
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
try {
|
||||
const res = await fetch('manage_users.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ username, password, role, location, supervisorID, mustChangePassword: mustChange })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok || !result.success) {
|
||||
msg.textContent = result.error || 'Create failed';
|
||||
return;
|
||||
}
|
||||
msg.textContent = '';
|
||||
content.querySelector('#nu_username').value = '';
|
||||
content.querySelector('#nu_password').value = '';
|
||||
content.querySelector('#nu_role').value = '';
|
||||
content.querySelector('#nu_location').value = '';
|
||||
content.querySelector('#nu_supervisor').value = '';
|
||||
content.querySelector('#nu_location_group').style.display = 'none';
|
||||
content.querySelector('#nu_supervisor_group').style.display = 'none';
|
||||
loadManageUsers();
|
||||
} catch (e) {
|
||||
msg.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- EVENT LISTENERS ---------- */
|
||||
$('#loginForm').addEventListener('submit', login);
|
||||
$('#changePwForm').addEventListener('submit', handleForcedPwChange);
|
||||
$('#volChangePwForm').addEventListener('submit', handleVoluntaryPwChange);
|
||||
$('#vcpCancel').addEventListener('click', hideVoluntaryPwChange);
|
||||
$('#logoutBtn').addEventListener('click', setLoggedOut);
|
||||
$('#changePwBtn').addEventListener('click', showVoluntaryPwChange);
|
||||
$('#assignBtn').addEventListener('click', showAssignPanel);
|
||||
$('#assignBackBtn').addEventListener('click', hideAssignPanel);
|
||||
$('#manageUsersBtn').addEventListener('click', showManageUsersPanel);
|
||||
$('#manageUsersBackBtn').addEventListener('click', hideManageUsersPanel);
|
||||
|
||||
/* ---------- AUTO-LOGIN ---------- */
|
||||
const t = localStorage.getItem('qdb_token');
|
||||
const u = localStorage.getItem('qdb_user');
|
||||
if (t && u) setLoggedIn(u, t);
|
||||
const r = localStorage.getItem('qdb_role');
|
||||
if (t && u) setLoggedIn(u, t, r);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user