16 Commits

Author SHA1 Message Date
ProgrammGamer
0cabca874d Implement iterative minimum participant checks for workshop assignments
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
2026-03-02 13:08:02 +01:00
ProgrammGamer
8a7b0d71ee Enhance workshop assignment logic with minimum participant checks and improved reassignment handling
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 11s
2026-03-02 13:00:40 +01:00
Blitz08
0f5d575cb9 Improve workshop minimum participant handling and reassignment logic
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
2026-03-02 12:49:37 +01:00
90d29cf500 Update KC Admin Button Reihenfolge
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
2026-02-26 22:55:49 +00:00
ProgrammGamer
73ad38423d Implement feature X to enhance user experience and fix bug Y in module Z
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
2026-02-25 18:51:54 +01:00
ProgrammGamer
74fdea3f9e enhance API authentication and error handling
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
2026-02-25 18:42:26 +01:00
ProgrammGamer
508c03a29f refactor API error handling and response structure
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
2026-02-25 18:38:29 +01:00
ProgrammGamer
412caacf9d implement API
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
2026-02-25 18:36:14 +01:00
ProgrammGamer
5e765f0ff5 Finalase Export
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
2026-02-25 18:28:13 +01:00
ProgrammGamer
593c1845c6 update Export
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
2026-02-25 18:20:38 +01:00
ProgrammGamer
ef874cd672 update Wahlen
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
2026-02-25 18:17:30 +01:00
ProgrammGamer
876223400e update Export Feature
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
2026-02-25 18:15:11 +01:00
ProgrammGamer
64c4297e8d update xport
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 14s
2026-02-25 18:11:13 +01:00
ProgrammGamer
a4c5a5dbe9 update Teilnehmer Export
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
2026-02-25 18:07:54 +01:00
ProgrammGamer
c91ff4be78 Create CSV export for Teilnehmr
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 14s
2026-02-25 17:57:58 +01:00
ProgrammGamer
688889251d add markdown files from deployment
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 14s
2026-02-06 19:47:10 +01:00
7 changed files with 1919 additions and 205 deletions

View File

@@ -98,7 +98,6 @@ jobs:
--exclude Notes \ --exclude Notes \
--exclude "*.yml" \ --exclude "*.yml" \
--exclude "*.yaml" \ --exclude "*.yaml" \
--exclude "*.md" \
. .
# Choose target path: prefer absolute plugin path; otherwise derive from HOST_WP_ROOT # Choose target path: prefer absolute plugin path; otherwise derive from HOST_WP_ROOT

View File

@@ -47,3 +47,8 @@ Dieses WordPress-Plugin ermöglicht die Verwaltung und Durchführung von Worksho
--- ---
Für Detailfragen zu Datenbankstruktur, Shortcodes oder Zuteilungslogik siehe die jeweiligen Dateien im `includes/`-Verzeichnis. Für Detailfragen zu Datenbankstruktur, Shortcodes oder Zuteilungslogik siehe die jeweiligen Dateien im `includes/`-Verzeichnis.
## Release Notes
Siehe die ausführlichen Hinweise zur Version 1.0 in [RELEASE_NOTES.md](RELEASE_NOTES.md).

View File

