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; } } }