initial upload
This commit is contained in:
330
index.html
Normal file
330
index.html
Normal file
@ -0,0 +1,330 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Questionnaire Database</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 20px; }
|
||||
.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:hover, a.button:hover { background:#f7f7f7; }
|
||||
.muted { color:#666; }
|
||||
|
||||
/* Nur der Tabellenbereich scrollt vertikal */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
/* 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;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Questionnaire Database</h1>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
<p id="loginMsg"></p>
|
||||
</div>
|
||||
|
||||
<div id="appCard" class="card hidden">
|
||||
<div class="actions">
|
||||
<span>Signed in as <b id="who"></b>.</span>
|
||||
<button id="logoutBtn">Logout</button>
|
||||
<button id="exportSelectedBtn">Download</button>
|
||||
<span id="selInfo" class="muted">(0 selected)</span>
|
||||
|
||||
<div class="find-wrap">
|
||||
<label for="findClient">Find client:</label>
|
||||
<input id="findClient" type="text" list="clientList" placeholder="Client code" />
|
||||
<datalist id="clientList"></datalist>
|
||||
<button id="findBtn" type="button">Go</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" id="tableWrap">
|
||||
<div id="dbContainer"><em>Loading…</em></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = sel => document.querySelector(sel);
|
||||
|
||||
function setLoggedIn(user, token) {
|
||||
localStorage.setItem('qdb_user', user);
|
||||
localStorage.setItem('qdb_token', token);
|
||||
$('#who').textContent = user;
|
||||
$('#loginCard').classList.add('hidden');
|
||||
$('#appCard').classList.remove('hidden');
|
||||
loadDbView();
|
||||
}
|
||||
|
||||
function setLoggedOut() {
|
||||
localStorage.removeItem('qdb_user');
|
||||
localStorage.removeItem('qdb_token');
|
||||
$('#dbContainer').innerHTML = '';
|
||||
$('#appCard').classList.add('hidden');
|
||||
$('#loginCard').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function login(evt) {
|
||||
evt.preventDefault();
|
||||
$('#loginMsg').textContent = 'Signing in…';
|
||||
try {
|
||||
const res = await fetch('login.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: $('#username').value,
|
||||
password: $('#password').value
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) {
|
||||
$('#loginMsg').textContent = data.message || 'Login failed';
|
||||
return;
|
||||
}
|
||||
$('#loginMsg').textContent = '';
|
||||
setLoggedIn(data.user, data.token);
|
||||
} catch (e) {
|
||||
$('#loginMsg').textContent = 'Error: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function sizeTableArea() {
|
||||
const wrap = document.getElementById('tableWrap');
|
||||
if (!wrap) return;
|
||||
const rect = wrap.getBoundingClientRect();
|
||||
const top = rect.top;
|
||||
const avail = window.innerHeight - top - 16;
|
||||
wrap.style.maxHeight = (avail > 200 ? avail : 200) + 'px';
|
||||
wrap.style.overflowY = 'auto';
|
||||
}
|
||||
|
||||
async function loadDbView() {
|
||||
const token = localStorage.getItem('qdb_token');
|
||||
if (!token) return setLoggedOut();
|
||||
$('#dbContainer').innerHTML = '<em>Loading…</em>';
|
||||
try {
|
||||
const res = await fetch('db_view_ordered.php', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
setLoggedOut();
|
||||
alert('Session expired or invalid. Please sign in again.');
|
||||
return;
|
||||
}
|
||||
const html = await res.text();
|
||||
$('#dbContainer').innerHTML = html;
|
||||
|
||||
const table = document.querySelector('#qdb-table');
|
||||
|
||||
const selInfo = $('#selInfo');
|
||||
function updateCount(){
|
||||
const n = table.querySelectorAll('tbody .rowchk:checked').length;
|
||||
selInfo.textContent = `(${n} selected)`;
|
||||
}
|
||||
const chkAll = table.querySelector('#chkAll');
|
||||
if (chkAll) {
|
||||
chkAll.addEventListener('change', () => {
|
||||
table.querySelectorAll('tbody .rowchk').forEach(ch => ch.checked = chkAll.checked);
|
||||
updateCount();
|
||||
});
|
||||
}
|
||||
table.querySelectorAll('tbody .rowchk').forEach(ch => ch.addEventListener('change', updateCount));
|
||||
updateCount();
|
||||
|
||||
$('#exportSelectedBtn').onclick = () => exportSelectedToXLSX();
|
||||
|
||||
buildClientSearchList();
|
||||
const findInput = $('#findClient');
|
||||
const findBtn = $('#findBtn');
|
||||
findBtn.onclick = () => gotoClientRow(findInput.value.trim());
|
||||
findInput.addEventListener('keydown', (e) => {
|
||||
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) {
|
||||
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);
|
||||
|
||||
function exportSelectedToXLSX() {
|
||||
const table = document.querySelector('#qdb-table');
|
||||
if (!table) return alert('Table not found.');
|
||||
const headRows = table.tHead.rows;
|
||||
if (headRows.length < 2) return alert('Header rows missing.');
|
||||
|
||||
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);
|
||||
}
|
||||
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]);
|
||||
|
||||
rows.forEach(tr => {
|
||||
const tds = tr.cells;
|
||||
const arr = [];
|
||||
for (let i=1; i<tds.length; i++) arr.push(tds[i].textContent.trim());
|
||||
aoa.push(arr);
|
||||
});
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet(aoa);
|
||||
ws['!cols'] = ids.map(()=>({wch:36}));
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'Headers');
|
||||
XLSX.writeFile(wb, 'SelectedClients.xlsx');
|
||||
}
|
||||
|
||||
/* ======= Suche (nur Client-Code) ======= */
|
||||
function buildClientSearchList() {
|
||||
const table = document.querySelector('#qdb-table');
|
||||
if (!table) return;
|
||||
|
||||
let clientCol = -1;
|
||||
const thead = table.tHead;
|
||||
const rows = thead ? thead.rows : [];
|
||||
if (rows.length >= 2) {
|
||||
const cells2 = Array.from(rows[1].cells).map(c => c.textContent.trim().toLowerCase());
|
||||
clientCol = cells2.findIndex(t => t === 'client code' || t === 'client_code');
|
||||
}
|
||||
if (clientCol < 0 && rows.length >= 1) {
|
||||
const cells1 = Array.from(rows[0].cells).map(c => (c.getAttribute('data-id') || c.textContent).trim().toLowerCase());
|
||||
clientCol = cells1.findIndex(t => t === 'client code' || t === 'client_code');
|
||||
}
|
||||
if (clientCol < 0) return;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 : [];
|
||||
if (rows.length >= 2) {
|
||||
const cells2 = Array.from(rows[1].cells).map(c => c.textContent.trim().toLowerCase());
|
||||
clientCol = cells2.findIndex(t => t === 'client code' || t === 'client_code');
|
||||
}
|
||||
if (clientCol < 0 && rows.length >= 1) {
|
||||
const cells1 = Array.from(rows[0].cells).map(c => (c.getAttribute('data-id') || c.textContent).trim().toLowerCase());
|
||||
clientCol = cells1.findIndex(t => t === 'client code' || t === 'client_code');
|
||||
}
|
||||
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 (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
target.classList.add('row-highlight');
|
||||
setTimeout(() => target.classList.remove('row-highlight'), 1800);
|
||||
} else {
|
||||
alert('No matching client found.');
|
||||
}
|
||||
}
|
||||
/* ======= Ende Suche ======= */
|
||||
|
||||
$('#loginForm').addEventListener('submit', login);
|
||||
$('#logoutBtn').addEventListener('click', setLoggedOut);
|
||||
|
||||
const t = localStorage.getItem('qdb_token');
|
||||
const u = localStorage.getItem('qdb_user');
|
||||
if (t && u) setLoggedIn(u, t);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user