diff --git a/App/admin-licenses.html b/App/admin-licenses.html index f866ceb..e4af42d 100644 --- a/App/admin-licenses.html +++ b/App/admin-licenses.html @@ -481,7 +481,7 @@ function renderIssueResult(){ + '' + '' + '' - + '
'+(m.count>40?'Große Charge: die Codes gehen automatisch als Datei-Anhang mit.':'Die Codes stehen im Text.')+' Der Text ist auch für den Selbstversand kopierbar.
' + + '
'+(m.count>40?'Große Charge: die Codes gehen automatisch als .txt- und .csv-Anhang mit.':'Die Codes stehen im Text.')+' Der Text ist auch für den Selbstversand kopierbar.
' + '
' + ''; document.getElementById('iss-result').innerHTML = h; diff --git a/App/php/api/licenses.php b/App/php/api/licenses.php index 2dc6000..b25531a 100644 --- a/App/php/api/licenses.php +++ b/App/php/api/licenses.php @@ -215,19 +215,19 @@ if ($method === 'GET') { if (!$class) Response::error('Klasse nicht gefunden', 404); $licenses = $db->fetchAll( - 'SELECT l.id, l.code, l.school_year, l.student_id, l.redeemed_at, + 'SELECT l.id, l.code, l.school_year, l.student_id, l.redeemed_at, l.valid_until, s.username as student_name, s.display_name as student_display FROM licenses l JOIN students s ON s.id = l.student_id WHERE s.class_id = ? - ORDER BY s.username', + ORDER BY l.redeemed_at, l.id', [$classId] ); // Auch freie Lizenzen des Lehrers (noch nicht zugewiesen), storniert ausgeschlossen. // Reihenfolge = id (= Reihenfolge, in der die Codes ausgegeben/reinkopiert wurden). $freeLicenses = $db->fetchAll( - 'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL AND canceled_at IS NULL ORDER BY id', + 'SELECT id, code, school_year, redeemed_at, valid_until FROM licenses WHERE teacher_id = ? AND student_id IS NULL AND canceled_at IS NULL ORDER BY id', [$teacherId] ); @@ -484,7 +484,14 @@ if ($method === 'POST') { $html = Mailer::renderPlainBody($textBody); $attach = []; if (count($codeList) > 40) { - $attach[] = ['content' => implode("\r\n", $codeList) . "\r\n", 'name' => 'lizenzcodes.txt', 'type' => 'text/plain']; + // Reine Code-Liste (eine pro Zeile) als .txt UND .csv anhängen. + // Dateiname wie beim Download: _geograsim_. + $codesTxt = implode("\r\n", $codeList) . "\r\n"; + $bRow = $db->fetchOne("SELECT order_no FROM license_batches WHERE id = ?", [$bid]); + $ord = ($bRow && !empty($bRow['order_no'])) ? preg_replace('/[^A-Za-z0-9_\-]/', '_', $bRow['order_no']) : date('Y-m-d-H-i'); + $fbase = $ord . '_geograsim_' . $bid; + $attach[] = ['content' => $codesTxt, 'name' => $fbase . '.txt', 'type' => 'text/plain']; + $attach[] = ['content' => $codesTxt, 'name' => $fbase . '.csv', 'type' => 'text/csv']; } $db->execute( "INSERT INTO license_mails (batch_id, to_email, subject, body_text, body_html, provider, status, created_by) VALUES (?,?,?,?,?,?,?,?)", diff --git a/App/teacher.html b/App/teacher.html index 999dbbd..f7e0399 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -3872,11 +3872,12 @@ async function loadStudents() { if (students.error) students = []; // Lizenzen laden für Anzeige var licData = await api('licenses?class_id=' + currentClassId); - var assignedMap = {}; - if (licData.assigned) licData.assigned.forEach(function(l) { assignedMap[l.student_id] = l.code; }); + var assignedMap = {}, unlic = {}; + if (licData.assigned) licData.assigned.forEach(function(l) { assignedMap[l.student_id] = l; }); + if (licData.unlicensed) licData.unlicensed.forEach(function(u) { unlic[u.id] = 1; }); - // Count without license - var withoutLicense = students.filter(function(s) { return !assignedMap[s.id]; }).length; + // Count ohne GÜLTIGE Lizenz + var withoutLicense = students.filter(function(s) { return unlic[s.id]; }).length; var html = '
Klasse — ' + esc(currentClass?.name || '') + '
'; html += '
'; @@ -3903,8 +3904,11 @@ async function loadStudents() { } var name = s.display_name || s.username; if (s.first_name || s.last_name) name = [s.first_name, s.last_name].filter(Boolean).join(' '); - var lic = assignedMap[s.id]; - var licHtml = lic ? ''+lic+'' : ''; + var la = assignedMap[s.id]; + var hasValid = !unlic[s.id]; + var licHtml = hasValid + ? '' + : 'fehlt'; var easyChecked = s.easy_language ? ' checked' : ''; var pinHtml; if (s.password_plaintext) { @@ -3915,7 +3919,7 @@ async function loadStudents() { pinHtml = 'selbst gesetzt'; } html += ''; - html += ''; + html += ''; html += ''+emoji+''; html += ''+esc(s.username)+''; html += ''+esc(name)+''; @@ -5441,6 +5445,8 @@ function renderFluggesellschaftRun(dt, dur, r, d) { } // === LIZENZEN === +function fmtLicDate(d) { if (!d) return '—'; var dt = new Date(String(d).replace(' ','T')); return isNaN(dt) ? '—' : dt.toLocaleDateString('de-AT'); } + async function loadLicenses() { var r = await api('licenses?class_id=' + currentClassId); if (r.error) r = {assigned:[], free:[]}; @@ -5474,11 +5480,12 @@ async function loadLicenses() { // Zugewiesene Lizenzen html += '
Zugewiesene Lizenzen
'; if (r.assigned && r.assigned.length > 0) { - html += ''; - r.assigned.forEach(function(l) { - html += ''; + html += '
Schüler*inLizenzcodeSchuljahr
'+esc(l.student_display || l.student_name)+'
'; + r.assigned.forEach(function(l, i) { + html += ''; html += ''; - html += ''; + html += ''; + html += ''; }); html += '
#Schüler*inLizenzcodeEingelöst amGültig bis
'+(i+1)+''+esc(l.student_display || l.student_name)+''+l.code+''+l.school_year+'
'+fmtLicDate(l.redeemed_at)+''+fmtLicDate(l.valid_until)+'
'; } else { @@ -5489,10 +5496,11 @@ async function loadLicenses() { // Freie Lizenzen html += '
Freie Lizenzen (nicht zugewiesen)
'; if (r.free && r.free.length > 0) { - html += '
'; - r.free.forEach(function(l) { - html += ''; - html += ''; + html += '
LizenzcodeSchuljahr
'+l.code+''+l.school_year+'
'; + r.free.forEach(function(l, i) { + html += ''; + html += ''; + html += ''; }); html += '
#LizenzcodeEingelöst amGültig bis
'+(i+1)+''+l.code+''+fmtLicDate(l.redeemed_at)+''+fmtLicDate(l.valid_until)+'
'; } else {