Files
Workshop-Wahlen/includes/zuteilungslogik.php
Blitz08 0f5d575cb9
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
Improve workshop minimum participant handling and reassignment logic
2026-03-02 12:49:37 +01:00

484 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Zuteilungslogik für Konficastle Workshopwahl
function kc_run_zuteilung($wahl_id) {
global $wpdb;
$prefix = $wpdb->prefix;
$wahl_id = intval($wahl_id);
error_log('kc_run_zuteilung: Start für Wahl '.$wahl_id);
// 0. Entferne alte Zuteilungen für diese Wahl
$wpdb->delete("{$prefix}kc_zuteilung", ['wahl_id' => $wahl_id]);
// 1. Teilnehmer einlesen (alle Phasen werden getrennt verarbeitet)
$phasen = intval($wpdb->get_var($wpdb->prepare("SELECT anzahl_einheiten FROM {$prefix}kc_wahlen WHERE id=%d", $wahl_id)));
if($phasen < 1) $phasen = 1;
// 2. Workshops einlesen (inkl. Kapazität)
$workshops_rows = $wpdb->get_results($wpdb->prepare(
"SELECT ws.id, ws.name, ws.max_teilnehmer FROM {$prefix}kc_workshops ws JOIN {$prefix}kc_wahl_workshops ww ON ws.id = ww.workshop_id WHERE ww.wahl_id=%d",
$wahl_id
));
$workshops = [];
$workshop_caps = [];
foreach($workshops_rows as $w) {
$workshops[$w->id] = $w;
$workshop_caps[$w->id] = intval($w->max_teilnehmer);
}
// 3. Force-Zuteilungen lesen (global, nach Phase filtern)
$forces_all = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$prefix}kc_force_zuteilung WHERE wahl_id=%d", $wahl_id));
// Prozessiere jede Phase einzeln
for($phase=1; $phase<=$phasen; $phase++) {
error_log("kc_run_zuteilung: Verarbeite Phase $phase");
// 1) Teilnehmer einlesen (alle TN dieser Wahl+Phase)
$teilnehmer = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$prefix}kc_teilnehmer WHERE wahl_id=%d AND phase=%d",
$wahl_id, $phase
));
// Prepare helper maps
$teilnehmer_by_id = [];
foreach($teilnehmer as $t) $teilnehmer_by_id[$t->id] = $t;
// keep a full copy for later reassignments if needed
$all_teilnehmer_by_id = $teilnehmer_by_id;
// 2) Kopie der Kapazitäten für diese Phase (werden während Zuordnung reduziert)
$caps = $workshop_caps;
// 3) Force-Zuteilungen anwenden (nur für diese Phase)
foreach($forces_all as $f) {
if(intval($f->phase) !== $phase) continue;
$tn_id = intval($f->teilnehmer_id);
$ws_id = intval($f->workshop_id);
if(!isset($teilnehmer_by_id[$tn_id])) continue; // Teilnehmer nicht in dieser Phase
// Nur wenn Workshop existiert und noch Kapazität >=1
if(isset($caps[$ws_id]) && $caps[$ws_id] > 0) {
$t = $teilnehmer_by_id[$tn_id];
$wpdb->insert("{$prefix}kc_zuteilung", [
'wahl_id' => $wahl_id,
'teilnehmer_id' => $t->id,
'vorname' => $t->vorname,
'nachname' => $t->nachname,
'phase' => $phase,
'workshop_id' => $ws_id,
'wunsch_rang' => 0
]);
$caps[$ws_id]--;
// Entferne TN aus Liste (er ist bereits zugeteilt)
unset($teilnehmer_by_id[$tn_id]);
}
}
// 4) Teilnehmer mischen (zufällige Reihenfolge)
$remaining = array_values($teilnehmer_by_id);
if(count($remaining) > 1) shuffle($remaining);
// Hilfsfunktion: Try assign participant to workshop if cap>0
$assign = function($tn, $ws_id, $rang) use (&$caps, $wpdb, $prefix, $wahl_id, $phase, &$assigned_ids) {
if(!isset($caps[$ws_id]) || $caps[$ws_id] <= 0) return false;
$wpdb->insert("{$prefix}kc_zuteilung", [
'wahl_id' => $wahl_id,
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $phase,
'workshop_id' => $ws_id,
'wunsch_rang' => $rang
]);
$caps[$ws_id]--;
return true;
};
// 5) Wunschrunden 1..3
for($wunsch=1;$wunsch<=3;$wunsch++) {
$not_assigned = [];
foreach($remaining as $tn) {
$ws_choice = intval($tn->{"wunsch$wunsch"});
if($ws_choice > 0 && isset($caps[$ws_choice]) && $caps[$ws_choice] > 0) {
$assigned = $assign($tn, $ws_choice, $wunsch);
if(!$assigned) $not_assigned[] = $tn;
} else {
$not_assigned[] = $tn;
}
}
$remaining = $not_assigned;
// optional: reshuffle after each round to keep fairness
if(count($remaining) > 1) shuffle($remaining);
}
// 6) Restliche zufällig verteilen (auf alle freie Workshops)
$freie = array_keys(array_filter($caps, function($c){return $c>0;}));
foreach($remaining as $tn) {
$freie = array_keys(array_filter($caps, function($c){return $c>0;}));
if(count($freie) === 0) {
// Kein Platz mehr: TN bleibt unzugeordnet
$wpdb->insert("{$prefix}kc_zuteilung", [
'wahl_id' => $wahl_id,
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $phase,
'workshop_id' => null,
'wunsch_rang' => -1
]);
continue;
}
$ws_id = $freie[array_rand($freie)];
$assign($tn, $ws_id, 99);
}
// 6.5 Verbesserte Mindestanzahl-Sicherung: Unterbesetzte Workshops auflösen
$ws_ids_in_wahl = array_keys($workshops);
if (!empty($ws_ids_in_wahl)) {
// min_teilnehmer laden (einmalig)
$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
));
$min_map = [];
foreach($min_rows as $r) {
$min_map[intval($r->id)] = intval($r->min_teilnehmer);
}
// 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');
$assigned_counts = [];
foreach($assigned_counts_raw as $ar) {
$assigned_counts[intval($ar['workshop_id'])] = intval($ar['cnt']);
}
$assigned_counts = [];
foreach($assigned_counts_raw as $ar) {
$assigned_counts[intval($ar['workshop_id'])] = intval($ar['cnt']);
}
// Unterbesetzte Workshops finden (mit Zuweisung > 0 aber < min)
$failing = [];
foreach($ws_ids_in_wahl as $wsid) {
$min_req = $min_map[$wsid] ?? 0;
$cnt = $assigned_counts[$wsid] ?? 0;
if ($min_req > 0 && $cnt < $min_req) { // ← auch wenn cnt == 0 !
$failing[] = $wsid;
}
}
// ──────────────── WICHTIGE DIAGNOSE ────────────────
error_log("=== DIAGNOSE Mindestanzahl Phase $phase ===");
error_log("Workshops insgesamt: " . count($ws_ids_in_wahl));
foreach ($ws_ids_in_wahl as $wsid) {
$name = $workshops[$wsid]->name ?? 'ID '.$wsid;
$min = $min_map[$wsid] ?? 0;
$cnt = $assigned_counts[$wsid] ?? 0;
$cap = $caps[$wsid] ?? 'unbekannt';
$status = ($min > 0 && $cnt > 0 && $cnt < $min) ? 'UNTER MIN → wird umverteilt' :
(($min > 0 && $cnt >= $min) ? 'OK' :
($cnt == 0 ? 'leer (ignoriert)' : 'keine min-Anforderung'));
error_log("WS $wsid ($name) | min=$min | jetzt=$cnt | Restkap=$cap | $status");
}
error_log("Anzahl failing Workshops: " . count($failing));
if (!empty($failing)) {
error_log("Umverteilung startet für WS: " . implode(', ', $failing));
} else {
error_log("Keine Workshops unter Mindestanzahl mit Teilnehmern → nichts zu tun");
}
error_log("============================");
if (!empty($failing)) {
error_log("Phase $phase: Folgende Workshops unter Mindestanzahl → Teilnehmer werden umverteilt: " . implode(', ', $failing));
// Alle betroffenen Teilnehmer sammeln
$to_reassign = [];
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
));
foreach($rows as $r) {
$to_reassign[] = intval($r->teilnehmer_id);
}
}
if (!empty($to_reassign)) {
// Alte Zuweisungen löschen
$fw_list = implode(',', array_map('intval', $failing));
$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] = ($caps[$fw] ?? 0) + $freed;
}
// Teilnehmer erneut versuchen zuzuweisen zuerst Wünsche, dann Rest
foreach($to_reassign as $tid) {
$tn = $all_teilnehmer_by_id[$tid] ?? $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$prefix}kc_teilnehmer WHERE id = %d", $tid
));
if (!$tn) continue;
$reassigned = false;
// Nochmal Wunsch 13 versuchen
for($w = 1; $w <= 3; $w++) {
$choice = intval($tn->{"wunsch$w"});
if ($choice > 0 && isset($caps[$choice]) && $caps[$choice] > 0) {
if ($assign($tn, $choice, $w)) {
$reassigned = true;
break;
}
}
}
if ($reassigned) continue;
// Falls kein Wunsch frei → irgendein freier Workshop
$available = array_keys(array_filter($caps, fn($c) => $c > 0));
if (!empty($available)) {
$target = $available[array_rand($available)];
if ($assign($tn, $target, 99)) {
$reassigned = true;
}
}
// Wenn immer noch nichts frei → unassigned
if (!$reassigned) {
$wpdb->insert("{$prefix}kc_zuteilung", [
'wahl_id' => $wahl_id,
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $phase,
'workshop_id' => null,
'wunsch_rang' => -1
]);
}
}
}
}
}
// 7) Kapazitätsprüfung (Debug / Log)
foreach($caps as $wsid=>$capleft) {
if($capleft < 0) {
error_log("kc_run_zuteilung: Überbuchung Workshop $wsid in Wahl $wahl_id Phase $phase (restcap=$capleft)");
}
}
// 8) Ergebnis: alles in DB geschrieben (kc_zuteilung). Logge Zusammenfassung
$total_assigned = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$prefix}kc_zuteilung WHERE wahl_id=%d AND phase=%d AND workshop_id IS NOT NULL", $wahl_id, $phase));
$total_unassigned = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$prefix}kc_zuteilung WHERE wahl_id=%d AND phase=%d AND workshop_id IS NULL", $wahl_id, $phase));
error_log("kc_run_zuteilung: Phase $phase - zugeteilt: $total_assigned, ohne Platz: $total_unassigned");
}
error_log('kc_run_zuteilung: Fertig für Wahl '.$wahl_id);
}
function kc_execute_blocks($blocks, &$context, &$i = 0) {
$count = count($blocks);
while($i < $count) {
$block = $blocks[$i];
$block_name = is_array($block) && isset($block['block']) ? $block['block'] : (is_string($block) && strpos($block, ':') !== false ? explode(':', $block, 2)[0] : $block);
switch($block_name) {
case 'repeat':
$repeat_count = 3;
if(is_array($block) && isset($block['repeat_count'])) {
$repeat_count = intval($block['repeat_count']);
} elseif(is_string($block) && strpos($block, ':') !== false) {
$parts = explode(':', $block, 2);
$repeat_count = intval($parts[1]);
}
$i++;
$start = $i;
$inner = [];
$depth = 1;
while($i < $count && $depth > 0) {
$inner_block = $blocks[$i];
$inner_block_name = is_array($inner_block) && isset($inner_block['block']) ? $inner_block['block'] : (is_string($inner_block) && strpos($inner_block, ':') !== false ? explode(':', $inner_block, 2)[0] : $inner_block);
if($inner_block_name === 'repeat') $depth++;
if($inner_block_name === 'endrepeat') $depth--;
if($depth > 0) $inner[] = $inner_block;
$i++;
}
for($r=0;$r<$repeat_count;$r++) {
$j = 0;
kc_execute_blocks($inner, $context, $j);
}
break;
case 'for_teilnehmer':
$i++;
$start = $i;
$inner = [];
$depth = 1;
while($i < $count && $depth > 0) {
if($blocks[$i] === 'for_teilnehmer') $depth++;
if($blocks[$i] === 'endfor_teilnehmer') $depth--;
if($depth > 0) $inner[] = $blocks[$i];
$i++;
}
foreach($context['teilnehmer'] as $tn) {
$context['tn'] = $tn;
$j = 0;
kc_execute_blocks($inner, $context, $j);
}
break;
case 'for_workshop':
$i++;
$start = $i;
$inner = [];
$depth = 1;
while($i < $count && $depth > 0) {
if($blocks[$i] === 'for_workshop') $depth++;
if($blocks[$i] === 'endfor_workshop') $depth--;
if($depth > 0) $inner[] = $blocks[$i];
$i++;
}
foreach($context['workshops'] as $ws) {
$context['ws'] = $ws;
$j = 0;
kc_execute_blocks($inner, $context, $j);
}
break;
case 'for_wunsch':
$i++;
$start = $i;
$inner = [];
$depth = 1;
while($i < $count && $depth > 0) {
if($blocks[$i] === 'for_wunsch') $depth++;
if($blocks[$i] === 'endfor_wunsch') $depth--;
if($depth > 0) $inner[] = $blocks[$i];
$i++;
}
for($w=1;$w<=3;$w++) {
$context['wunsch_nr'] = $w;
$j = 0;
kc_execute_blocks($inner, $context, $j);
}
break;
// --- Hier die eigentliche Blocklogik einfügen ---
case 'shuffle':
$rest = array_filter($context['teilnehmer'], function($tn) use ($context) {
return !in_array($tn->id, $context['zugeteilt_ids']);
});
$rest = array_values($rest);
if(count($rest) > 1) {
shuffle($rest);
$neu = [];
$i2 = 0;
foreach($context['teilnehmer'] as $tn) {
if(!in_array($tn->id, $context['zugeteilt_ids'])) {
$neu[] = $rest[$i2++];
} else {
$neu[] = $tn;
}
}
$context['teilnehmer'] = $neu;
}
$i++;
break;
case 'force':
foreach($context['forces'] as $f) {
$tn = $context['wpdb']->get_row("SELECT * FROM {$context['prefix']}kc_teilnehmer WHERE id=".intval($f->teilnehmer_id));
if(!$tn) continue;
if(isset($context['workshop_caps'][$f->workshop_id]) && $context['workshop_caps'][$f->workshop_id] > 0 && !in_array($tn->id, $context['zugeteilt_ids'])) {
$context['wpdb']->insert("{$context['prefix']}kc_zuteilung", [
'wahl_id' => $context['wahl_id'],
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $context['phase'],
'workshop_id' => $f->workshop_id,
'wunsch_rang' => 0
]);
$context['workshop_caps'][$f->workshop_id]--;
$context['zugeteilt_ids'][] = $tn->id;
}
}
$i++;
break;
case 'wunsch1':
case 'wunsch1_kapazitaet':
case 'wunsch2':
case 'wunsch2_kapazitaet':
case 'wunsch3':
case 'wunsch3_kapazitaet':
$wunsch_num = ($block === 'wunsch1' || $block === 'wunsch1_kapazitaet') ? 1 : (($block === 'wunsch2' || $block === 'wunsch2_kapazitaet') ? 2 : 3);
foreach($context['teilnehmer'] as $tn) {
if(in_array($tn->id, $context['zugeteilt_ids'])) continue;
$ws_id = intval($tn->{"wunsch$wunsch_num"});
if(isset($context['workshop_caps'][$ws_id]) && $context['workshop_caps'][$ws_id] > 0) {
$context['wpdb']->insert("{$context['prefix']}kc_zuteilung", [
'wahl_id' => $context['wahl_id'],
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $context['phase'],
'workshop_id' => $ws_id,
'wunsch_rang' => $wunsch_num
]);
$context['workshop_caps'][$ws_id]--;
$context['zugeteilt_ids'][] = $tn->id;
}
}
$i++;
break;
case 'random':
$freie_workshops = array_keys(array_filter($context['workshop_caps'], function($cap){return $cap>0;}));
$rest = array_filter($context['teilnehmer'], function($tn) use ($context) {
return !in_array($tn->id, $context['zugeteilt_ids']);
});
$rest = array_values($rest);
foreach($rest as $tn) {
if(count($freie_workshops)>0) {
$ws_id = $freie_workshops[array_rand($freie_workshops)];
$context['wpdb']->insert("{$context['prefix']}kc_zuteilung", [
'wahl_id' => $context['wahl_id'],
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $context['phase'],
'workshop_id' => $ws_id,
'wunsch_rang' => 99
]);
$context['workshop_caps'][$ws_id]--;
$context['zugeteilt_ids'][] = $tn->id;
$freie_workshops = array_keys(array_filter($context['workshop_caps'], function($cap){return $cap>0;}));
} else {
$context['wpdb']->insert("{$context['prefix']}kc_zuteilung", [
'wahl_id' => $context['wahl_id'],
'teilnehmer_id' => $tn->id,
'vorname' => $tn->vorname,
'nachname' => $tn->nachname,
'phase' => $context['phase'],
'workshop_id' => null,
'wunsch_rang' => -1
]);
}
}
$i++;
break;
default:
$i++;
break;
}
}
}