Files
Workshop-Wahlen/includes/zuteilungslogik.php
ProgrammGamer 0cabca874d
All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 12s
Implement iterative minimum participant checks for workshop assignments
2026-03-02 13:08:02 +01:00

526 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 ITERATIVE MINIMALANZAHL-SICHERUNG: Bis alle erfüllt sind
$ws_ids_in_wahl = array_keys($workshops);
$iteration = 0;
$max_iterations = 10;
while($iteration < $max_iterations) {
$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');
$assigned_counts = [];
foreach($assigned_counts_raw as $ar) {
$assigned_counts[intval($ar['workshop_id'])] = intval($ar['cnt']);
}
// Unterbesetzte Workshops finden (NUR die mit Teilnehmern, 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 > 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 = [];
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)) {
// Keine TN zu umverteilen
error_log("Phase $phase [Iter $iteration]: Keine Teilnehmer in unterbesetzten Workshops gefunden");
break;
}
error_log("Phase $phase [Iter $iteration]: " . count($to_reassign) . " Teilnehmer werden entfernt und neu verteilt");
// Alle Zuweisungen aus unterbesetzten Workshops 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] = $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;
}
}
error_log("Phase $phase [Iter $iteration]: " . count($safe_workshops) . " sichere Workshops verfügbar");
// Teilnehmer auf sichere Workshops neu 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 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;
// 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)) {
$target = $available[array_rand($available)];
if ($assign($tn, $target, 99)) {
$reassigned = true;
}
}
}
// Falls immer noch nichts frei → unzugewiesen
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
]);
}
}
}
}
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)
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;
}
}
}