'Y-m-d H:i' ]. */ private static function get_notified() { $m = get_option( 'advk_notified', array() ); return is_array( $m ) ? $m : array(); } /** @param array $map Notified map to persist. */ private static function save_notified( $map ) { update_option( 'advk_notified', $map, false ); } /** * Timestamp a winner was notified, or '' when not yet. * * @param int $id Entry id. * @return string */ public static function notified_at( $id ) { $m = self::get_notified(); return isset( $m[ (int) $id ] ) ? (string) $m[ (int) $id ] : ''; } /** * Send the winner e-mail to all winners (optionally a single day) that have * not been notified yet, and record the notification. */ public function handle_notify() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'adventura-interactive-advent-calendar' ) ); } check_admin_referer( 'advk_notify_winners' ); $day = isset( $_POST['day'] ) ? (int) $_POST['day'] : 0; $opts = ADVK_Settings::get_options(); $winners = ADVK_Entries::query( array( 'per_page' => 100000, 'offset' => 0, 'day' => ( $day >= 1 && $day <= ADVK_Data::door_count() ) ? $day : 0, 'winners_only' => true, ) ); $sitename = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ); $subject_tpl = (string) $opts['winner_email_subject']; $body_tpl = (string) $opts['winner_email_body']; $headers = ADVK_Emails::mail_headers( $opts, true ); $notified = self::get_notified(); $sent = 0; foreach ( $winners as $w ) { if ( isset( $notified[ (int) $w->id ] ) || ! is_email( $w->email ) ) { continue; // already notified or no valid address } $prize = self::prize_for_winner( (int) $w->day, (int) $w->id ); $replace = array( '{first}' => $w->first_name, '{last}' => $w->last_name, '{day}' => (int) $w->day, '{prize}' => '' !== $prize ? $prize : $this->prize_titles_for_day( (int) $w->day ), '{losnr}' => $w->losnr, '{sitename}' => $sitename, ); $subject = strtr( $subject_tpl, $replace ); $body = strtr( $body_tpl, $replace ); $html = ADVK_Emails::html_wrap( $subject, $body, $opts ); if ( wp_mail( $w->email, $subject, $html, $headers ) ) { $notified[ (int) $w->id ] = wp_date( 'Y-m-d H:i' ); ++$sent; } } self::save_notified( $notified ); $this->redirect_back( sprintf( /* translators: %d: number of winner e-mails sent. */ _n( '%d Gewinner benachrichtigt.', '%d Gewinner benachrichtigt.', $sent, 'adventura-interactive-advent-calendar' ), $sent ) ); } /** * Fallback prize label for a day (all prize titles) when no specific prize * was assigned to the winner (e.g. manually flagged winners). * * @param int $day Door day. * @return string */ private function prize_titles_for_day( $day ) { $door = $this->door_by_day( $day ); if ( ! $door || empty( $door['prizes'] ) ) { return ''; } $titles = array(); foreach ( $door['prizes'] as $p ) { $t = isset( $p['title'] ) ? trim( (string) $p['title'] ) : ''; if ( '' !== $t ) { $titles[] = $t; } } return implode( ', ', $titles ); } public function add_menu() { add_submenu_page( ADVK_Dashboard::SLUG, __( 'Auslosung', 'adventura-interactive-advent-calendar' ), __( 'Auslosung', 'adventura-interactive-advent-calendar' ), 'manage_options', 'advk-winners', array( $this, 'render_page' ) ); } private function redirect_back( $notice = '' ) { $url = admin_url( 'admin.php?page=advk-winners' ); if ( $notice ) { $url = add_query_arg( 'advk_notice', rawurlencode( $notice ), $url ); } wp_safe_redirect( $url ); exit; } public function handle_draw() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'adventura-interactive-advent-calendar' ) ); } check_admin_referer( 'advk_draw_winner' ); $day = isset( $_POST['day'] ) ? (int) $_POST['day'] : 0; $correct_only = ! empty( $_POST['correct_only'] ); if ( $day < 1 || $day > ADVK_Data::door_count() ) { $this->redirect_back( __( 'Bitte ein Türchen wählen.', 'adventura-interactive-advent-calendar' ) ); } $door = $this->door_by_day( $day ); $prizes = ( $door && isset( $door['prizes'] ) && is_array( $door['prizes'] ) ) ? $door['prizes'] : array(); $winner_count = ( $door && isset( $door['winnerCount'] ) ) ? (int) $door['winnerCount'] : 1; if ( $winner_count < 1 ) { $winner_count = 1; } $opts = ADVK_Settings::get_options(); $direct_min = (int) $opts['donation_direct_min']; $direct_prize = (string) $opts['donation_direct_prize']; $eligible = ADVK_Entries::eligible_ids( $day, $correct_only ); $donations = ADVK_Entries::get_donations(); if ( empty( $eligible ) ) { /* translators: %d: door day number (1–24). */ $this->redirect_back( sprintf( __( 'Keine passenden Teilnahmen für Tag %d.', 'adventura-interactive-advent-calendar' ), $day ) ); } $map = self::get_prize_map(); $map[ $day ] = isset( $map[ $day ] ) && is_array( $map[ $day ] ) ? $map[ $day ] : array(); // 1) Direct wins: any donation of at least the threshold guarantees the // configured direct-win prize (independent of the random draw). $direct = array(); $rest = array(); foreach ( $eligible as $id ) { $amt = isset( $donations[ $id ] ) ? (float) $donations[ $id ] : 0.0; if ( $direct_min > 0 && $amt >= $direct_min ) { $direct[] = $id; } else { $rest[] = $id; } } foreach ( $direct as $id ) { ADVK_Entries::set_winner( $id, 1 ); $map[ $day ][ $id ] = $direct_prize; } // 2) Regular draw from the remaining pool. A donation (> 0) adds a second // lot, so donors are twice as likely to be drawn (without replacement). $pool = array(); foreach ( $rest as $id ) { $pool[] = $id; if ( isset( $donations[ $id ] ) && (float) $donations[ $id ] > 0 ) { $pool[] = $id; // extra lot } } $picked = array(); while ( count( $picked ) < $winner_count && ! empty( $pool ) ) { $k = wp_rand( 0, count( $pool ) - 1 ); $id = (int) $pool[ $k ]; $picked[] = $id; $pool = array_values( array_filter( $pool, function ( $x ) use ( $id ) { return (int) $x !== $id; } ) ); } // Distribute the door's prizes over the drawn winners (round-robin). $titles = array(); foreach ( $prizes as $p ) { $title = isset( $p['title'] ) ? trim( (string) $p['title'] ) : ''; if ( '' !== $title ) { $titles[] = $title; } } $wc = max( 1, count( $picked ) ); $byWinner = array(); for ( $j = 0; $j < count( $titles ); $j++ ) { $byWinner[ $j % $wc ][] = $titles[ $j ]; } foreach ( $picked as $wi => $id ) { ADVK_Entries::set_winner( $id, 1 ); $map[ $day ][ $id ] = isset( $byWinner[ $wi ] ) ? implode( ', ', $byWinner[ $wi ] ) : ''; } self::save_prize_map( $map ); $this->redirect_back( sprintf( /* translators: 1: total winners, 2: door day, 3: number of direct wins. */ __( '%1$d Gewinner für Tag %2$d gezogen (davon %3$d Direktgewinne).', 'adventura-interactive-advent-calendar' ), count( $picked ) + count( $direct ), $day, count( $direct ) ) ); } /** * Full door data for a day (from the authoritative calendar), or null. * * @param int $day Door day. * @return array|null */ private function door_by_day( $day ) { foreach ( ADVK_Data::get_calendar() as $d ) { if ( (int) $d['day'] === (int) $day ) { return $d; } } return null; } /** @return array Winner→prize map: [ day => [ entry_id => prize label ] ]. */ private static function get_prize_map() { $m = get_option( 'advk_prize_map', array() ); return is_array( $m ) ? $m : array(); } /** @param array $map Winner→prize map to persist. */ private static function save_prize_map( $map ) { update_option( 'advk_prize_map', $map, false ); } /** * Prize label assigned to a winner entry (empty when none stored). * * @param int $day Door day. * @param int $entry_id Entry id. * @return string */ public static function prize_for_winner( $day, $entry_id ) { $m = self::get_prize_map(); return isset( $m[ (int) $day ][ (int) $entry_id ] ) ? (string) $m[ (int) $day ][ (int) $entry_id ] : ''; } /** * Assign (or, with an empty label, clear) the prize for a winner entry. * Used when a winner is picked/edited by hand from the entries list. * * @param int $day Door day. * @param int $entry_id Entry id. * @param string $label Prize label ('' clears the assignment). */ public static function set_prize_for_winner( $day, $entry_id, $label ) { $day = (int) $day; $entry_id = (int) $entry_id; $label = (string) $label; $m = self::get_prize_map(); if ( ! isset( $m[ $day ] ) || ! is_array( $m[ $day ] ) ) { $m[ $day ] = array(); } if ( '' === $label ) { unset( $m[ $day ][ $entry_id ] ); } else { $m[ $day ][ $entry_id ] = $label; } if ( empty( $m[ $day ] ) ) { unset( $m[ $day ] ); } self::save_prize_map( $m ); } /** * Remove any stored prize assignment for a winner entry. * * @param int $day Door day. * @param int $entry_id Entry id. */ public static function clear_prize_for_winner( $day, $entry_id ) { self::set_prize_for_winner( $day, $entry_id, '' ); } public function handle_toggle() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'adventura-interactive-advent-calendar' ) ); } check_admin_referer( 'advk_toggle_winner' ); $id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0; $flag = ! empty( $_POST['flag'] ) ? 1 : 0; if ( $id ) { ADVK_Entries::set_winner( $id, $flag ); } $this->redirect_back( $flag ? __( 'Als Gewinner markiert.', 'adventura-interactive-advent-calendar' ) : __( 'Gewinner-Markierung entfernt.', 'adventura-interactive-advent-calendar' ) ); } public function handle_clear() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'adventura-interactive-advent-calendar' ) ); } check_admin_referer( 'advk_clear_winners' ); $day = isset( $_POST['day'] ) ? (int) $_POST['day'] : 0; ADVK_Entries::clear_winners( $day ); $map = self::get_prize_map(); $notified = self::get_notified(); if ( $day >= 1 && $day <= ADVK_Data::door_count() ) { // Drop notified flags for this day's entries too. $ids = ADVK_Entries::query( array( 'per_page' => 100000, 'offset' => 0, 'day' => $day ) ); foreach ( $ids as $row ) { unset( $notified[ (int) $row->id ] ); } unset( $map[ $day ] ); } else { $map = array(); $notified = array(); } self::save_prize_map( $map ); self::save_notified( $notified ); $this->redirect_back( $day /* translators: %d: door day number (1–24). */ ? sprintf( __( 'Gewinner für Tag %d zurückgesetzt.', 'adventura-interactive-advent-calendar' ), $day ) : __( 'Alle Gewinner zurückgesetzt.', 'adventura-interactive-advent-calendar' ) ); } public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $opts = ADVK_Settings::get_options(); $correct_only = (int) $opts['winner_correct_only']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only list filter via bookmarkable GET link; page is capability-guarded above. $day_filter = isset( $_GET['advk_day'] ) ? (int) $_GET['advk_day'] : 0; $winners = ADVK_Entries::query( array( 'per_page' => 1000, 'offset' => 0, 'day' => $day_filter, 'winners_only' => true, 'orderby' => 'day', 'order' => 'ASC', ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin notice flag set by our own post-redirect; page is capability-guarded above. $notice = isset( $_GET['advk_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['advk_notice'] ) ) : ''; $export_url = wp_nonce_url( admin_url( 'admin-post.php?action=advk_export_winners' ), 'advk_export_winners' ); ?>
| day; ?> | first_name . ' ' . $w->last_name ) ); ?> | email ); ?> | day, $w->id ) ); ?> | correct_count; ?>/total_questions; ?> | losnr ); ?> |
id ); ?> ✓ — | |