Files
Workshop-Wahlen/includes/zuteilungslogik.php
ProgrammGamer 8a7b0d71ee
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 11s
Enhance workshop assignment logic with minimum participant checks and improved reassignment handling
2026-03-02 13:00:40 +01:00

506 lines
24 KiB
PHP

<?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;
// 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)
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 (OHNE Minimalanzahl-Prüfung - volle Priorität!)
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 (OHNE Minimalanzahl-Prüfung)
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 NEUE LOGIK: NACH Zufallsverteilung Minimalanzahlen überprüfen
// Unterbesetzte Workshops auflösen und TN nur auf "sichere" Workshops verteilen
$ws_ids_in_wahl = array_keys($workshops);
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');
$assigned_counts = [];
foreach($assigned_counts_raw as $ar) {
$assigned_counts[intval($ar['workshop_id'])] = intval($ar['cnt']);
}
// ─────────────── DIAGNOSE VOR UMVERTEILUNG ───────────────
error_log("=== DIAGNOSE Mindestanzahl Phase $phase (VOR Umverteilung) ===");
foreach ($ws_ids_in_wahl as $wsid) {
$name = $workshops[$wsid]->name ?? 'ID '.$wsid;
$min = $min_map[$wsid] ?? 0;
$cnt = $assigned_counts[$wsid] ?? 0;
$status = ($min > 0 && $cnt > 0 && $cnt < $min) ? 'UNTER MIN → wird umverteilt' :
(($min > 0 && $cnt >= $min) ? 'OK ✓' : ($cnt == 0 ? 'leer' : 'OK (kein Min)'));
error_log("WS $wsid ($name) | min=$min | TN=$cnt | $status");
}
error_log("============================");
// Unterbesetzte Workshops finden
$failing = [];
foreach($ws_ids_in_wahl as $wsid) {
$min_req = $min_map[$wsid] ?? 0;
$cnt = $assigned_counts[$wsid] ?? 0;
// Workshop ist unterbesetzt wenn: er hat Teilnehmer, aber weniger als Minimum
if ($min_req > 0 && $cnt > 0 && $cnt < $min_req) {
$failing[] = $wsid;
}
}
if (!empty($failing)) {
error_log("Phase $phase: Workshops UNTER Mindestanzahl: " . implode(', ', $failing) . " → Umverteilung startet");
// Alle TN aus unterbesetzten Workshops 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)) {
error_log("Phase $phase: " . count($to_reassign) . " Teilnehmer werden umverteilt");
// 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;
}
// "Sichere" Workshops: Nur die, die KEIN Minimum haben ODER ihr Minimum BEREITS erfüllt haben
$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: kein Min UND Kapazität übrig OR Min erfüllt und noch Platz
if (($min_req == 0 && isset($caps[$wsid]) && $caps[$wsid] > 0) ||
($min_req > 0 && $cnt >= $min_req && isset($caps[$wsid]) && $caps[$wsid] > 0)) {
$safe_workshops[] = $wsid;
}
}
error_log("Phase $phase: " . count($safe_workshops) . " 'sichere' Workshops für Umverteilung verfügbar");
// Teilnehmer auf sichere Workshops verteilen
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;
// Zuerst Wünsche versuchen (nur 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;
// Falls kein Wunsch auf sicherer Liste frei → zufällig auf sichere verteilen
if (!empty($safe_workshops)) {
$available = [];
foreach($safe_workshops as $wsid) {
if (isset($caps[$wsid]) && $caps[$wsid] > 0) {
$available[] = $wsid;
}
}
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
]);
}
}
// Unterbesetzte Workshops sind nun aus dem Pool raus
foreach($failing as $fw) {
error_log("Workshop $fw wird aus dem Pool entfernt (konnte nicht mit Minimum befüllt werden)");
}
}
} else {
error_log("Phase $phase: Alle Workshops erfüllen ihre Minimalanzahl ✓");
}
}
// 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;
}
}
}