All checks were successful
Deploy Workshop-Wahlen (DEV / PROD) / deploy (push) Successful in 13s
484 lines
22 KiB
PHP
484 lines
22 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;
|
||
|
||
// 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 1–3 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;
|
||
}
|
||
}
|
||
}
|
||
|