Files
RR-Door-Voting/door-status-voting.php
2026-02-07 13:06:39 +01:00

736 lines
30 KiB
PHP

<?php
/**
* Plugin Name: Restaurant Tür-Status
* Plugin URI: https://lucas-orth.de
* Description: Interaktives Voting-Widget mit animierter Tür - Besucher können abstimmen ob ein Restaurant geöffnet oder geschlossen ist
* Version: 1.1.0
* Author: Lucas Orth
* Author URI: https://lucas-orth.de
* License: GPL v2 or later
* Text Domain: door-status
*/
if (!defined('ABSPATH')) {
exit;
}
define('DSV_VERSION', '1.0.0');
define('DSV_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('DSV_PLUGIN_URL', plugin_dir_url(__FILE__));
class DoorStatusVoting {
private static $instance = null;
public static function get_instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Frontend
add_shortcode('door_status', [$this, 'render_shortcode']);
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
// AJAX Handlers
add_action('wp_ajax_dsv_vote', [$this, 'handle_vote']);
add_action('wp_ajax_nopriv_dsv_vote', [$this, 'handle_vote']);
add_action('wp_ajax_dsv_remove_vote', [$this, 'handle_remove_vote']);
add_action('wp_ajax_nopriv_dsv_remove_vote', [$this, 'handle_remove_vote']);
add_action('wp_ajax_dsv_get_status', [$this, 'get_status']);
add_action('wp_ajax_nopriv_dsv_get_status', [$this, 'get_status']);
// Admin
add_action('admin_menu', [$this, 'add_admin_menu']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
add_action('wp_ajax_dsv_save_settings', [$this, 'save_settings']);
add_action('wp_ajax_dsv_reset_votes', [$this, 'reset_votes']);
add_action('wp_ajax_dsv_reset_votes_by_type', [$this, 'reset_votes_by_type']);
add_action('wp_ajax_dsv_update_meta_status', [$this, 'update_meta_status']);
}
// =========================================================================
// FRONTEND
// =========================================================================
/**
* Assets für Frontend laden
*/
public function enqueue_frontend_assets() {
wp_enqueue_style(
'dsv-frontend',
DSV_PLUGIN_URL . 'assets/css/frontend.css',
[],
DSV_VERSION
);
wp_enqueue_script(
'dsv-frontend',
DSV_PLUGIN_URL . 'assets/js/frontend.js',
[],
DSV_VERSION,
true
);
wp_localize_script('dsv-frontend', 'dsvConfig', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('dsv_nonce')
]);
}
/**
* Shortcode rendern
*/
public function render_shortcode($atts) {
$atts = shortcode_atts([
'post_id' => get_the_ID()
], $atts);
$post_id = intval($atts['post_id']);
// Prüfen ob für diesen Post deaktiviert
$disabled_posts = get_option('dsv_disabled_posts', []);
if (in_array($post_id, $disabled_posts)) {
return '';
}
// Votes laden
$votes = $this->get_votes($post_id);
$total = $votes['open'] + $votes['closed'];
$open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50;
// Status ermitteln
if ($votes['open'] > $votes['closed']) {
$status = 'open';
$status_text = 'GEÖFFNET';
$status_icon = '🟢';
} elseif ($votes['closed'] > $votes['open']) {
$status = 'closed';
$status_text = 'GESCHLOSSEN';
$status_icon = '🔴';
} else {
$status = 'tied';
$status_text = 'UNKLAR';
$status_icon = '🟡';
}
// User Vote aus Cookie prüfen
$cookie_name = 'dsv_vote_' . $post_id;
$user_vote = isset($_COOKIE[$cookie_name]) ? sanitize_text_field($_COOKIE[$cookie_name]) : null;
$has_voted = !empty($user_vote);
ob_start();
?>
<div class="dsv-widget"
data-post-id="<?php echo esc_attr($post_id); ?>"
data-status="<?php echo esc_attr($status); ?>"
data-has-voted="<?php echo $has_voted ? 'true' : 'false'; ?>"
data-user-vote="<?php echo esc_attr($user_vote); ?>">
<h3 class="dsv-title">Ist das Restaurant noch geöffnet?</h3>
<div class="dsv-main-row">
<!-- Tür Container -->
<div class="dsv-door-container">
<!-- Status Badge -->
<div class="dsv-status-badge dsv-status-<?php echo esc_attr($status); ?>">
<?php echo $status_icon; ?> <?php echo esc_html($status_text); ?>
</div>
<!-- Türrahmen -->
<div class="dsv-door-frame">
<!-- Licht hinter der Tür -->
<div class="dsv-light"></div>
<!-- Lichtstrahlen -->
<div class="dsv-light-rays">
<div class="dsv-ray"></div>
<div class="dsv-ray"></div>
<div class="dsv-ray"></div>
<div class="dsv-ray"></div>
<div class="dsv-ray"></div>
</div>
<!-- Die Tür -->
<div class="dsv-door">
<div class="dsv-door-grain"></div>
<div class="dsv-door-panel dsv-door-panel-top"></div>
<div class="dsv-door-panel dsv-door-panel-bottom"></div>
<div class="dsv-door-handle">
<div class="dsv-handle-plate"></div>
<div class="dsv-handle-lever"></div>
</div>
</div>
</div>
<!-- Schatten -->
<div class="dsv-shadow"></div>
</div>
<!-- Rechte Seite: Voting -->
<div class="dsv-voting-section">
<!-- Fortschrittsbalken -->
<div class="dsv-progress-container">
<div class="dsv-progress-bar">
<div class="dsv-progress-open" style="width: <?php echo $open_percent; ?>%">
<?php if ($open_percent > 15): ?>
<span class="dsv-progress-count"><?php echo $votes['open']; ?></span>
<?php endif; ?>
</div>
<div class="dsv-progress-closed" style="width: <?php echo 100 - $open_percent; ?>%">
<?php if ((100 - $open_percent) > 15): ?>
<span class="dsv-progress-count"><?php echo $votes['closed']; ?></span>
<?php endif; ?>
</div>
<div class="dsv-progress-divider"></div>
</div>
<div class="dsv-progress-labels">
<span class="dsv-label-open">Geöffnet (<?php echo $open_percent; ?>%)</span>
<span class="dsv-label-closed">Geschlossen (<?php echo 100 - $open_percent; ?>%)</span>
</div>
</div>
<!-- Voting Buttons -->
<div class="dsv-buttons <?php echo $has_voted ? 'dsv-voted' : ''; ?>">
<?php if (!$has_voted): ?>
<button type="button" class="dsv-btn dsv-btn-open" data-vote="open">
<span class="dsv-btn-icon">✓</span>
<span class="dsv-btn-text">Geöffnet</span>
</button>
<button type="button" class="dsv-btn dsv-btn-closed" data-vote="closed">
<span class="dsv-btn-icon">✗</span>
<span class="dsv-btn-text">Geschlossen</span>
</button>
<?php else: ?>
<div class="dsv-voted-message dsv-voted-<?php echo esc_attr($user_vote); ?>">
<span class="dsv-voted-icon"><?php echo $user_vote === 'open' ? '✓' : '✗'; ?></span>
<span class="dsv-voted-text">
Du hast für "<?php echo $user_vote === 'open' ? 'Geöffnet' : 'Geschlossen'; ?>" gestimmt
</span>
<button type="button" class="dsv-remove-vote" title="Stimme zurücknehmen">✕</button>
</div>
<?php endif; ?>
</div>
<!-- Teilnehmer Info -->
<p class="dsv-info">
<span class="dsv-total"><?php echo $total; ?></span> Besucher haben abgestimmt
</p>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Votes aus Post Meta laden
*/
private function get_votes($post_id) {
$data = get_post_meta($post_id, '_dsv_votes', true);
if (!is_array($data)) {
return ['open' => 0, 'closed' => 0];
}
$open = $closed = 0;
foreach ($data as $vote) {
if ($vote === 'open') $open++;
if ($vote === 'closed') $closed++;
}
return ['open' => $open, 'closed' => $closed];
}
/**
* User-Key generieren (IP Hash)
*/
private function get_user_key() {
$ip = '';
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} else {
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
return md5($ip . wp_salt());
}
/**
* AJAX: Vote speichern
*/
public function handle_vote() {
check_ajax_referer('dsv_nonce', 'nonce');
$post_id = intval($_POST['post_id'] ?? 0);
$vote = sanitize_text_field($_POST['vote'] ?? '');
if (!$post_id || !in_array($vote, ['open', 'closed'])) {
wp_send_json_error(['message' => 'Ungültige Anfrage']);
}
// Prüfen ob deaktiviert
$disabled_posts = get_option('dsv_disabled_posts', []);
if (in_array($post_id, $disabled_posts)) {
wp_send_json_error(['message' => 'Voting deaktiviert']);
}
$user_key = $this->get_user_key();
$data = get_post_meta($post_id, '_dsv_votes', true);
if (!is_array($data)) $data = [];
// Bereits gevotet? (Server-side Check)
if (isset($data[$user_key])) {
wp_send_json_error(['message' => 'Bereits abgestimmt']);
}
// Vote speichern
$data[$user_key] = $vote;
update_post_meta($post_id, '_dsv_votes', $data);
// Cookie setzen (30 Tage)
$cookie_name = 'dsv_vote_' . $post_id;
setcookie($cookie_name, $vote, time() + (30 * DAY_IN_SECONDS), COOKIEPATH, COOKIE_DOMAIN);
// Neue Votes zurückgeben
$votes = $this->get_votes($post_id);
$total = $votes['open'] + $votes['closed'];
$open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50;
wp_send_json_success([
'votes' => $votes,
'total' => $total,
'openPercent' => $open_percent,
'status' => $votes['open'] > $votes['closed'] ? 'open' :
($votes['closed'] > $votes['open'] ? 'closed' : 'tied')
]);
}
/**
* AJAX: Aktuellen Status abrufen
*/
public function get_status() {
$post_id = intval($_GET['post_id'] ?? 0);
if (!$post_id) {
wp_send_json_error(['message' => 'Ungültige Post ID']);
}
$votes = $this->get_votes($post_id);
$total = $votes['open'] + $votes['closed'];
$open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50;
wp_send_json_success([
'votes' => $votes,
'total' => $total,
'openPercent' => $open_percent,
'status' => $votes['open'] > $votes['closed'] ? 'open' :
($votes['closed'] > $votes['open'] ? 'closed' : 'tied')
]);
}
/**
* AJAX: Vote entfernen
*/
public function handle_remove_vote() {
check_ajax_referer('dsv_nonce', 'nonce');
$post_id = intval($_POST['post_id'] ?? 0);
if (!$post_id) {
wp_send_json_error(['message' => 'Ungültige Anfrage']);
}
$user_key = $this->get_user_key();
$data = get_post_meta($post_id, '_dsv_votes', true);
if (!is_array($data) || !isset($data[$user_key])) {
wp_send_json_error(['message' => 'Kein Vote gefunden']);
}
// Vote entfernen
unset($data[$user_key]);
update_post_meta($post_id, '_dsv_votes', $data);
// Cookie löschen
$cookie_name = 'dsv_vote_' . $post_id;
setcookie($cookie_name, '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN);
// Neue Votes zurückgeben
$votes = $this->get_votes($post_id);
$total = $votes['open'] + $votes['closed'];
$open_percent = $total > 0 ? round(($votes['open'] / $total) * 100) : 50;
wp_send_json_success([
'votes' => $votes,
'total' => $total,
'openPercent' => $open_percent,
'status' => $votes['open'] > $votes['closed'] ? 'open' :
($votes['closed'] > $votes['open'] ? 'closed' : 'tied')
]);
}
// =========================================================================
// ADMIN
// =========================================================================
/**
* Admin Menü unter Werkzeuge
*/
public function add_admin_menu() {
add_management_page(
'Tür-Status Voting',
'Tür-Status',
'manage_options',
'door-status-settings',
[$this, 'render_admin_page']
);
}
/**
* Admin Assets laden
*/
public function enqueue_admin_assets($hook) {
if ($hook !== 'tools_page_door-status-settings') {
return;
}
wp_enqueue_style(
'dsv-admin',
DSV_PLUGIN_URL . 'assets/css/admin.css',
[],
DSV_VERSION
);
wp_enqueue_script(
'dsv-admin',
DSV_PLUGIN_URL . 'assets/js/admin.js',
['jquery'],
DSV_VERSION,
true
);
wp_localize_script('dsv-admin', 'dsvAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('dsv_admin_nonce')
]);
}
/**
* Admin Seite rendern
*/
public function render_admin_page() {
if (!current_user_can('manage_options')) {
wp_die('Keine Berechtigung');
}
$disabled_posts = get_option('dsv_disabled_posts', []);
// Alle Posts mit Votes holen
global $wpdb;
$posts_with_votes = $wpdb->get_col(
"SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_dsv_votes'"
);
?>
<div class="wrap dsv-admin-wrap">
<h1>🚪 Tür-Status Voting</h1>
<div class="dsv-admin-grid">
<!-- Shortcode Info -->
<div class="dsv-admin-card">
<h2>Shortcode</h2>
<p>Füge das Voting-Widget mit folgendem Shortcode ein:</p>
<code class="dsv-shortcode">[door_status]</code>
<p class="description">
Der Shortcode verwendet automatisch die aktuelle Post-ID.
Du kannst ihn in Posts, Seiten oder Page-Builder einfügen.
</p>
</div>
<!-- Deaktivierte Posts -->
<div class="dsv-admin-card">
<h2>Voting deaktivieren</h2>
<p>Gib Post-IDs ein, für die das Voting deaktiviert werden soll (kommagetrennt):</p>
<form id="dsv-settings-form">
<textarea
name="disabled_posts"
id="dsv-disabled-posts"
class="large-text"
rows="3"
placeholder="z.B. 123, 456, 789"
><?php echo esc_textarea(implode(', ', $disabled_posts)); ?></textarea>
<p class="description">
Der Shortcode wird auf diesen Seiten nichts ausgeben.
</p>
<p>
<button type="submit" class="button button-primary">
Einstellungen speichern
</button>
<span class="dsv-save-status"></span>
</p>
</form>
</div>
<!-- Statistiken -->
<div class="dsv-admin-card dsv-admin-card-full">
<h2>Voting Übersicht</h2>
<?php if (empty($posts_with_votes)): ?>
<p class="dsv-no-data">Noch keine Votes vorhanden.</p>
<?php else: ?>
<p>
<button type="button" id="dsv-filter-mismatch" class="button">
Nur Abweichungen anzeigen
</button>
</p>
<table class="wp-list-table widefat fixed striped" id="dsv-votes-table">
<thead>
<tr>
<th>Post</th>
<th>Meta-Status</th>
<th>Voting-Status</th>
<th>Geöffnet</th>
<th>Geschlossen</th>
<th>Gesamt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<?php foreach ($posts_with_votes as $pid):
$post = get_post($pid);
if (!$post) continue;
$votes = $this->get_votes($pid);
$total = $votes['open'] + $votes['closed'];
$is_disabled = in_array($pid, $disabled_posts);
if ($votes['open'] > $votes['closed']) {
$status_badge = '<span class="dsv-badge dsv-badge-open">Geöffnet</span>';
} elseif ($votes['closed'] > $votes['open']) {
$status_badge = '<span class="dsv-badge dsv-badge-closed">Geschlossen</span>';
} else {
$status_badge = '<span class="dsv-badge dsv-badge-tied">Unklar</span>';
}
// JetEngine Meta-Feld "status" auslesen
$meta_status = get_post_meta($pid, 'status', true);
$meta_status_lower = mb_strtolower(trim($meta_status));
if (in_array($meta_status_lower, ['geöffnet', 'geoeffnet', 'open', 'offen'])) {
$meta_badge_class = 'dsv-badge-open';
$meta_voting_match = ($votes['open'] > $votes['closed']);
} elseif (in_array($meta_status_lower, ['geschlossen', 'closed', 'zu'])) {
$meta_badge_class = 'dsv-badge-closed';
$meta_voting_match = ($votes['closed'] > $votes['open']);
} elseif (in_array($meta_status_lower, ['neuer betreiber'])) {
$meta_badge_class = 'dsv-badge-tied';
$meta_voting_match = false;
} elseif (!empty($meta_status)) {
$meta_badge_class = 'dsv-badge-tied';
$meta_voting_match = false;
} else {
$meta_badge_class = '';
$meta_voting_match = true; // kein Meta = kein Abweichung
}
if (!empty($meta_status)) {
$meta_status_display = '<span class="dsv-badge ' . $meta_badge_class . '">' . esc_html($meta_status) . '</span>';
} else {
$meta_status_display = '<span class="dsv-meta-empty">—</span>';
}
$row_mismatch = !$meta_voting_match;
?>
<tr data-post-id="<?php echo esc_attr($pid); ?>"<?php if ($row_mismatch): ?> class="dsv-mismatch"<?php endif; ?>>
<td>
<strong><a href="<?php echo get_edit_post_link($pid); ?>" target="_blank"><?php echo esc_html($post->post_title); ?></a></strong>
<br>
<small>ID: <?php echo $pid; ?></small>
<?php if ($is_disabled): ?>
<br><em class="dsv-disabled-label">Voting deaktiviert</em>
<?php endif; ?>
</td>
<td class="dsv-meta-status-cell">
<?php echo $meta_status_display; ?>
<div class="dsv-meta-buttons">
<button type="button" class="button button-small dsv-meta-set-btn<?php echo ($meta_status_lower === 'geöffnet') ? ' active' : ''; ?>" data-post-id="<?php echo esc_attr($pid); ?>" data-status="geöffnet" title="Geöffnet">🟢</button>
<button type="button" class="button button-small dsv-meta-set-btn<?php echo ($meta_status_lower === 'geschlossen') ? ' active' : ''; ?>" data-post-id="<?php echo esc_attr($pid); ?>" data-status="geschlossen" title="Geschlossen">🔴</button>
</div>
</td>
<td><?php echo $status_badge; ?></td>
<td class="dsv-count-open"><?php echo $votes['open']; ?></td>
<td class="dsv-count-closed"><?php echo $votes['closed']; ?></td>
<td class="dsv-count-total"><?php echo $total; ?></td>
<td class="dsv-actions-cell">
<div class="dsv-action-buttons">
<button type="button"
class="button button-small dsv-reset-type-btn"
data-post-id="<?php echo esc_attr($pid); ?>"
data-type="open"
title="Alle 'Geöffnet' Stimmen löschen">
🟢 Löschen
</button>
<button type="button"
class="button button-small dsv-reset-type-btn"
data-post-id="<?php echo esc_attr($pid); ?>"
data-type="closed"
title="Alle 'Geschlossen' Stimmen löschen">
🔴 Löschen
</button>
<button type="button"
class="button button-small dsv-reset-btn"
data-post-id="<?php echo esc_attr($pid); ?>"
title="Alle Stimmen löschen">
Alle löschen
</button>
<a href="<?php echo get_permalink($pid); ?>"
target="_blank"
class="button button-small">
Ansehen
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<?php
}
/**
* AJAX: Einstellungen speichern
*/
public function save_settings() {
check_ajax_referer('dsv_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Keine Berechtigung']);
}
$disabled_posts_raw = sanitize_text_field($_POST['disabled_posts'] ?? '');
$disabled_posts = [];
if (!empty($disabled_posts_raw)) {
$ids = explode(',', $disabled_posts_raw);
foreach ($ids as $id) {
$id = intval(trim($id));
if ($id > 0) {
$disabled_posts[] = $id;
}
}
}
update_option('dsv_disabled_posts', $disabled_posts);
wp_send_json_success(['message' => 'Gespeichert']);
}
/**
* AJAX: Votes zurücksetzen
*/
public function reset_votes() {
check_ajax_referer('dsv_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Keine Berechtigung']);
}
$post_id = intval($_POST['post_id'] ?? 0);
if (!$post_id) {
wp_send_json_error(['message' => 'Ungültige Post ID']);
}
delete_post_meta($post_id, '_dsv_votes');
wp_send_json_success(['message' => 'Votes zurückgesetzt']);
}
/**
* AJAX: Votes nach Typ zurücksetzen (nur open oder nur closed)
*/
public function reset_votes_by_type() {
check_ajax_referer('dsv_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Keine Berechtigung']);
}
$post_id = intval($_POST['post_id'] ?? 0);
$type = sanitize_text_field($_POST['type'] ?? '');
if (!$post_id || !in_array($type, ['open', 'closed'])) {
wp_send_json_error(['message' => 'Ungültige Parameter']);
}
$data = get_post_meta($post_id, '_dsv_votes', true);
if (!is_array($data)) {
wp_send_json_error(['message' => 'Keine Votes vorhanden']);
}
// Nur Votes des angegebenen Typs entfernen
$removed = 0;
foreach ($data as $key => $vote) {
if ($vote === $type) {
unset($data[$key]);
$removed++;
}
}
update_post_meta($post_id, '_dsv_votes', $data);
// Neue Zahlen berechnen
$votes = $this->get_votes($post_id);
wp_send_json_success([
'message' => $removed . ' Stimmen gelöscht',
'votes' => $votes,
'removed' => $removed
]);
}
/**
* AJAX: Meta-Status ändern
*/
public function update_meta_status() {
check_ajax_referer('dsv_admin_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Keine Berechtigung']);
}
$post_id = intval($_POST['post_id'] ?? 0);
$status = sanitize_text_field($_POST['status'] ?? '');
if (!$post_id || !in_array($status, ['geöffnet', 'geschlossen'])) {
wp_send_json_error(['message' => 'Ungültige Parameter']);
}
update_post_meta($post_id, 'status', $status);
wp_send_json_success(['message' => 'Status aktualisiert', 'status' => $status]);
}
}
// Plugin initialisieren
DoorStatusVoting::get_instance();
// Aktivierungs-Hook
register_activation_hook(__FILE__, function() {
add_option('dsv_disabled_posts', []);
});
// Deaktivierungs-Hook
register_deactivation_hook(__FILE__, function() {
// Optional: Daten behalten oder löschen
});