@@ -1,4 +1,103 @@
<?php <?php
// CSV Export Handler: früh ausführen, damit kein HTML in der CSV landet
if (isset($_GET['export_teilnehmer_csv']) && current_user_can('manage_options')) {
global $wpdb;
$prefix = $wpdb->prefix;
$all_wahlen = $wpdb->get_results("SELECT id, name FROM {$prefix}kc_wahlen WHERE deleted=0 ORDER BY id DESC");
$all_workshops = $wpdb->get_results("SELECT id, name FROM {$prefix}kc_workshops ORDER BY name");
$wahl_name_map = [];
if (!empty($all_wahlen)) {
foreach ($all_wahlen as $w) {
$wahl_name_map[intval($w->id)] = $w->name;
}
}
$workshops_map = [];
if (!empty($all_workshops)) {
foreach ($all_workshops as $ws) {
$workshops_map[intval($ws->id)] = $ws->name;
}
}
$export_wahl_id = isset($_GET['wahl_id']) ? intval($_GET['wahl_id']) : 0;
$export_phase = isset($_GET['phase']) ? intval($_GET['phase']) : 0;
$sql = "SELECT * FROM {$prefix}kc_teilnehmer";
if ($export_wahl_id > 0 && $export_phase > 0) {
$sql .= $wpdb->prepare(" WHERE wahl_id=%d AND phase=%d", $export_wahl_id, $export_phase);
} elseif ($export_wahl_id > 0) {
$sql .= $wpdb->prepare(" WHERE wahl_id=%d", $export_wahl_id);
} elseif ($export_phase > 0) {
$sql .= $wpdb->prepare(" WHERE phase=%d", $export_phase);
}
$sql .= " ORDER BY wahl_id, phase, nachname, vorname";
$export_rows = $wpdb->get_results($sql);
$filename_suffix = '_alle';
if ($export_wahl_id > 0) {
$wahl_name_for_file = isset($wahl_name_map[$export_wahl_id]) ? $wahl_name_map[$export_wahl_id] : ('wahl_'.$export_wahl_id);
$wahl_slug = sanitize_title($wahl_name_for_file);
if ($wahl_slug === '') {
$wahl_slug = 'wahl_'.$export_wahl_id;
}
$filename_suffix = '_'.$wahl_slug;
if ($export_phase > 0) {
$filename_suffix .= '_phase_'.$export_phase;
}
} elseif ($export_phase > 0) {
$filename_suffix = '_phase_'.$export_phase;
}
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="kc_teilnehmer'.$filename_suffix.'.csv"');
echo "\xEF\xBB\xBF";
$output = fopen('php://output', 'w');
$delimiter = ';';
fputcsv($output, ['Vorname','Nachname','Wahl','Phase','Wunsch 1','Wunsch 2','Wunsch 3'], $delimiter);
$csv_clean = function($value) {
$text = html_entity_decode((string)$value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$text = wp_strip_all_tags($text, true);
$text = preg_replace('/\s+/u', ' ', $text);
return trim($text);
};
foreach ($export_rows as $tn) {
$w1_id = intval($tn->wunsch1);
$w2_id = intval($tn->wunsch2);
$w3_id = intval($tn->wunsch3);
$w1_disp = $w1_id && isset($workshops_map[$w1_id]) ? $workshops_map[$w1_id] : ($w1_id ? strval($w1_id) : '');
$w2_disp = $w2_id && isset($workshops_map[$w2_id]) ? $workshops_map[$w2_id] : ($w2_id ? strval($w2_id) : '');
$w3_disp = $w3_id && isset($workshops_map[$w3_id]) ? $workshops_map[$w3_id] : ($w3_id ? strval($w3_id) : '');
$wahl_name = isset($wahl_name_map[intval($tn->wahl_id)]) ? $wahl_name_map[intval($tn->wahl_id)] : '';
$vorname = $csv_clean($tn->vorname);
$nachname = $csv_clean($tn->nachname);
$wahl_name_clean = $csv_clean($wahl_name);
$w1_clean = $csv_clean($w1_disp);
$w2_clean = $csv_clean($w2_disp);
$w3_clean = $csv_clean($w3_disp);
fputcsv($output, [
$vorname,
$nachname,
$wahl_name_clean,
intval($tn->phase),
$w1_clean,
$w2_clean,
$w3_clean
], $delimiter);
}
fclose($output);
exit;
}
function kc_teilnehmer_page() { function kc_teilnehmer_page() {
global $wpdb; global $wpdb;
$prefix = $wpdb->prefix; $prefix = $wpdb->prefix;
@@ -7,6 +106,14 @@ function kc_teilnehmer_page() {
$all_wahlen = $wpdb->get_results("SELECT id, name, anzahl_einheiten FROM {$prefix}kc_wahlen WHERE deleted=0 ORDER BY id DESC"); $all_wahlen = $wpdb->get_results("SELECT id, name, anzahl_einheiten FROM {$prefix}kc_wahlen WHERE deleted=0 ORDER BY id DESC");
$all_workshops = $wpdb->get_results("SELECT id, name FROM {$prefix}kc_workshops ORDER BY name"); $all_workshops = $wpdb->get_results("SELECT id, name FROM {$prefix}kc_workshops ORDER BY name");
// Map of wahl id => name for quick lookup
$wahl_name_map = [];
if (!empty($all_wahlen)) {
foreach ($all_wahlen as $w) {
$wahl_name_map[intval($w->id)] = $w->name;
}
}
// Map of workshop id => name for quick lookup in overview // Map of workshop id => name for quick lookup in overview
$workshops_map = []; $workshops_map = [];
if (!empty($all_workshops)) { if (!empty($all_workshops)) {
@@ -222,13 +329,10 @@ function kc_teilnehmer_page() {
} }
// Übersicht // Übersicht
// Map für Wahl-ID => Name // Map für Wahl-ID => Name ist bereits oben aufgebaut
$wahl_name_map = [];
foreach($all_wahlen as $w) {
$wahl_name_map[intval($w->id)] = $w->name;
}
echo '<div class="kc-admin-table-wrap">'; echo '<div class="kc-admin-table-wrap">';
echo '<h2 style="margin-top:0;">Alle Teilnehmer</h2>'; echo '<h2 style="margin-top:0;">Alle Teilnehmer</h2>';
echo '<a id="kc-export-current-view" class="kc-btn" style="float:right;margin-right:8px;margin-bottom:12px;background:#2da66a;color:#fff;" href="?page=kc_teilnehmer&export_teilnehmer_csv=1">Teilnehmer-Export (Ansicht)</a>';
echo '<a class="kc-btn" style="float:right;margin-bottom:12px;" href="?page=kc_teilnehmer&new=1">+ Neuer Teilnehmer</a>'; echo '<a class="kc-btn" style="float:right;margin-bottom:12px;" href="?page=kc_teilnehmer&new=1">+ Neuer Teilnehmer</a>';
// Wahl-Filter-Buttons sortiert nach KC1, KC2, KC3, dann Rest // Wahl-Filter-Buttons sortiert nach KC1, KC2, KC3, dann Rest
@@ -271,11 +375,19 @@ function kc_teilnehmer_page() {
foreach ($gruppen as $wid => $phasen) { foreach ($gruppen as $wid => $phasen) {
$wahl_disp = isset($wahl_name_map[$wid]) ? esc_html($wahl_name_map[$wid]) : $wid; $wahl_disp = isset($wahl_name_map[$wid]) ? esc_html($wahl_name_map[$wid]) : $wid;
$csv_wahl_url = add_query_arg([
'page' => 'kc_teilnehmer',
'export_teilnehmer_csv' => 1,
'wahl_id' => intval($wid)
], admin_url('admin.php'));
// Gesamtanzahl Teilnehmer für diese Wahl berechnen // Gesamtanzahl Teilnehmer für diese Wahl berechnen
$gesamt = 0; $gesamt = 0;
foreach ($phasen as $tns) $gesamt += count($tns); foreach ($phasen as $tns) $gesamt += count($tns);
echo '<details style="margin:10px 0 18px 0;border:1px solid #eaeaea;border-radius:6px;padding:8px;">'; echo '<details style="margin:10px 0 18px 0;border:1px solid #eaeaea;border-radius:6px;padding:8px;">';
echo '<summary style="font-weight:700;cursor:pointer;">'. $wahl_disp . ' <span style="color:#555;font-weight:600;">(' . $gesamt . ' TN)</span></summary>'; echo '<summary style="font-weight:700;cursor:pointer;">'. $wahl_disp . ' <span style="color:#555;font-weight:600;">(' . $gesamt . ' TN)</span></summary>';
echo '<div style="text-align:right;margin:8px 0 4px 0;">';
echo '<a class="kc-btn" style="background:#2da66a;color:#fff;" href="'.esc_url($csv_wahl_url).'">Teilnehmer-Export dieser Wahl</a>';
echo '</div>';
foreach ($phasen as $phase => $tns) { foreach ($phasen as $phase => $tns) {
echo '<details style="margin:10px 0 10px 0;border:1px solid #f0f0f0;border-radius:6px;padding:8px;">'; echo '<details style="margin:10px 0 10px 0;border:1px solid #f0f0f0;border-radius:6px;padding:8px;">';
echo '<summary style="font-weight:600;">Phase '.intval($phase).' <span style="color:#555;font-weight:600;">('.count($tns).' TN)</span></summary>'; echo '<summary style="font-weight:600;">Phase '.intval($phase).' <span style="color:#555;font-weight:600;">('.count($tns).' TN)</span></summary>';
@@ -316,11 +428,20 @@ function kc_teilnehmer_page() {
var rows = Array.prototype.slice.call(document.querySelectorAll(".kc-admin-table tbody tr")); var rows = Array.prototype.slice.call(document.querySelectorAll(".kc-admin-table tbody tr"));
var status = document.getElementById("kc-wahl-filter-count"); var status = document.getElementById("kc-wahl-filter-count");
var phaseRow = document.getElementById("kc-phase-filter-row"); var phaseRow = document.getElementById("kc-phase-filter-row");
var exportBtn = document.getElementById("kc-export-current-view");
// Wahl-Map für Phasen // Wahl-Map für Phasen
var wahlMap = '.json_encode($wahl_map, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP).'; var wahlMap = '.json_encode($wahl_map, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP).';
var currentWahl = ""; var currentWahl = "";
var currentPhase = ""; var currentPhase = "";
function updateExportButton() {
if (!exportBtn) return;
var href = "?page=kc_teilnehmer&export_teilnehmer_csv=1";
if (currentWahl) href += "&wahl_id=" + encodeURIComponent(currentWahl);
if (currentPhase) href += "&phase=" + encodeURIComponent(currentPhase);
exportBtn.setAttribute("href", href);
}
function renderPhaseButtons(wahlId) { function renderPhaseButtons(wahlId) {
phaseRow.innerHTML = ""; phaseRow.innerHTML = "";
if (!wahlId || !wahlMap[wahlId]) { phaseRow.style.display = "none"; currentPhase = ""; return; } if (!wahlId || !wahlMap[wahlId]) { phaseRow.style.display = "none"; currentPhase = ""; return; }
@@ -366,6 +487,7 @@ function kc_teilnehmer_page() {
if (show) shown++; if (show) shown++;
}); });
if (status) status.textContent = (wahlId ? (shown+" angezeigt") : "Alle anzeigen"); if (status) status.textContent = (wahlId ? (shown+" angezeigt") : "Alle anzeigen");
updateExportButton();
} }
function attachPhaseEvents() { function attachPhaseEvents() {
@@ -396,6 +518,7 @@ function kc_teilnehmer_page() {
if(btns.length) btns[0].classList.add("active"); if(btns.length) btns[0].classList.add("active");
renderPhaseButtons(""); renderPhaseButtons("");
applyFilter("", ""); applyFilter("", "");
updateExportButton();
})(); })();
</script>'; </script>';
echo '</div>'; echo '</div>';

1483
includes/api.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,21 @@ function kc_run_zuteilung($wahl_id) {
// 2) Kopie der Kapazitäten für diese Phase (werden während Zuordnung reduziert) // 2) Kopie der Kapazitäten für diese Phase (werden während Zuordnung reduziert)
$caps = $workshop_caps; $caps = $workshop_caps;
// Minimalanzahlen einmalig laden
$ws_ids_in_wahl = array_keys($workshops);
$min_map = [];
if (!empty($ws_ids_in_wahl)) {
$placeholders = implode(',', array_fill(0, count($ws_ids_in_wahl), '%d'));
$min_rows = $wpdb->get_results($wpdb->prepare(
"SELECT id, min_teilnehmer FROM {$prefix}kc_workshops WHERE id IN ($placeholders)",
$ws_ids_in_wahl
));
foreach($min_rows as $r) {
$min_map[intval($r->id)] = intval($r->min_teilnehmer);
}
}
// 3) Force-Zuteilungen anwenden (nur für diese Phase) // 3) Force-Zuteilungen anwenden (nur für diese Phase)
foreach($forces_all as $f) { foreach($forces_all as $f) {
if(intval($f->phase) !== $phase) continue; if(intval($f->phase) !== $phase) continue;
@@ -93,7 +107,7 @@ function kc_run_zuteilung($wahl_id) {
return true; return true;
}; };
// 5) Wunschrunden 1..3 // 5) Wunschrunden 1..3 (OHNE Minimalanzahl-Prüfung - volle Priorität!)
for($wunsch=1;$wunsch<=3;$wunsch++) { for($wunsch=1;$wunsch<=3;$wunsch++) {
$not_assigned = []; $not_assigned = [];
foreach($remaining as $tn) { foreach($remaining as $tn) {
@@ -110,8 +124,7 @@ function kc_run_zuteilung($wahl_id) {
if(count($remaining) > 1) shuffle($remaining); if(count($remaining) > 1) shuffle($remaining);
} }
// 6) Restliche zufällig verteilen (auf alle freie Workshops) // 6) Restliche zufällig verteilen (OHNE Minimalanzahl-Prüfung)
$freie = array_keys(array_filter($caps, function($c){return $c>0;}));
foreach($remaining as $tn) { foreach($remaining as $tn) {
$freie = array_keys(array_filter($caps, function($c){return $c>0;})); $freie = array_keys(array_filter($caps, function($c){return $c>0;}));
if(count($freie) === 0) { if(count($freie) === 0) {
@@ -131,86 +144,177 @@ function kc_run_zuteilung($wahl_id) {
$assign($tn, $ws_id, 99); $assign($tn, $ws_id, 99);
} }
// 6.5 Consolidate workshops that did not reach their minimal Teilnehmerzahl // 6.5 ITERATIVE MINIMALANZAHL-SICHERUNG: Bis alle erfüllt sind
// Load minimal requirements for workshops in this Wahl
$ws_ids_in_wahl = array_keys($workshops); $ws_ids_in_wahl = array_keys($workshops);
if (!empty($ws_ids_in_wahl)) { $iteration = 0;
$placeholders = implode(',', array_fill(0, count($ws_ids_in_wahl), '%d')); $max_iterations = 10;
$min_rows = $wpdb->get_results($wpdb->prepare("SELECT id, min_teilnehmer FROM {$prefix}kc_workshops WHERE id IN ($placeholders)", $ws_ids_in_wahl));
$min_map = []; while($iteration < $max_iterations) {
foreach($min_rows as $r) $min_map[intval($r->id)] = intval($r->min_teilnehmer); $iteration++;
error_log("Phase $phase: Minimalanzahl-Check Iteration $iteration");
if (!empty($ws_ids_in_wahl)) {
// Aktuelle Belegung zählen
$assigned_counts_raw = $wpdb->get_results($wpdb->prepare(
"SELECT workshop_id, COUNT(*) AS cnt
FROM {$prefix}kc_zuteilung
WHERE wahl_id=%d AND phase=%d AND workshop_id IS NOT NULL
GROUP BY workshop_id",
$wahl_id, $phase
), 'ARRAY_A');
// Count current assignments per workshop $assigned_counts = [];
$assigned_counts_raw = $wpdb->get_results($wpdb->prepare("SELECT workshop_id, COUNT(*) AS cnt FROM {$prefix}kc_zuteilung WHERE wahl_id=%d AND phase=%d AND workshop_id IS NOT NULL GROUP BY workshop_id", $wahl_id, $phase), ARRAY_A); foreach($assigned_counts_raw as $ar) {
$assigned_counts = []; $assigned_counts[intval($ar['workshop_id'])] = intval($ar['cnt']);
foreach($assigned_counts_raw as $ar) $assigned_counts[intval($ar['workshop_id'])] = intval($ar['cnt']);
// Find failing workshops (assigned >0 but < min)
$failing = [];
foreach($ws_ids_in_wahl as $wsid) {
$min_req = intval($min_map[$wsid] ?? 0);
$cnt = intval($assigned_counts[$wsid] ?? 0);
if ($cnt > 0 && $min_req > 0 && $cnt < $min_req) {
$failing[] = $wsid;
} }
}
if (!empty($failing)) { // Unterbesetzte Workshops finden (NUR die mit Teilnehmern, aber < min)
// collect participants from failing workshops $failing = [];
foreach($ws_ids_in_wahl as $wsid) {
$min_req = $min_map[$wsid] ?? 0;
$cnt = $assigned_counts[$wsid] ?? 0;
if ($min_req > 0 && $cnt > 0 && $cnt < $min_req) {
$failing[] = $wsid;
}
}
if (empty($failing)) {
// Alle erfüllt!
error_log("Phase $phase: ✓ Alle Workshops erfüllen ihre Minimalanzahl (Iteration $iteration)");
break;
}
error_log("Phase $phase [Iter $iteration]: $" . count($failing) . " Workshops UNTER Mindestanzahl → umverteilen");
// Alle TN aus unterbesetzten Workshops sammeln
$to_reassign = []; $to_reassign = [];
foreach($failing as $fw) { foreach($failing as $fw) {
$rows = $wpdb->get_results($wpdb->prepare("SELECT teilnehmer_id FROM {$prefix}kc_zuteilung WHERE wahl_id=%d AND phase=%d AND workshop_id=%d", $wahl_id, $phase, $fw)); $rows = $wpdb->get_results($wpdb->prepare(
foreach($rows as $r) $to_reassign[] = intval($r->teilnehmer_id); "SELECT teilnehmer_id FROM {$prefix}kc_zuteilung
WHERE wahl_id=%d AND phase=%d AND workshop_id=%d",
$wahl_id, $phase, $fw
));
foreach($rows as $r) {
$to_reassign[] = intval($r->teilnehmer_id);
}
} }
if (!empty($to_reassign)) { if (empty($to_reassign)) {
// remove those assignments // Keine TN zu umverteilen
$fw_placeholders = implode(',', array_map('intval', $failing)); error_log("Phase $phase [Iter $iteration]: Keine Teilnehmer in unterbesetzten Workshops gefunden");
$wpdb->query("DELETE FROM {$prefix}kc_zuteilung WHERE wahl_id=".intval($wahl_id)." AND phase=".intval($phase)." AND workshop_id IN ($fw_placeholders)"); break;
}
// free capacity for the failing workshops error_log("Phase $phase [Iter $iteration]: " . count($to_reassign) . " Teilnehmer werden entfernt und neu verteilt");
foreach($failing as $fw) {
$freed = intval($assigned_counts[$fw] ?? 0); // Alle Zuweisungen aus unterbesetzten Workshops löschen
if (!isset($caps[$fw])) $caps[$fw] = 0; $fw_list = implode(',', array_map('intval', $failing));
$caps[$fw] += $freed; $wpdb->query("DELETE FROM {$prefix}kc_zuteilung
WHERE wahl_id = " . intval($wahl_id) . "
AND phase = " . intval($phase) . "
AND workshop_id IN ($fw_list)");
// Kapazitäten wieder freigeben
foreach($failing as $fw) {
$freed = $assigned_counts[$fw] ?? 0;
$caps[$fw] = $workshop_caps[$fw]; // Auf max zurücksetzen
}
// "Sichere" Workshops für Umverteilung: Kein Min ODER Min erfüllt (OHNE underbesetzte)
$safe_workshops = [];
foreach($ws_ids_in_wahl as $wsid) {
if (in_array($wsid, $failing)) continue; // Unterbesetzte raus
$min_req = $min_map[$wsid] ?? 0;
$cnt = $assigned_counts[$wsid] ?? 0;
// Sicher wenn:
// A) Kein Minimum UND noch Platz, ODER
// B) Min > 0 UND Min schon erreicht UND noch Platz, ODER
// C) Min > 0 UND alles folgenden TN passen rein (reicht für Min)
$remaining_tns_to_reassign = count($to_reassign);
$cap_left = $caps[$wsid] ?? 0;
$can_fit = false;
if ($min_req == 0) {
$can_fit = $cap_left > 0;
} else {
// Wenn Min schon erfüllt, kann man noch hinzufügen
$current_cnt = $assigned_counts[$wsid] ?? 0;
if ($current_cnt >= $min_req && $cap_left > 0) {
$can_fit = true;
}
// ODER: Wenn noch Platz für den Rest + Min
elseif ($cap_left >= $min_req) {
$can_fit = true;
}
} }
if ($can_fit) {
$safe_workshops[] = $wsid;
}
}
// Try to reassign each participant preferring their wishes 1..3 error_log("Phase $phase [Iter $iteration]: " . count($safe_workshops) . " sichere Workshops verfügbar");
foreach($to_reassign as $tid) {
$tn = isset($all_teilnehmer_by_id[$tid]) ? $all_teilnehmer_by_id[$tid] : $wpdb->get_row($wpdb->prepare("SELECT * FROM {$prefix}kc_teilnehmer WHERE id=%d", $tid)); // Teilnehmer auf sichere Workshops neu verteilen
if (!$tn) continue; foreach($to_reassign as $tid) {
$reassigned = false; $tn = $all_teilnehmer_by_id[$tid] ?? $wpdb->get_row($wpdb->prepare(
for($w=1;$w<=3;$w++) { "SELECT * FROM {$prefix}kc_teilnehmer WHERE id = %d", $tid
$choice = intval($tn->{"wunsch$w"}); ));
if ($choice > 0 && isset($caps[$choice]) && $caps[$choice] > 0) { if (!$tn) continue;
$assigned = $assign($tn, $choice, $w);
if ($assigned) { $reassigned = true; break; } $reassigned = false;
// Zuerst Wünsche auf sichere Workshops
for($w = 1; $w <= 3; $w++) {
$choice = intval($tn->{"wunsch$w"});
if ($choice > 0 && in_array($choice, $safe_workshops) && isset($caps[$choice]) && $caps[$choice] > 0) {
if ($assign($tn, $choice, $w)) {
$reassigned = true;
break;
} }
} }
if ($reassigned) continue; }
// otherwise assign to any workshop with free capacity if ($reassigned) continue;
$available = array_keys(array_filter($caps, function($c){return $c>0;}));
// Zufällig auf sichere Workshops
if (!empty($safe_workshops)) {
$available = [];
foreach($safe_workshops as $wsid) {
if (isset($caps[$wsid]) && $caps[$wsid] > 0) {
$available[] = $wsid;
}
}
if (!empty($available)) { if (!empty($available)) {
$target = $available[array_rand($available)]; $target = $available[array_rand($available)];
$assign($tn, $target, 99); if ($assign($tn, $target, 99)) {
continue; $reassigned = true;
}
} }
}
// lastly, mark as unassigned // Falls immer noch nichts frei → unzugewiesen
if (!$reassigned) {
$wpdb->insert("{$prefix}kc_zuteilung", [ $wpdb->insert("{$prefix}kc_zuteilung", [
'wahl_id' => $wahl_id, 'wahl_id' => $wahl_id,
'teilnehmer_id' => $tn->id, 'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname, 'vorname' => $tn->vorname,
'nachname' => $tn->nachname, 'nachname' => $tn->nachname,
'phase' => $phase, 'phase' => $phase,
'workshop_id' => null, 'workshop_id' => null,
'wunsch_rang' => -1 'wunsch_rang' => -1
]); ]);
} }
} }
} }
} }
if ($iteration >= $max_iterations) {
error_log("Phase $phase: WARNUNG - Max Iterationen ($max_iterations) erreicht. Es könnten noch unterbesetzte Workshops existieren.");
}
// 7) Kapazitätsprüfung (Debug / Log) // 7) Kapazitätsprüfung (Debug / Log)
foreach($caps as $wsid=>$capleft) { foreach($caps as $wsid=>$capleft) {

View File

@@ -1,140 +1,139 @@
<?php <?php
if (!defined('ABSPATH')) exit; if (!defined('ABSPATH')) exit;
/** /**
* Create plugin database tables on activation and remove them on deactivation. * Create plugin database tables on activation and remove them on deactivation.
*/
*/ function kc_install_tables() {
function kc_install_tables() { global $wpdb;
global $wpdb; $prefix = $wpdb->prefix;
$prefix = $wpdb->prefix; $charset_collate = $wpdb->get_charset_collate();
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Tables definitions
// Tables definitions $tables_sql = [];
$tables_sql = [];
$tables_sql[] = "CREATE TABLE {$prefix}kc_wahlen (
$tables_sql[] = "CREATE TABLE {$prefix}kc_wahlen ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, name varchar(191) NOT NULL,
name varchar(191) NOT NULL, beschreibung text DEFAULT NULL,
beschreibung text DEFAULT NULL, anzahl_einheiten tinyint NOT NULL DEFAULT 1,
anzahl_einheiten tinyint NOT NULL DEFAULT 1, min_kapazitaet int NOT NULL DEFAULT 0,
min_kapazitaet int NOT NULL DEFAULT 0, max_kapazitaet int NOT NULL DEFAULT 0,
max_kapazitaet int NOT NULL DEFAULT 0, freigegeben tinyint(1) NOT NULL DEFAULT 0,
freigegeben tinyint(1) NOT NULL DEFAULT 0, deleted tinyint(1) NOT NULL DEFAULT 0,
deleted tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_workshops (
$tables_sql[] = "CREATE TABLE {$prefix}kc_workshops ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, name varchar(191) NOT NULL,
name varchar(191) NOT NULL, beschreibung text DEFAULT NULL,
beschreibung text DEFAULT NULL, max_teilnehmer int NOT NULL DEFAULT 0,
max_teilnehmer int NOT NULL DEFAULT 0, min_teilnehmer int NOT NULL DEFAULT 0,
min_teilnehmer int NOT NULL DEFAULT 0, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_teamer (
$tables_sql[] = "CREATE TABLE {$prefix}kc_teamer ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, vorname varchar(191) DEFAULT NULL,
vorname varchar(191) DEFAULT NULL, nachname varchar(191) DEFAULT NULL,
nachname varchar(191) DEFAULT NULL, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_teilnehmer (
$tables_sql[] = "CREATE TABLE {$prefix}kc_teilnehmer ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, vorname varchar(191) DEFAULT NULL,
vorname varchar(191) DEFAULT NULL, nachname varchar(191) DEFAULT NULL,
nachname varchar(191) DEFAULT NULL, wahl_id bigint(20) unsigned NOT NULL DEFAULT 0,
wahl_id bigint(20) unsigned NOT NULL DEFAULT 0, phase tinyint NOT NULL DEFAULT 1,
phase tinyint NOT NULL DEFAULT 1, wunsch1 bigint(20) unsigned DEFAULT NULL,
wunsch1 bigint(20) unsigned DEFAULT NULL, wunsch2 bigint(20) unsigned DEFAULT NULL,
wunsch2 bigint(20) unsigned DEFAULT NULL, wunsch3 bigint(20) unsigned DEFAULT NULL,
wunsch3 bigint(20) unsigned DEFAULT NULL, deleted tinyint(1) NOT NULL DEFAULT 0,
deleted tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_wahl_workshops (
$tables_sql[] = "CREATE TABLE {$prefix}kc_wahl_workshops ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, wahl_id bigint(20) unsigned NOT NULL DEFAULT 0,
wahl_id bigint(20) unsigned NOT NULL DEFAULT 0, workshop_id bigint(20) unsigned NOT NULL DEFAULT 0,
workshop_id bigint(20) unsigned NOT NULL DEFAULT 0, phase tinyint NOT NULL DEFAULT 1,
phase tinyint NOT NULL DEFAULT 1, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_force_zuteilung (
$tables_sql[] = "CREATE TABLE {$prefix}kc_force_zuteilung ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, teilnehmer_id bigint(20) unsigned NOT NULL DEFAULT 0,
teilnehmer_id bigint(20) unsigned NOT NULL DEFAULT 0, wahl_id bigint(20) unsigned NOT NULL DEFAULT 0,
wahl_id bigint(20) unsigned NOT NULL DEFAULT 0, phase tinyint NOT NULL DEFAULT 1,
phase tinyint NOT NULL DEFAULT 1, workshop_id bigint(20) unsigned DEFAULT NULL,
workshop_id bigint(20) unsigned DEFAULT NULL, kommentar text DEFAULT NULL,
kommentar text DEFAULT NULL, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_zuteilung (
$tables_sql[] = "CREATE TABLE {$prefix}kc_zuteilung ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, teilnehmer_id bigint(20) unsigned NOT NULL DEFAULT 0,
teilnehmer_id bigint(20) unsigned NOT NULL DEFAULT 0, vorname varchar(191) DEFAULT NULL,
vorname varchar(191) DEFAULT NULL, nachname varchar(191) DEFAULT NULL,
nachname varchar(191) DEFAULT NULL, wahl_id bigint(20) unsigned NOT NULL DEFAULT 0,
wahl_id bigint(20) unsigned NOT NULL DEFAULT 0, phase tinyint NOT NULL DEFAULT 1,
phase tinyint NOT NULL DEFAULT 1, workshop_id bigint(20) unsigned DEFAULT NULL,
workshop_id bigint(20) unsigned DEFAULT NULL, wunsch_rang tinyint DEFAULT NULL,
wunsch_rang tinyint DEFAULT NULL, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
$tables_sql[] = "CREATE TABLE {$prefix}kc_workshop_teamer (
$tables_sql[] = "CREATE TABLE {$prefix}kc_workshop_teamer ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
id bigint(20) unsigned NOT NULL AUTO_INCREMENT, workshop_id bigint(20) unsigned DEFAULT NULL,
workshop_id bigint(20) unsigned DEFAULT NULL, teamer_id bigint(20) unsigned DEFAULT NULL,
teamer_id bigint(20) unsigned DEFAULT NULL, PRIMARY KEY (id)
PRIMARY KEY (id) ) $charset_collate";
) $charset_collate";
foreach ($tables_sql as $sql) {
foreach ($tables_sql as $sql) { dbDelta($sql);
dbDelta($sql); }
}
// Post-creation migration guard: ensure min_teilnehmer exists for older installs
// Post-creation migration guard: ensure min_teilnehmer exists for older installs $col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM {$prefix}kc_workshops LIKE %s", 'min_teilnehmer'));
$col = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM {$prefix}kc_workshops LIKE %s", 'min_teilnehmer')); if (empty($col)) {
if (empty($col)) { // try to add the column (no-op on newer installs)
// try to add the column (no-op on newer installs) $wpdb->query("ALTER TABLE {$prefix}kc_workshops ADD COLUMN min_teilnehmer INT NOT NULL DEFAULT 0");
$wpdb->query("ALTER TABLE {$prefix}kc_workshops ADD COLUMN min_teilnehmer INT NOT NULL DEFAULT 0"); }
}
// Optionally store plugin version
// Optionally store plugin version add_option('kc_workshopwahl_db_version', '1.0');
add_option('kc_workshopwahl_db_version', '1.0'); }
}
function kc_uninstall_tables() {
function kc_uninstall_tables() { global $wpdb;
global $wpdb; $prefix = $wpdb->prefix;
$prefix = $wpdb->prefix;
$tables = [
$tables = [ "{$prefix}kc_zuteilung",
"{$prefix}kc_zuteilung", "{$prefix}kc_force_zuteilung",
"{$prefix}kc_force_zuteilung", "{$prefix}kc_wahl_workshops",
"{$prefix}kc_wahl_workshops", "{$prefix}kc_teilnehmer",
"{$prefix}kc_teilnehmer", "{$prefix}kc_teamer",
"{$prefix}kc_teamer", "{$prefix}kc_workshops",
"{$prefix}kc_workshops", "{$prefix}kc_wahlen",
"{$prefix}kc_wahlen", "{$prefix}kc_workshop_teamer"
"{$prefix}kc_workshop_teamer" ];
];
foreach ($tables as $t) {
foreach ($tables as $t) { $wpdb->query("DROP TABLE IF EXISTS $t");
$wpdb->query("DROP TABLE IF EXISTS $t"); }
}
delete_option('kc_workshopwahl_db_version');
delete_option('kc_workshopwahl_db_version'); }
}
// Backwards-compat wrapper for register_activation_hook usage
// Backwards-compat wrapper for register_activation_hook usage if (!function_exists('kc_register_hooks_internal')) {
if (!function_exists('kc_register_hooks_internal')) { function kc_register_hooks_internal() {
function kc_register_hooks_internal() { // intentionally left blank
// intentionally left blank }
} }
}
?>
?>

View File

@@ -77,4 +77,5 @@ require_once plugin_dir_path(__FILE__).'includes/frontend-form.php';
require_once plugin_dir_path(__FILE__).'includes/frontend-ergebnis.php'; require_once plugin_dir_path(__FILE__).'includes/frontend-ergebnis.php';
require_once plugin_dir_path(__FILE__).'includes/zuteilungslogik.php'; require_once plugin_dir_path(__FILE__).'includes/zuteilungslogik.php';
require_once plugin_dir_path(__FILE__).'includes/admin-data.php'; require_once plugin_dir_path(__FILE__).'includes/admin-data.php';
require_once plugin_dir_path(__FILE__).'includes/api.php';
require_once plugin_dir_path(__FILE__).'install.php'; require_once plugin_dir_path(__FILE__).'install.php';