Lizenz-Feinschliff: CSV-Mailanhang, Lehrer-Listenspalten, Klassenliste-Haken
- Mailanhang bei grossen Chargen: zusaetzlich zur .txt jetzt auch .csv (gleiche reine Codeliste), Dateiname <ExtBestellnr|Datum>_geograsim_<intNr>. - Lehrer-Lizenzlisten (zugewiesen + frei): vorne laufende Nr., Eingeloest am, Gueltig bis (Backend liefert redeemed_at + valid_until). - Klassenliste Lizenz-Spalte: nur Haken (gruen) bzw. "fehlt" (rot); Tooltip zeigt Lizenznummer + gueltig bis. "Gueltig" via unlicensed-Liste (nicht storniert/abgelaufen). Bulk-Auswahl/Zaehler nutzen jetzt dieselbe Wahrheit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -481,7 +481,7 @@ function renderIssueResult(){
|
||||
+ '</div>'
|
||||
+ '<label style="margin-top:.5rem;display:block">Mailtext (editierbar)</label>'
|
||||
+ '<textarea id="mail-body" rows="10">'+esc(mailText)+'</textarea>'
|
||||
+ '<div class="hint-s">'+(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.</div>'
|
||||
+ '<div class="hint-s">'+(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.</div>'
|
||||
+ '<div class="row-act" style="margin-top:.4rem"><button class="btn" onclick="sendMail('+m.batch_id+')">✉ Absenden</button><span id="mail-msg" class="mini"></span></div>'
|
||||
+ '</div>';
|
||||
document.getElementById('iss-result').innerHTML = h;
|
||||
|
||||
@@ -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: <ExtBestellnr|Datum>_geograsim_<interneNr>.
|
||||
$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 (?,?,?,?,?,?,?,?)",
|
||||
|
||||
+23
-15
@@ -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 = '<div class="card"><div class="card-h">Klasse — ' + esc(currentClass?.name || '') + '</div>';
|
||||
html += '<div class="toolbar">';
|
||||
@@ -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 ? '<span style="font-family:monospace;font-size:.6rem;color:#5a8a5e">'+lic+'</span>' : '<span style="font-size:.6rem;color:#c07a6b">—</span>';
|
||||
var la = assignedMap[s.id];
|
||||
var hasValid = !unlic[s.id];
|
||||
var licHtml = hasValid
|
||||
? '<span style="color:#2e7d32;font-weight:800;cursor:default" title="'+(la ? ('Lizenz '+la.code+' · gültig bis '+fmtLicDate(la.valid_until)) : 'gültige Lizenz')+'">✓</span>'
|
||||
: '<span style="color:#c0392b;font-weight:700;font-size:.66rem;cursor:default" title="Keine gültige Lizenz">fehlt</span>';
|
||||
var easyChecked = s.easy_language ? ' checked' : '';
|
||||
var pinHtml;
|
||||
if (s.password_plaintext) {
|
||||
@@ -3915,7 +3919,7 @@ async function loadStudents() {
|
||||
pinHtml = '<span style="font-size:.6rem;color:#8a8a8a" title="Passwort wurde von der/dem Schüler*in selbst gesetzt — Lehrkraft kann zurücksetzen">selbst gesetzt</span>';
|
||||
}
|
||||
html += '<tr>';
|
||||
html += '<td><input type="checkbox" class="bulk-cb" value="'+s.id+'" onchange="bulkUpdate()"'+(lic?' disabled title="Hat bereits Lizenz"':'')+'></td>';
|
||||
html += '<td><input type="checkbox" class="bulk-cb" value="'+s.id+'" onchange="bulkUpdate()"'+(hasValid?' disabled title="Hat bereits eine gültige Lizenz"':'')+'></td>';
|
||||
html += '<td style="font-size:1.1rem;text-align:center">'+emoji+'</td>';
|
||||
html += '<td><strong>'+esc(s.username)+'</strong></td>';
|
||||
html += '<td>'+esc(name)+'</td>';
|
||||
@@ -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 += '<div class="card"><div class="card-h">Zugewiesene Lizenzen</div>';
|
||||
if (r.assigned && r.assigned.length > 0) {
|
||||
html += '<table class="students"><tr><th>Schüler*in</th><th>Lizenzcode</th><th>Schuljahr</th></tr>';
|
||||
r.assigned.forEach(function(l) {
|
||||
html += '<tr><td>'+esc(l.student_display || l.student_name)+'</td>';
|
||||
html += '<table class="students"><tr><th>#</th><th>Schüler*in</th><th>Lizenzcode</th><th>Eingelöst am</th><th>Gültig bis</th></tr>';
|
||||
r.assigned.forEach(function(l, i) {
|
||||
html += '<tr><td>'+(i+1)+'</td><td>'+esc(l.student_display || l.student_name)+'</td>';
|
||||
html += '<td style="font-family:\'Courier New\',monospace;font-size:.7rem;letter-spacing:.03em">'+l.code+'</td>';
|
||||
html += '<td>'+l.school_year+'</td></tr>';
|
||||
html += '<td>'+fmtLicDate(l.redeemed_at)+'</td>';
|
||||
html += '<td>'+fmtLicDate(l.valid_until)+'</td></tr>';
|
||||
});
|
||||
html += '</table>';
|
||||
} else {
|
||||
@@ -5489,10 +5496,11 @@ async function loadLicenses() {
|
||||
// Freie Lizenzen
|
||||
html += '<div class="card"><div class="card-h">Freie Lizenzen (nicht zugewiesen)</div>';
|
||||
if (r.free && r.free.length > 0) {
|
||||
html += '<div style="max-height:250px;overflow-y:auto"><table class="students"><tr><th>Lizenzcode</th><th>Schuljahr</th></tr>';
|
||||
r.free.forEach(function(l) {
|
||||
html += '<tr><td style="font-family:\'Courier New\',monospace;font-size:.7rem;letter-spacing:.03em">'+l.code+'</td>';
|
||||
html += '<td>'+l.school_year+'</td></tr>';
|
||||
html += '<div style="max-height:250px;overflow-y:auto"><table class="students"><tr><th>#</th><th>Lizenzcode</th><th>Eingelöst am</th><th>Gültig bis</th></tr>';
|
||||
r.free.forEach(function(l, i) {
|
||||
html += '<tr><td>'+(i+1)+'</td><td style="font-family:\'Courier New\',monospace;font-size:.7rem;letter-spacing:.03em">'+l.code+'</td>';
|
||||
html += '<td>'+fmtLicDate(l.redeemed_at)+'</td>';
|
||||
html += '<td>'+fmtLicDate(l.valid_until)+'</td></tr>';
|
||||
});
|
||||
html += '</table></div>';
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user