Files
wp-multi-ticket/wp-multi-ticket.php

1332 lines
75 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/*
Plugin Name: WP Multi Ticket Pro (Secure Guest)
Description: Version 10.5 Fix: Syntax Error in get_option + Chart.js Verbesserungen.
Version: 1.0
Author: M_Viper
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class WP_Multi_Ticket_Pro {
private $table_tickets;
private $table_messages;
public function __construct() {
global $wpdb;
$this->table_tickets = $wpdb->prefix . 'wmt_tickets';
$this->table_messages = $wpdb->prefix . 'wmt_messages';
register_activation_hook( __FILE__, array( $this, 'create_tables' ) );
add_action( 'plugins_loaded', array( $this, 'check_db_update' ) );
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'load_analytics_scripts' ) );
add_shortcode( 'wmt_form', array( $this, 'render_creation_form' ) );
add_shortcode( 'wmt_view', array( $this, 'render_ticket_view' ) );
add_shortcode( 'wmt_lookup', array( $this, 'render_lookup_form' ) );
add_action( 'init', array( $this, 'handle_guest_creation' ) );
add_action( 'init', array( $this, 'handle_guest_reply' ) );
add_action( 'init', array( $this, 'handle_guest_lookup' ) );
add_action( 'admin_post_wmt_admin_reply', array( $this, 'handle_admin_post' ) );
add_action( 'admin_init', array( $this, 'handle_delete_ticket' ) );
add_action( 'admin_init', array( $this, 'handle_csv_export' ) );
}
// *** HIER: korrigierter Hook-Name für die Analytics-Unterseite ***
public function load_analytics_scripts( $hook ) {
// Für Submenu: parent slug wmt_tickets -> Hook: wmt-tickets_page_wmt_analytics
if ( 'wmt-tickets_page_wmt_analytics' !== $hook ) return;
$local_js = plugin_dir_path( __FILE__ ) . 'chart.js';
$js_url = plugins_url( 'chart.js', __FILE__ );
if ( file_exists( $local_js ) ) {
wp_enqueue_script( 'chartjs', $js_url, array(), '4.4.0', true );
}
}
public function enqueue_styles() {
?>
<style>
.wmt-box { max-width: 800px; margin: 20px auto; background: #fff; padding: 25px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; }
.wmt-btn { background: #0073aa; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; text-decoration: none; display: inline-block; line-height: 1.4; }
.wmt-btn:hover { background: #005177; }
.wmt-btn.danger { background: #dc3545; }
.wmt-btn.danger:hover { background: #a71d2a; }
.wmt-input, .wmt-select, .wmt-textarea { width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
.wmt-label { display: block; font-weight: bold; margin-bottom:5px; }
.wmt-chat { background: #f4f6f8; padding: 20px; border-radius: 8px; margin-bottom: 20px; max-height: 600px; overflow-y: auto; border: 1px solid #eee; }
.wmt-bubble { margin-bottom: 15px; clear: both; padding: 15px; border-radius: 12px; max-width: 80%; position: relative; font-size: 14px; line-height:1.5; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.wmt-left { float: left; background: #fff; border-bottom-left-radius: 2px; }
.wmt-right { float: right; background: #e3f2fd; border-bottom-right-radius: 2px; border: 1px solid #bbdefb; }
.wmt-meta { font-size: 11px; color: #888; margin-top: 8px; display: block; }
.wmt-system-msg { text-align: center; margin: 20px 0; font-size: 12px; color: #666; background: #e9ecef; padding: 5px 10px; border-radius: 20px; display: inline-block; width: 100%; box-sizing: border-box; border: 1px solid #ced4da; }
.wmt-internal-note { background: #eee; border-left: 4px solid #6c757d; padding: 10px; margin-bottom: 10px; font-size: 12px; color: #333; font-style: italic; display: block; }
.wmt-internal-note strong { display: block; font-style: normal; color: #495057; margin-bottom: 3px; }
.wmt-alert { padding: 15px; background: #fff3cd; color: #856404; border: 1px solid #ffeeba; margin-bottom: 20px; border-radius: 4px; }
.wmt-error { padding: 15px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; margin-bottom: 20px; border-radius: 4px; }
.wmt-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: bold; font-size: 14px; margin-right: 10px; flex-shrink: 0; text-transform: uppercase; }
.wmt-msg-row { display: flex; align-items: flex-start; margin-bottom: 15px; }
.wmt-export-btn, .wmt-print-btn { float: right; margin-top: 5px; background: #28a745; }
.wmt-print-btn { background: #6c757d; }
.wmt-template-box { border: 1px solid #ccc; padding: 15px; background: #f9f9f9; margin-bottom: 10px; border-radius: 4px; position: relative; }
.wmt-tpl-row { display: flex; gap: 10px; margin-bottom: 5px; }
.wmt-tpl-row select, .wmt-tpl-row input { margin-bottom: 0; width: auto; }
.wmt-tpl-remove { position: absolute; top: 10px; right: 10px; color: #a00; text-decoration: none; font-weight: bold; cursor: pointer; }
.wmt-tpl-remove:hover { color: #d00; }
/* Analytics Styles */
.wmt-analytics-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.wmt-analytics-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
.wmt-stat-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: transform 0.2s; }
.wmt-stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.wmt-stat-card h3 { margin: 0 0 5px 0; font-size: 13px; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.wmt-stat-card .number { font-size: 28px; font-weight: 700; color: #111827; margin: 0; line-height: 1.2; }
.wmt-stat-card .trend { font-size: 12px; margin-top: 5px; color: #6b7280; }
.wmt-stat-card.blue { border-top: 4px solid #3b82f6; }
.wmt-stat-card.red { border-top: 4px solid #ef4444; }
.wmt-stat-card.green { border-top: 4px solid #10b981; }
.wmt-stat-card.orange { border-top: 4px solid #f59e0b; }
.wmt-charts-wrapper { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
.wmt-chart-container { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
.wmt-chart-container h3 { margin-top: 0; color: #374151; font-size: 16px; border-bottom: 1px solid #f3f4f6; padding-bottom: 10px; }
.wmt-full-chart { grid-column: span 2; }
@media (max-width: 1200px) {
.wmt-analytics-grid { grid-template-columns: repeat(2, 1fr); }
.wmt-charts-wrapper { grid-template-columns: 1fr; }
.wmt-full-chart { grid-column: span 1; }
}
@media (max-width: 600px) {
.wmt-analytics-grid { grid-template-columns: 1fr; }
}
/* CSS Fallback Styles */
.wmt-css-chart-row { display: flex; align-items: center; margin-bottom: 8px; }
.wmt-css-label { width: 120px; font-size: 13px; font-weight: 600; text-align: right; margin-right: 10px; }
.wmt-css-bar-bg { flex-grow: 1; background: #f3f4f6; height: 24px; border-radius: 4px; overflow: hidden; position: relative; }
.wmt-css-bar-fill { height: 100%; background: #3b82f6; display: flex; align-items: center; padding-left: 10px; color: #fff; font-size: 11px; white-space: nowrap; transition: width 0.5s ease; }
.wmt-css-bar-val { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 11px; color: #6b7280; font-weight: bold; }
</style>
<?php
}
public function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql_tickets = "CREATE TABLE $this->table_tickets (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
category varchar(100) DEFAULT 'Allgemein',
department varchar(100) DEFAULT NULL,
status varchar(50) DEFAULT 'Offen',
priority varchar(50) DEFAULT 'Mittel',
guest_name varchar(100) NOT NULL,
guest_email varchar(100) NOT NULL,
ticket_hash varchar(64) NOT NULL,
assigned_to bigint(20) UNSIGNED DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY assigned_to (assigned_to)
) $charset_collate;";
$sql_messages = "CREATE TABLE $this->table_messages (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
ticket_id bigint(20) UNSIGNED NOT NULL,
sender_name varchar(100) NOT NULL,
sender_type varchar(20) NOT NULL,
message longtext DEFAULT NULL,
internal_note text DEFAULT NULL,
file_url varchar(255) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql_tickets );
dbDelta( $sql_messages );
}
public function check_db_update() {
global $wpdb;
$cols = $wpdb->get_col( "SHOW COLUMNS FROM $this->table_tickets" );
if( !in_array('ticket_hash', $cols) ) $wpdb->query( "ALTER TABLE $this->table_tickets ADD COLUMN ticket_hash varchar(64) NOT NULL AFTER guest_email" );
if( !in_array('guest_name', $cols) ) { $wpdb->query( "ALTER TABLE $this->table_tickets ADD COLUMN guest_name varchar(100) NOT NULL" ); $wpdb->query( "ALTER TABLE $this->table_tickets ADD COLUMN guest_email varchar(100) NOT NULL" ); }
if( !in_array('department', $cols) ) $wpdb->query( "ALTER TABLE $this->table_tickets ADD COLUMN department varchar(100) DEFAULT NULL AFTER category" );
$mcols = $wpdb->get_col( "SHOW COLUMNS FROM $this->table_messages" );
if( !in_array('sender_type', $mcols) ) { $wpdb->query( "ALTER TABLE $this->table_messages ADD COLUMN sender_name varchar(100) NOT NULL" ); $wpdb->query( "ALTER TABLE $this->table_messages ADD COLUMN sender_type varchar(20) NOT NULL" ); }
if( !in_array('internal_note', $mcols) ) $wpdb->query( "ALTER TABLE $this->table_messages ADD COLUMN internal_note text DEFAULT NULL AFTER message" );
if( !in_array('file_url', $mcols) ) $wpdb->query( "ALTER TABLE $this->table_messages ADD COLUMN file_url varchar(255) DEFAULT NULL AFTER internal_note" );
$wpdb->query( "ALTER TABLE $this->table_messages MODIFY COLUMN message longtext DEFAULT NULL" );
}
public function add_admin_menu() {
add_menu_page( 'Tickets', 'Tickets Pro', 'manage_options', 'wmt_tickets', array( $this, 'render_admin_page' ), 'dashicons-tickets-alt', 30 );
add_submenu_page( 'wmt_tickets', 'Übersicht', 'Übersicht', 'manage_options', 'wmt_tickets', array( $this, 'render_admin_page' ) );
add_submenu_page( 'wmt_tickets', 'Analytics', 'Analytics', 'manage_options', 'wmt_analytics', array( $this, 'render_analytics_page' ) );
add_submenu_page( 'wmt_tickets', 'Einstellungen', 'Einstellungen', 'manage_options', 'wmt_settings', array( $this, 'render_settings_page' ) );
add_submenu_page( 'wmt_tickets', 'Benachrichtigungen', 'Benachrichtigungen', 'manage_options', 'wmt_notifications', array( $this, 'render_notifications_page' ) );
}
public function register_settings() {
register_setting( 'wmt_settings_group', 'wmt_categories' );
register_setting( 'wmt_settings_group', 'wmt_departments' );
register_setting( 'wmt_settings_group', 'wmt_priorities' );
register_setting( 'wmt_settings_group', 'wmt_statuses' );
register_setting( 'wmt_settings_group', 'wmt_admin_email' );
register_setting( 'wmt_settings_group', 'wmt_templates' );
register_setting( 'wmt_settings_group', 'wmt_allowed_filetypes' );
register_setting( 'wmt_notifications_group', 'wmt_discord_webhook' );
register_setting( 'wmt_notifications_group', 'wmt_telegram_token' );
register_setting( 'wmt_notifications_group', 'wmt_telegram_chat_id' );
register_setting( 'wmt_notifications_group', 'wmt_new_ticket_notify_users' );
}
public function render_analytics_page() {
global $wpdb;
$total_tickets = $wpdb->get_var("SELECT COUNT(*) FROM $this->table_tickets");
if ( $total_tickets == 0 ) {
echo '<div class="wrap">';
echo '<div style="background: #fff; padding: 50px; text-align: center; border-radius: 8px; border: 1px solid #eee; max-width: 600px; margin: 50px auto;">';
echo '<h2 style="color: #555;">Noch keine Daten</h2>';
echo '<p style="color: #777; font-size: 16px;">Es wurden noch keine Tickets erstellt.</p>';
echo '<a href="' . admin_url('admin.php?page=wmt_tickets') . '" class="wmt-btn">Zu den Tickets</a>';
echo '</div></div>';
return;
}
$open_tickets = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $this->table_tickets WHERE status NOT LIKE %s", '%Geschlossen%'));
$closed_tickets = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $this->table_tickets WHERE status LIKE %s", '%Geschlossen%'));
$new_tickets_30d = $wpdb->get_var("SELECT COUNT(*) FROM $this->table_tickets WHERE created_at >= DATE_SUB(DATE_ADD(NOW(), INTERVAL 2 YEAR), INTERVAL 30 DAY)");
$status_data = $wpdb->get_results("SELECT COALESCE(status, 'Unbekannt') as status, COUNT(*) as count FROM $this->table_tickets GROUP BY status");
$cat_data = $wpdb->get_results("SELECT COALESCE(category, 'Keine Kategorie') as category, COUNT(*) as count FROM $this->table_tickets GROUP BY category ORDER BY count DESC LIMIT 8");
$agent_data = $wpdb->get_results("
SELECT u.display_name, COUNT(t.id) as count
FROM {$wpdb->users} u
LEFT JOIN $this->table_tickets t ON u.ID = t.assigned_to
GROUP BY u.ID
HAVING count > 0
ORDER BY count DESC
");
$timeline_data = $wpdb->get_results("
SELECT DATE(created_at) as date, COUNT(*) as count
FROM $this->table_tickets
GROUP BY DATE(created_at)
ORDER BY date ASC
");
?>
<div class="wrap">
<div class="wmt-analytics-header">
<h1>Analytics Dashboard</h1>
<span style="color: #666; font-size: 14px;">Live Daten</span>
</div>
<div class="wmt-analytics-grid">
<div class="wmt-stat-card blue">
<h3>Gesamt Tickets</h3>
<p class="number"><?php echo intval($total_tickets); ?></p>
</div>
<div class="wmt-stat-card red">
<h3>Aktiv / Offen</h3>
<p class="number"><?php echo intval($open_tickets); ?></p>
<p class="trend">Benötigt Aufmerksamkeit</p>
</div>
<div class="wmt-stat-card green">
<h3>Geschlossen</h3>
<p class="number"><?php echo intval($closed_tickets); ?></p>
</div>
<div class="wmt-stat-card orange">
<h3>Letzte 30 Tage</h3>
<p class="number"><?php echo intval($new_tickets_30d); ?></p>
<p class="trend">Neue Eingänge</p>
</div>
</div>
<div class="wmt-charts-wrapper">
<!-- Status -->
<div class="wmt-chart-container">
<h3>Status Verteilung</h3>
<div style="height: 250px; position: relative;"><canvas id="statusChart"></canvas></div>
</div>
<!-- Kategorien -->
<div class="wmt-chart-container">
<h3>Top Kategorien</h3>
<div style="height: 250px; position: relative;"><canvas id="catChart"></canvas></div>
</div>
<!-- Agent Performance -->
<div class="wmt-chart-container">
<h3>Workload pro Agent</h3>
<div style="height: 250px; position: relative;"><canvas id="agentChart"></canvas></div>
</div>
<!-- Timeline -->
<div class="wmt-chart-container">
<h3>Tickets pro Tag</h3>
<div style="height: 250px; position: relative;"><canvas id="timelineChart"></canvas></div>
</div>
</div>
</div>
<!-- Chart.js Fallback CDN (für 100% Funktionalität) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script type="text/javascript">
// Warten, bis Dokument und Skripte geladen sind
document.addEventListener('DOMContentLoaded', function() {
const statusLabels = <?php echo json_encode(array_column($status_data, 'status'), JSON_UNESCAPED_UNICODE); ?>;
const statusCounts = <?php echo json_encode(array_column($status_data, 'count'), JSON_UNESCAPED_UNICODE); ?>;
const catLabels = <?php echo json_encode(array_column($cat_data, 'category'), JSON_UNESCAPED_UNICODE); ?>;
const catCounts = <?php echo json_encode(array_column($cat_data, 'count'), JSON_UNESCAPED_UNICODE); ?>;
const agentLabels = <?php echo json_encode(array_column($agent_data, 'display_name'), JSON_UNESCAPED_UNICODE); ?>;
const agentCounts = <?php echo json_encode(array_column($agent_data, 'count'), JSON_UNESCAPED_UNICODE); ?>;
const timeLabels = <?php echo json_encode(array_column($timeline_data, 'date'), JSON_UNESCAPED_UNICODE); ?>;
const timeCounts = <?php echo json_encode(array_column($timeline_data, 'count'), JSON_UNESCAPED_UNICODE); ?>;
// DEBUG: Ausgabe des Pfades in der Browser-Konsole (F12)
const localUrl = "<?php echo plugins_url( 'chart.js', __FILE__ ); ?>";
console.log("WMT DEBUG: Suche nach Chart.js auf: " + localUrl);
if (typeof Chart === 'undefined') {
console.warn("WMT WARN: Chart.js ist nicht geladen. Nutze CSS Fallback.");
const statusContainer = document.getElementById('statusChart').parentElement;
let statusHtml = '<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 10px; background: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px; color: #856404;">Chart.js konnte nicht geladen werden (Lokale Datei fehlt?). Nutze CSS Fallback:</div>';
statusLabels.forEach((label, i) => {
statusHtml += `<div style="background: #f9fafb; padding: 10px; border-radius: 4px; border: 1px solid #eee;"><strong>${label}:</strong> ${statusCounts[i]}</div>`;
});
statusHtml += '</div>';
statusContainer.innerHTML = statusHtml;
const catContainer = document.getElementById('catChart').parentElement;
let catHtml = '<div style="padding:10px 0;">';
if (catLabels.length > 0) {
let maxCat = Math.max(...catCounts) || 1;
catLabels.forEach((label, i) => {
let pct = (catCounts[i] / maxCat) * 100;
catHtml += `
<div class="wmt-css-chart-row">
<div class="wmt-css-label">${label}</div>
<div class="wmt-css-bar-bg">
<div class="wmt-css-bar-fill" style="width: ${pct}%">
${catCounts[i]}
</div>
<div class="wmt-css-bar-val">${catCounts[i]}</div>
</div>
</div>`;
});
} else {
catHtml = '<div style="padding:50px; text-align:center; color:#999;">Keine Kategorien.</div>';
}
catHtml += '</div><div style="margin-top:20px; font-size:11px; color:#999; text-align:center;">* Lokale Chart.js Datei fehlt oder JS blockiert.</div>';
catContainer.innerHTML = catHtml;
const agentContainer = document.getElementById('agentChart').parentElement;
let agentHtml = '<div style="padding:10px 0;">';
if (agentLabels.length > 0) {
let maxAgent = Math.max(...agentCounts) || 1;
agentLabels.forEach((label, i) => {
let pct = (agentCounts[i] / maxAgent) * 100;
agentHtml += `
<div class="wmt-css-chart-row">
<div class="wmt-css-label">${label}</div>
<div class="wmt-css-bar-bg">
<div class="wmt-css-bar-fill" style="background:#34d399; width: ${pct}%">
${agentCounts[i]}
</div>
<div class="wmt-css-bar-val">${agentCounts[i]}</div>
</div>
</div>`;
});
} else {
agentHtml = '<div style="padding:50px; text-align:center; color:#999;">Keine zugewiesenen Tickets.</div>';
}
agentHtml += '</div>';
agentContainer.innerHTML = agentHtml;
const timeContainer = document.getElementById('timelineChart').parentElement;
let timeHtml = '<div style="padding:10px 0;"><ul style="list-style:none; padding:0;">';
if (timeLabels.length > 0) {
timeLabels.forEach((date, i) => {
timeHtml += `<li style="display:flex; justify-content:space-between; border-bottom:1px solid #eee; padding:8px 0;"><span>${date}</span> <strong>${timeCounts[i]}</strong></li>`;
});
} else {
timeHtml = '<div style="padding:50px; text-align:center; color:#999;">Keine Timeline Daten.</div>';
}
timeHtml += '</ul></div>';
timeContainer.innerHTML = timeHtml;
} else {
console.log("WMT SUCCESS: Chart.js geladen. Erstelle Diagramme.");
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' }
}
};
new Chart(document.getElementById('statusChart'), {
type: 'doughnut',
data: {
labels: statusLabels,
datasets: [{
data: statusCounts,
backgroundColor: ['#3b82f6', '#10b981', '#ef4444', '#f59e0b', '#8b5cf6', '#9ca3af'],
borderWidth: 0
}]
},
options: { ...commonOptions, cutout: '70%' }
});
new Chart(document.getElementById('catChart'), {
type: 'bar',
data: {
labels: catLabels,
datasets: [{
label: 'Anzahl',
data: catCounts,
backgroundColor: '#60a5fa',
borderRadius: 4
}]
},
options: { ...commonOptions, scales: { y: { beginAtZero: true, grid: { display: false } }, x: { grid: { display: false } } } }
});
if (agentLabels.length > 0) {
new Chart(document.getElementById('agentChart'), {
type: 'bar',
data: {
labels: agentLabels,
datasets: [{
label: 'Zugewiesene Tickets',
data: agentCounts,
backgroundColor: '#34d399',
borderRadius: 4
}]
},
options: { ...commonOptions, indexAxis: 'y', scales: { x: { beginAtZero: true } } }
});
} else {
document.getElementById('agentChart').parentElement.innerHTML = '<div style="padding:50px; text-align:center; color:#999;">Keine zugewiesenen Tickets.</div>';
}
if (timeLabels.length > 0) {
new Chart(document.getElementById('timelineChart'), {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: 'Neue Tickets',
data: timeCounts,
borderColor: '#818cf8',
backgroundColor: 'rgba(129, 140, 248, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 2
}]
},
options: { ...commonOptions, scales: { x: { ticks: { maxTicksLimit: 7 } } } }
});
}
}
});
</script>
<?php
}
public function render_settings_page() {
$templates = get_option( 'wmt_templates', array() ); // FIX: Corrected array()
if ( is_string($templates) ) $templates = array();
?>
<div class="wrap">
<h1>Einstellungen</h1>
<form method="post" action="options.php">
<?php settings_fields( 'wmt_settings_group' ); ?>
<table class="form-table">
<tr><th>Kategorien</th><td><input type="text" name="wmt_categories" value="<?php echo esc_attr( get_option('wmt_categories', 'Allgemein, Technik, Billing') ); ?>" class="regular-text"></td></tr>
<tr><th>Kategorie ➔ Abteilung Zuordnung</th><td><input type="text" name="wmt_departments" value="<?php echo esc_attr( get_option('wmt_departments', 'Technik:IT, Billing:Buchhaltung') ); ?>" class="large-text" style="width:100%;"></td></tr>
<tr><th>Prioritäten</th><td><input type="text" name="wmt_priorities" value="<?php echo esc_attr( get_option('wmt_priorities', 'Hoch, Mittel, Niedrig') ); ?>" class="regular-text"></td></tr>
<tr><th>Status</th><td><input type="text" name="wmt_statuses" value="<?php echo esc_attr( get_option('wmt_statuses', 'Offen, In Bearbeitung, Wartet auf Antwort, Geschlossen') ); ?>" class="regular-text"></td></tr>
<tr><th>Admin E-Mail</th><td><input type="email" name="wmt_admin_email" value="<?php echo esc_attr( get_option('wmt_admin_email', get_option('admin_email') ) ); ?>" class="regular-text"></td></tr>
<tr><th>Erlaubte Dateiendungen</th><td><input type="text" name="wmt_allowed_filetypes" value="<?php echo esc_attr( get_option('wmt_allowed_filetypes', 'pdf, doc, docx, jpg, jpeg, png, txt') ); ?>" class="regular-text"><p class="description">Kommagetrennt (z.B. pdf, doc, png).</p></td></tr>
<tr>
<th>Textbausteine</th>
<td>
<div id="wmt-templates-container">
<?php foreach( $templates as $index => $tpl ): ?>
<div class="wmt-template-box">
<a href="#" class="wmt-tpl-remove" onclick="removeTemplate(this); return false;">×</a>
<div class="wmt-tpl-row">
<select name="wmt_templates[<?php echo $index; ?>][type]" style="width: 120px;">
<option value="public" <?php selected($tpl['type'], 'public'); ?>>Öffentlich</option>
<option value="internal" <?php selected($tpl['type'], 'internal'); ?>>Intern</option>
</select>
<input type="text" name="wmt_templates[<?php echo $index; ?>][name]" value="<?php echo esc_attr($tpl['name']); ?>" class="regular-text" placeholder="Name der Vorlage" style="flex-grow:1;">
</div>
<textarea name="wmt_templates[<?php echo $index; ?>][content]" class="large-text" rows="3"><?php echo esc_textarea($tpl['content']); ?></textarea>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="button" onclick="addTemplate()">+ Neue Box hinzufügen</button>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<script type="text/javascript">
function addTemplate() {
var container = document.getElementById('wmt-templates-container');
var count = container.children.length;
var div = document.createElement('div');
div.className = 'wmt-template-box';
div.innerHTML = `
<a href="#" class="wmt-tpl-remove" onclick="removeTemplate(this); return false;">×</a>
<div class="wmt-tpl-row">
<select name="wmt_templates[${count}][type]" style="width: 120px;">
<option value="public">Öffentlich</option>
<option value="internal">Intern</option>
</select>
<input type="text" name="wmt_templates[${count}][name]" class="regular-text" placeholder="Name der Vorlage" style="flex-grow:1;">
</div>
<textarea name="wmt_templates[${count}][content]" class="large-text" rows="3"></textarea>
`;
container.appendChild(div);
}
function removeTemplate(btn) { btn.parentElement.remove(); }
</script>
<?php
}
public function render_notifications_page() {
$all_users = get_users( array( 'orderby' => 'display_name' ) );
$selected_users = get_option( 'wmt_new_ticket_notify_users', array() ); // FIX: Corrected array()
if ( ! is_array( $selected_users ) ) $selected_users = array();
?>
<div class="wrap">
<h1>Benachrichtigungen</h1>
<form method="post" action="options.php">
<?php settings_fields( 'wmt_notifications_group' ); ?>
<table class="form-table">
<tr>
<th>Discord Webhook URL</th>
<td>
<input type="url" name="wmt_discord_webhook" value="<?php echo esc_attr( get_option('wmt_discord_webhook') ); ?>" class="regular-text" placeholder="https://discord.com/api/webhooks/...">
<p class="description">Wird bei neuem Ticket und neuer Gast-Antwort gesendet.</p>
</td>
</tr>
<tr>
<th>Telegram Bot Token</th>
<td>
<input type="text" name="wmt_telegram_token" value="<?php echo esc_attr( get_option('wmt_telegram_token') ); ?>" class="regular-text">
</td>
</tr>
<tr>
<th>Telegram Chat ID</th>
<td>
<input type="text" name="wmt_telegram_chat_id" value="<?php echo esc_attr( get_option('wmt_telegram_chat_id') ); ?>" class="regular-text">
<p class="description">Für Gruppen/Kanäle mit -100 beginnen.</p>
</td>
</tr>
<tr>
<th>Zusätzliche Benachrichtigungen<br>bei neuem Ticket</th>
<td>
<select name="wmt_new_ticket_notify_users[]" multiple size="10" style="width: 400px; height: 200px;">
<?php foreach ( $all_users as $user ): ?>
<option value="<?php echo $user->ID; ?>" <?php echo in_array( $user->ID, $selected_users ) ? 'selected' : ''; ?>>
<?php echo esc_html( $user->display_name ); ?> (<?php echo esc_html( $user->user_email ); ?>)
</option>
<?php endforeach; ?>
</select>
<p class="description">Diese User erhalten <strong>zusätzlich</strong> eine E-Mail bei jedem neuen Ticket (Strg/Cmd klicken für Mehrfachauswahl).</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
public function add_dashboard_widget() {
wp_add_dashboard_widget(
'wmt_dashboard_widget',
'Support Tickets',
array( $this, 'render_dashboard_widget' ),
'high'
);
}
public function render_dashboard_widget() {
global $wpdb;
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $this->table_tickets WHERE status NOT LIKE %s", '%Geschlossen%' ) );
$link = admin_url( 'admin.php?page=wmt_tickets' );
echo '<div class="wmt-box" style="padding: 15px; margin: 0; border: 1px solid #ccd0d4; background: #fff;">';
if ( $count > 0 ) {
echo '<div style="font-size: 24px; font-weight: bold; color: #d63638; line-height: 1;">' . intval( $count ) . '</div>';
echo '<div style="color: #666;">Offene Tickets</div>';
} else {
echo '<div style="font-size: 24px; font-weight: bold; color: #28a745; line-height: 1;">0</div>';
echo '<div style="color: #666;">Keine offenen Tickets</div>';
}
echo '<a href="' . $link . '" class="wmt-btn" style="display: block; text-align: center; margin-top: 10px;">Alle Tickets ansehen</a>';
echo '</div>';
}
public function render_admin_page() {
if ( isset( $_GET['wmt_print'] ) && $_GET['wmt_print'] == '1' && isset( $_GET['id'] ) ) {
$this->render_print_view( intval( $_GET['id'] ) );
return;
}
if ( isset( $_GET['action'] ) && $_GET['action'] === 'edit' && isset( $_GET['id'] ) ) {
$this->render_admin_detail( intval( $_GET['id'] ) );
return;
}
if( isset( $_GET['deleted'] ) && $_GET['deleted'] == '1' ) {
echo '<div class="notice notice-success is-dismissible"><p>Ticket erfolgreich gelöscht.</p></div>';
}
$search = isset( $_GET['s'] ) ? sanitize_text_field( $_GET['s'] ) : '';
$status_filter = isset( $_GET['status_filter'] ) ? sanitize_text_field( $_GET['status_filter'] ) : '';
global $wpdb;
$sql = "SELECT * FROM $this->table_tickets WHERE 1=1";
if ( $search ) {
$sql .= $wpdb->prepare( " AND (title LIKE %s OR guest_name LIKE %s OR guest_email LIKE %s)", '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%' );
}
if ( $status_filter ) {
$sql .= $wpdb->prepare( " AND status = %s", $status_filter );
}
$sql .= " ORDER BY updated_at DESC";
$tickets = $wpdb->get_results( $sql );
echo '<div class="wrap"><h1>Ticket Übersicht</h1>';
echo '<div style="margin-bottom: 20px; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">';
echo '<form method="get" style="display:flex; gap:10px; align-items:center;">';
echo '<input type="hidden" name="page" value="wmt_tickets">';
echo '<input type="text" name="s" value="' . esc_attr($search) . '" placeholder="Suche..." class="regular-text">';
echo '<select name="status_filter" class="regular-text">';
echo '<option value="">Alle Status</option>';
foreach( array_map('trim', explode(',', get_option('wmt_statuses'))) as $st ) {
$sel = selected($status_filter, $st, false);
echo '<option value="' . esc_attr($st) . '" ' . $sel . '>' . esc_html($st) . '</option>';
}
echo '</select>';
echo '<input type="submit" value="Filtern" class="button">';
echo '</form>';
$export_url = admin_url( 'admin.php?page=wmt_tickets&wmt_action=export' );
if($search) $export_url = add_query_arg( 's', $search, $export_url );
echo '<a href="' . $export_url . '" class="wmt-btn wmt-export-btn">CSV Export</a>';
echo '</div>';
echo '<table class="wp-list-table widefat fixed striped">';
echo '<thead><tr><th>ID</th><th>Betreff</th><th>Kat.</th><th>Abt.</th><th>Prio</th><th>Gast Info</th><th>Status</th><th>Zugewiesen</th><th>Aktionen</th></tr></thead>';
echo '<tbody>';
foreach ( $tickets as $t ) {
$prio_style = strtolower( $t->priority ) === 'hoch' ? 'color: #d63638; font-weight: bold; font-size: 1.1em;' : '';
$assigned_user = $t->assigned_to ? get_userdata( $t->assigned_to ) : null;
$assigned_name = $assigned_user ? $assigned_user->display_name : '-';
echo '<tr>';
echo '<td>#' . $t->id . '</td>';
echo '<td>' . esc_html( $t->title ) . '</td>';
echo '<td>' . esc_html( $t->category ) . '</td>';
echo '<td>' . ( $t->department ? esc_html($t->department) : '-' ) . '</td>';
echo '<td style="'.$prio_style.'">' . esc_html( $t->priority ) . '</td>';
echo '<td><strong>' . esc_html( $t->guest_name ) . '</strong><br><small>' . esc_html( $t->guest_email ) . '</small></td>';
echo '<td>' . esc_html( $t->status ) . '</td>';
echo '<td>' . esc_html( $assigned_name ) . '</td>';
echo '<td>';
echo '<a href="' . admin_url( 'admin.php?page=wmt_tickets&action=edit&id=' . $t->id ) . '" class="button">Bearbeiten</a> ';
$delete_url = wp_nonce_url( admin_url( 'admin.php?page=wmt_tickets&action=wmt_delete&id=' . $t->id ), 'wmt_delete_ticket_' . $t->id );
echo '<a href="' . $delete_url . '" class="button wmt-btn danger" onclick="return confirm(\'Wirklich löschen?\')">Löschen</a>';
echo '</td>';
echo '</tr>';
}
echo '</tbody></table></div>';
}
private function render_admin_detail( $id ) {
global $wpdb;
$ticket = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_tickets WHERE id = %d", $id ) );
if ( ! $ticket ) return;
if ( isset( $_GET['msg'] ) && $_GET['msg'] == 'sent' ) {
echo '<div class="notice notice-success is-dismissible"><p>Aktualisierung erfolgreich!</p></div>';
}
$messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $this->table_messages WHERE ticket_id = %d ORDER BY created_at ASC", $id ) );
$all_users = get_users( array( 'orderby' => 'display_name' ) );
$status_opts = array_map( 'trim', explode( ',', get_option('wmt_statuses') ) );
$mapping_raw = get_option('wmt_departments', '');
$dept_opts = array();
if($mapping_raw) {
$pairs = array_map('trim', explode(',', $mapping_raw));
foreach($pairs as $pair) {
if(strpos($pair, ':') !== false) {
list($cat, $dept) = explode(':', $pair, 2);
$dept_opts[] = trim($dept);
}
}
$dept_opts = array_unique($dept_opts);
}
$raw_templates = get_option('wmt_templates', array());
$tpl_public = '';
$tpl_internal = '';
if( is_array($raw_templates) ) {
foreach($raw_templates as $tpl) {
if(isset($tpl['type']) && isset($tpl['content'])) {
$name = isset($tpl['name']) && !empty($tpl['name']) ? esc_html($tpl['name']) : substr(esc_html($tpl['content']), 0, 30) . '...';
$content = esc_attr($tpl['content']);
$opt = '<option value="' . $content . '">' . $name . '</option>';
if($tpl['type'] === 'internal') $tpl_internal .= $opt;
else $tpl_public .= $opt;
}
}
}
$colors = array('#e57373', '#f06292', '#ba68c8', '#9575cd', '#7986cb', '#64b5f6', '#4fc3f7', '#4dd0e1', '#4db6ac', '#81c784', '#aed581', '#ffca28', '#ffa726');
?>
<div class="wrap">
<h1>Ticket #<?php echo $ticket->id; ?> bearbeiten</h1>
<div style="margin-bottom: 10px;">
<a href="<?php echo admin_url( 'admin.php?page=wmt_tickets' ); ?>" class="button">« Zurück</a>
<a href="#" onclick="wmt_print_ticket(<?php echo $ticket->id; ?>); return false;" class="wmt-btn wmt-print-btn" style="margin-left:10px;">Ticket drucken</a>
<?php
$delete_url = wp_nonce_url( admin_url( 'admin.php?page=wmt_tickets&action=wmt_delete&id=' . $ticket->id ), 'wmt_delete_ticket_' . $ticket->id );
echo '<a href="' . $delete_url . '" class="button wmt-btn danger" style="margin-left:10px;" onclick="return confirm(\'Ticket wirklich löschen?\')">Ticket löschen</a>';
?>
</div>
<div class="wmt-box" style="margin-top:20px; border:1px solid #ccc;">
<div class="wmt-chat">
<?php foreach ( $messages as $msg ) :
if( $msg->sender_type === 'system' ): ?>
<div class="wmt-system-msg"><strong>System:</strong> <?php echo esc_html($msg->message); ?> <small><?php echo $msg->created_at; ?></small></div>
<?php continue; endif;
$is_admin = ( $msg->sender_type === 'admin' );
$bg = $is_admin ? '#fff' : '#e3f2fd';
$align = $is_admin ? 'left' : 'right';
$initial = substr($msg->sender_name, 0, 1);
$char_code = ord(strtolower($initial));
$bg_color = $is_admin ? '#555' : $colors[$char_code % count($colors)];
?>
<div class="wmt-msg-row" style="flex-direction: <?php echo $is_admin ? 'row' : 'row-reverse'; ?>;">
<div class="wmt-avatar" style="background:<?php echo $bg_color; ?>;"><?php echo esc_html($initial); ?></div>
<div class="wmt-bubble wmt-<?php echo $align; ?>" style="background:<?php echo $bg; ?>;">
<strong><?php echo esc_html( $msg->sender_name ); ?></strong> <small><?php echo $msg->created_at; ?></small>
<?php if( $is_admin ) echo ' <span style="color:#0073aa;">(Support)</span>'; ?>
<?php if( $is_admin && !empty($msg->internal_note) ) : ?>
<div class="wmt-internal-note"><strong>Interne Notiz:</strong><?php echo nl2br( esc_html( $msg->internal_note ) ); ?></div>
<?php endif; ?>
<?php if( !empty($msg->message) ): ?>
<div style="margin-top:5px;"><?php echo wp_kses_post($msg->message); ?></div>
<?php else: ?>
<p style="font-size:11px; color:#999; font-style:italic;">Keine öffentliche Nachricht.</p>
<?php endif; ?>
<?php if ( $msg->file_url ) : ?>
<a href="<?php echo esc_url( $msg->file_url ); ?>" target="_blank" style="display:inline-block; margin-top:10px; text-decoration:none; background:#e9ecef; padding:5px 10px; border-radius:4px; border:1px solid #ccc;">Datei herunterladen</a>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<hr>
<form method="post" action="<?php echo admin_url( 'admin-post.php' ); ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="wmt_admin_reply">
<?php wp_nonce_field( 'wmt_admin_action', 'wmt_nonce' ); ?>
<input type="hidden" name="ticket_id" value="<?php echo $ticket->id; ?>">
<table class="form-table">
<tr>
<th>Abteilung</th>
<td>
<select name="department" class="regular-text">
<option value="">-- Keine --</option>
<?php foreach( $dept_opts as $d ): ?>
<option value="<?php echo esc_attr($d); ?>" <?php selected($ticket->department, $d); ?>><?php echo esc_html($d); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th>Status</th>
<td>
<select name="status" class="regular-text">
<?php foreach( $status_opts as $s ): ?>
<option value="<?php echo esc_attr($s); ?>" <?php selected($ticket->status, $s); ?>><?php echo esc_html($s); ?></option>
<?php endforeach; ?>
</select>
<p class="description"><strong>Automatik:</strong> Bei Antworten wird das Ticket automatisch auf "In Bearbeitung" gesetzt (außer Sie wählen "Geschlossen").</p>
</td>
</tr>
<tr>
<th>Priorität</th>
<td><strong style="font-size: 16px; color: #333;"><?php echo esc_html( $ticket->priority ); ?></strong></td>
</tr>
<tr>
<th>Zuweisen an</th>
<td>
<select name="assigned_to" class="regular-text">
<option value="">-- Niemand --</option>
<?php foreach( $all_users as $u ): ?>
<option value="<?php echo $u->ID; ?>" <?php selected($ticket->assigned_to, $u->ID); ?>><?php echo esc_html( $u->display_name ); ?> (<?php echo esc_html( $u->user_email ); ?>)</option>
<?php endforeach; ?>
</select>
<p class="description"><strong>Auto-Detect:</strong> Wenn Sie antworten, wird das Ticket automatisch Ihnen zugewiesen (sofern noch niemand zugewiesen). Wählen Sie hier einen Kollegen aus, um das Ticket zu übergeben.</p>
</td>
</tr>
<tr>
<th>Antwort & Notizen</th>
<td>
<label class="wmt-label" style="color:#666; font-size:12px;">Interne Notiz (Optional)</label>
<select id="wmt_internal_template_select" class="regular-text" style="margin-bottom: 10px;"><option value="">-- Interne Vorlage --</option><?php echo $tpl_internal; ?></select>
<textarea name="internal_note" class="large-text" rows="3" placeholder="Notizen für Kollegen..."></textarea>
<hr style="margin: 20px 0; border:0; border-top:1px dashed #eee;">
<label class="wmt-label">Öffentliche Nachricht (Optional)</label>
<select id="wmt_public_template_select" class="regular-text" style="margin-bottom: 10px;"><option value="">-- Vorlage auswählen --</option><?php echo $tpl_public; ?></select>
<?php
$content = '';
$settings = array('textarea_name' => 'message', 'media_buttons' => false, 'textarea_rows' => 10, 'teeny' => false);
wp_editor( $content, 'wmt_message_editor', $settings );
?>
<label class="wmt-label" style="margin-top:10px;">Datei anhängen (Optional)</label>
<input type="file" name="ticket_file">
<p class="description">
Wird gesendet an <?php echo esc_html( $ticket->guest_email ); ?>.
</p>
</td>
</tr>
</table>
<?php submit_button( 'Speichern & Benachrichtigen' ); ?>
</form>
</div>
<script type="text/javascript">
document.getElementById('wmt_public_template_select').addEventListener('change', function() {
var editor = tinymce.get('wmt_message_editor');
if(this.value && editor) {
editor.setContent(editor.getContent() + (editor.getContent() ? '\n\n' : '') + this.value);
this.value = "";
}
});
document.getElementById('wmt_internal_template_select').addEventListener('change', function() {
var ta = document.querySelector('textarea[name="internal_note"]');
if(this.value && ta) { ta.value += (ta.value ? '\n\n' : '') + this.value; this.value = ""; }
});
function wmt_print_ticket(id) {
var url = '<?php echo admin_url( 'admin.php?page=wmt_tickets' ); ?>&wmt_print=1&id=' + id;
window.open(url, '_blank', 'width=800,height=600,scrollbars=yes');
}
</script>
</div>
<?php
}
private function render_print_view( $id ) {
if ( ! current_user_can( 'manage_options' ) ) return;
global $wpdb;
$ticket = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_tickets WHERE id = %d", $id ) );
if ( ! $ticket ) return;
$messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $this->table_messages WHERE ticket_id = %d ORDER BY created_at ASC", $id ) );
echo '<!DOCTYPE html><html><head><title>Ticket #' . $id . '</title>';
echo '<style type="text/css">
body { font-family: Helvetica, Arial, sans-serif; color: #333; margin: 20px; background: white; }
.header { border-bottom: 2px solid #000; padding-bottom: 20px; margin-bottom: 30px; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 5px; }
.info-label { font-weight: bold; }
.chat-item { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
.chat-meta { font-size: 12px; color: #666; margin-bottom: 5px; }
.chat-content { line-height: 1.5; font-size: 14px; }
.internal { background: #f9f9f9; padding: 10px; border-left: 3px solid #999; margin-top: 10px; font-size: 12px; font-style: italic; }
.system { text-align: center; background: #eee; padding: 5px; margin: 20px 0; font-size: 11px; color: #666; }
.file-link { font-size: 12px; margin-top: 5px; color: #0066cc; }
</style></head><body>';
echo '<div class="header"><h1>Ticket #' . $id . ': ' . esc_html( $ticket->title ) . '</h1></div>';
echo '<div class="info-row"><span class="info-label">Kunde:</span> ' . esc_html( $ticket->guest_name ) . ' (' . esc_html( $ticket->guest_email ) . ')</div>';
echo '<div class="info-row"><span class="info-label">Kategorie:</span> ' . esc_html( $ticket->category ) . '</div>';
echo '<div class="info-row"><span class="info-label">Abteilung:</span> ' . ($ticket->department ? esc_html($ticket->department) : '-') . '</div>';
echo '<div class="info-row"><span class="info-label">Status:</span> ' . esc_html( $ticket->status ) . '</div>';
echo '<div class="info-row"><span class="info-label">Priorität:</span> ' . esc_html( $ticket->priority ) . '</div>';
$assigned = $ticket->assigned_to ? get_userdata($ticket->assigned_to)->display_name : 'Niemand';
echo '<div class="info-row"><span class="info-label">Zugewiesen an:</span> ' . esc_html($assigned) . '</div>';
echo '<h2 style="margin-top: 40px; border-bottom: 1px solid #ccc; padding-bottom: 10px;">Verlauf</h2>';
foreach ( $messages as $msg ) {
if ( $msg->sender_type === 'system' ) {
echo '<div class="system">System: ' . esc_html($msg->message) . ' (' . $msg->created_at . ')</div>';
continue;
}
echo '<div class="chat-item">';
echo '<div class="chat-meta"><strong>' . esc_html( $msg->sender_name ) . '</strong> ' . ($msg->sender_type === 'admin' ? '(Support)' : '(Kunde)') . ' - ' . $msg->created_at . '</div>';
echo '<div class="chat-content">' . wp_kses_post($msg->message) . '</div>';
if ( $msg->sender_type === 'admin' && !empty($msg->internal_note) ) {
echo '<div class="internal">Interne Notiz: ' . esc_html($msg->internal_note) . '</div>';
}
if ( $msg->file_url ) {
echo '<div class="file-link">Datei: <a href="' . esc_url( $msg->file_url ) . '">' . esc_html(basename($msg->file_url)) . '</a></div>';
}
echo '</div>';
}
echo '<script>window.print();</script></body></html>';
exit;
}
private function handle_upload($file_input_name) {
if (empty($_FILES[$file_input_name]['name'])) return '';
$allowed_raw = get_option('wmt_allowed_filetypes', 'pdf, doc, docx, jpg, png');
$allowed_exts = array_map('trim', explode(',', strtolower($allowed_raw)));
$file_ext = strtolower(pathinfo($_FILES[$file_input_name]['name'], PATHINFO_EXTENSION));
if (!in_array($file_ext, $allowed_exts)) {
wp_die( "Fehler: Der Dateityp <strong>.{$file_ext}</strong> ist nicht erlaubt. Erlaubt sind: " . esc_html($allowed_raw) );
}
require_once(ABSPATH . 'wp-admin/includes/file.php');
$upload = wp_handle_upload($_FILES[$file_input_name], array('test_form' => false));
if (isset($upload['error'])) wp_die('Upload Fehler: ' . $upload['error']);
return isset($upload['url']) ? $upload['url'] : '';
}
private function send_discord_notification( $title, $description, $url ) {
$webhook = get_option( 'wmt_discord_webhook' );
if ( ! $webhook ) return;
$data = array(
"embeds" => array(
array(
"title" => $title,
"description" => $description,
"url" => $url,
"color" => 3447003,
"timestamp" => current_time( 'mysql' )
)
)
);
wp_remote_post( $webhook, array(
'body' => wp_json_encode( $data ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 10
) );
}
private function send_telegram_notification( $text ) {
$token = get_option( 'wmt_telegram_token' );
$chat_id = get_option( 'wmt_telegram_chat_id' );
if ( ! $token || ! $chat_id ) return;
$url = "https://api.telegram.org/bot{$token}/sendMessage";
wp_remote_post( $url, array(
'body' => array(
'chat_id' => $chat_id,
'text' => $text,
'parse_mode' => 'HTML'
),
'timeout' => 10
) );
}
public function handle_delete_ticket() {
if ( isset( $_GET['action'] ) && $_GET['action'] === 'wmt_delete' && isset( $_GET['id'] ) ) {
$id = intval( $_GET['id'] );
$nonce = isset( $_GET['_wpnonce'] ) ? $_GET['_wpnonce'] : '';
if ( ! wp_verify_nonce( $nonce, 'wmt_delete_ticket_' . $id ) || ! current_user_can( 'manage_options' ) ) wp_die( 'Sicherheitsfehler' );
global $wpdb;
$wpdb->delete( $this->table_messages, array( 'ticket_id' => $id ) );
$wpdb->delete( $this->table_tickets, array( 'id' => $id ) );
wp_redirect( admin_url( 'admin.php?page=wmt_tickets&deleted=1' ) );
exit;
}
}
public function handle_admin_post() {
if ( ! isset( $_POST['wmt_nonce'] ) || ! wp_verify_nonce( $_POST['wmt_nonce'], 'wmt_admin_action' ) ) wp_die( 'Sicherheitsfehler' );
$msg_content = isset( $_POST['message'] ) ? wp_kses_post( $_POST['message'] ) : '';
$tid = intval( $_POST['ticket_id'] );
// STATUS LOGIK:
// Wenn nicht "Geschlossen" gewählt wurde, setzen wir es auf "In Bearbeitung"
$status = sanitize_text_field( $_POST['status'] );
if ( strtolower( $status ) !== 'geschlossen' ) {
$status = 'In Bearbeitung';
}
$dept = sanitize_text_field( $_POST['department'] );
// Prüfen Dropdown: User manuell ausgewählt?
$assigned = !empty($_POST['assigned_to']) ? intval( $_POST['assigned_to'] ) : null;
// Auto-Detection Logik
// Wenn im Dropdown "Niemand" ausgewählt ist (NULL), ABER der User gerade antwortet...
if ( empty($assigned) && (!empty($msg_content) || !empty($_FILES['ticket_file']['name'])) ) {
// ...dann weisen wir es dem aktuellen User zu.
$assigned = get_current_user_id();
}
$internal_note = sanitize_textarea_field( $_POST['internal_note'] );
$file_url = $this->handle_upload('ticket_file');
global $wpdb;
$old_ticket = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_tickets WHERE id = %d", $tid ) );
$wpdb->update( $this->table_tickets,
array( 'status' => $status, 'department' => $dept ?: null, 'assigned_to' => $assigned ),
array( 'id' => $tid )
);
if ( $old_ticket && $old_ticket->department !== $dept ) {
$new_dept_name = $dept ?: 'Keine';
$wpdb->insert( $this->table_messages, array(
'ticket_id' => $tid, 'sender_name' => 'System', 'sender_type' => 'system', 'message' => "Abteilung geändert zu: {$new_dept_name}"
));
}
// Handover Benachrichtigung: Wenn sich der zugewiesene User ändert
if ( $old_ticket->assigned_to != $assigned && $assigned ) {
$user = get_userdata( $assigned );
if ( $user ) {
$subject = "Ticket #{$tid} wurde Ihnen zugewiesen";
$body = "Hallo {$user->display_name},\n\nDas Ticket #{$tid} „{$old_ticket->title}“ wurde Ihnen zur Bearbeitung zugewiesen.\n\nLink: " . admin_url( "admin.php?page=wmt_tickets&action=edit&id={$tid}" );
wp_mail( $user->user_email, $subject, $body );
}
}
if ( ! empty( $msg_content ) || ! empty( $file_url ) ) {
$current_user = wp_get_current_user();
$wpdb->insert( $this->table_messages, array(
'ticket_id' => $tid, 'sender_name' => $current_user->display_name, 'sender_type' => 'admin',
'message' => $msg_content, 'internal_note' => $internal_note, 'file_url' => $file_url
));
$view_link = add_query_arg( array( 'wmt_view' => $tid, 'hash' => $old_ticket->ticket_hash ), home_url() );
$subject = "Neue Antwort zu Ihrem Ticket #{$tid}";
$body = "Hallo {$old_ticket->guest_name},\n\nEs gibt eine neue Antwort:\n{$msg_content}\n\nLink zum Ticket:\n{$view_link}";
if($file_url) $body .= "\n\nAnhang: {$file_url}";
wp_mail( $old_ticket->guest_email, $subject, $body );
} elseif ( ! empty( $internal_note ) ) {
$current_user = wp_get_current_user();
$wpdb->insert( $this->table_messages, array(
'ticket_id' => $tid, 'sender_name' => $current_user->display_name, 'sender_type' => 'admin',
'message' => null, 'internal_note' => $internal_note
));
}
wp_redirect( admin_url( 'admin.php?page=wmt_tickets&action=edit&id=' . $tid . '&msg=sent' ) );
exit;
}
public function handle_csv_export() {
if ( ! isset( $_GET['wmt_action'] ) || $_GET['wmt_action'] !== 'export' || ! current_user_can( 'manage_options' ) ) return;
global $wpdb;
$search = isset( $_GET['s'] ) ? sanitize_text_field( $_GET['s'] ) : '';
$sql = "SELECT * FROM $this->table_tickets";
if($search) {
$sql .= $wpdb->prepare( " WHERE (title LIKE %s OR guest_name LIKE %s OR guest_email LIKE %s)", '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%' );
}
$results = $wpdb->get_results( $sql );
header( 'Content-Type: text/csv' );
header( 'Content-Disposition: attachment; filename=tickets-export.csv' );
$output = fopen( 'php://output', 'w' );
fputcsv( $output, array( 'ID', 'Titel', 'Kategorie', 'Abteilung', 'Status', 'Priorität', 'Name', 'E-Mail', 'Zugewiesen', 'Datum' ) );
foreach ( $results as $row ) {
$assigned = $row->assigned_to ? get_userdata($row->assigned_to)->display_name : '';
fputcsv( $output, array( $row->id, $row->title, $row->category, $row->department, $row->status, $row->priority, $row->guest_name, $row->guest_email, $assigned, $row->created_at ) );
}
fclose( $output );
exit;
}
public function render_creation_form() {
ob_start();
if(isset($_GET['upload_error'])) {
echo '<div class="wmt-error">' . esc_html(urldecode($_GET['upload_error'])) . '</div>';
}
if( isset( $_GET['wmt_success'] ) ) {
echo '<div class="wmt-alert">Ticket erfolgreich erstellt! Bitte prüfen Sie Ihre E-Mails.</div>';
}
?>
<div class="wmt-box">
<h2>Neues Support Ticket erstellen</h2>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field( 'wmt_guest_create', 'wmt_nonce' ); ?>
<label class="wmt-label">Ihr Name *</label>
<input type="text" name="guest_name" class="wmt-input" required>
<label class="wmt-label">Ihre E-Mail *</label>
<input type="email" name="guest_email" class="wmt-input" required>
<label class="wmt-label">Kategorie</label>
<select name="category" class="wmt-select">
<?php foreach( array_map('trim', explode(',', get_option('wmt_categories'))) as $c ): ?>
<option><?php echo esc_html($c); ?></option>
<?php endforeach; ?>
</select>
<label class="wmt-label">Priorität</label>
<select name="priority" class="wmt-select">
<?php foreach( array_map('trim', explode(',', get_option('wmt_priorities'))) as $p ): ?>
<option><?php echo esc_html($p); ?></option>
<?php endforeach; ?>
</select>
<label class="wmt-label">Betreff *</label>
<input type="text" name="title" class="wmt-input" required>
<label class="wmt-label">Nachricht *</label>
<textarea name="message" class="wmt-textarea" rows="10" required></textarea>
<label class="wmt-label">Datei anhängen (Optional)</label>
<input type="file" name="guest_file" class="wmt-input">
<button type="submit" name="wmt_create" class="wmt-btn">Ticket absenden</button>
</form>
</div>
<?php
return ob_get_clean();
}
public function render_lookup_form() {
ob_start();
if( isset( $_GET['wmt_lookup_sent'] ) ) {
echo '<div class="wmt-alert">Eine E-Mail mit Links wurde gesendet.</div>';
}
?>
<div class="wmt-box">
<h2>Meine Tickets finden</h2>
<form method="post">
<input type="email" name="lookup_email" class="wmt-input" placeholder="ihre@email.de" required>
<button type="submit" name="wmt_lookup" class="wmt-btn">Tickets suchen</button>
</form>
</div>
<?php
return ob_get_clean();
}
public function render_ticket_view() {
$tid = isset( $_GET['wmt_view'] ) ? intval( $_GET['wmt_view'] ) : 0;
$hash = isset( $_GET['hash'] ) ? sanitize_text_field( $_GET['hash'] ) : '';
if ( ! $tid || ! $hash ) return '<div class="wmt-box"><p>Kein Ticket ausgewählt.</p></div>';
global $wpdb;
$ticket = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_tickets WHERE id = %d AND ticket_hash = %s", $tid, $hash ) );
if ( ! $ticket ) return '<div class="wmt-box" style="background:#ffebee;"><p>Zugriff verweigert.</p></div>';
$messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $this->table_messages WHERE ticket_id = %d ORDER BY created_at ASC", $tid ) );
$is_closed = ( strtolower($ticket->status) === 'geschlossen' );
ob_start();
?>
<div class="wmt-box">
<div style="border-bottom:1px solid #eee; padding-bottom:10px; margin-bottom:10px;">
<h2>Ticket #<?php echo $ticket->id; ?>: <?php echo esc_html( $ticket->title ); ?></h2>
<p><strong>Status:</strong> <?php echo esc_html( $ticket->status ); ?></p>
</div>
<div class="wmt-chat">
<?php foreach ( $messages as $msg ) :
if( $msg->sender_type === 'system' ): ?>
<div class="wmt-system-msg"><?php echo esc_html($msg->message); ?></div>
<?php continue; endif;
$is_admin = ( $msg->sender_type === 'admin' );
if(empty($msg->message) && empty($msg->file_url)) continue;
?>
<div class="wmt-bubble wmt-<?php echo $is_admin ? 'left' : 'right'; ?>">
<strong><?php echo esc_html( $msg->sender_name ); ?></strong> <small><?php echo $msg->created_at; ?></small>
<p><?php echo nl2br( esc_html( $msg->message ) ); ?></p>
<?php if ( $msg->file_url ) : ?>
<a href="<?php echo esc_url( $msg->file_url ); ?>" target="_blank" style="display:inline-block; margin-top:10px; text-decoration:none; background:#e9ecef; padding:5px 10px; border-radius:4px; border:1px solid #ccc;">Datei ansehen</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if ( ! $is_closed ) : ?>
<form method="post" enctype="multipart/form-data">
<?php wp_nonce_field( 'wmt_guest_reply', 'wmt_nonce' ); ?>
<input type="hidden" name="ticket_id" value="<?php echo $ticket->id; ?>">
<label class="wmt-label">Antwort schreiben</label>
<textarea name="message" class="wmt-textarea" rows="4" required></textarea>
<label class="wmt-label" style="margin-top:10px;">Datei anhängen (Optional)</label>
<input type="file" name="guest_file" class="wmt-input">
<button type="submit" name="wmt_reply" class="wmt-btn">Antwort senden</button>
</form>
<?php else : ?>
<div class="wmt-alert">Ticket geschlossen.</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
public function handle_guest_creation() {
if ( ! isset( $_POST['wmt_create'] ) || ! wp_verify_nonce( $_POST['wmt_nonce'], 'wmt_guest_create' ) ) return;
$name = sanitize_text_field( $_POST['guest_name'] );
$email = sanitize_email( $_POST['guest_email'] );
$cat = sanitize_text_field( $_POST['category'] );
$prio = sanitize_text_field( $_POST['priority'] );
$title = sanitize_text_field( $_POST['title'] );
$msg = sanitize_textarea_field( $_POST['message'] );
$hash = wp_generate_password( 20, false );
$file_url = $this->handle_upload('guest_file');
$dept = null;
$mapping_raw = get_option('wmt_departments', '');
if($mapping_raw) {
foreach(array_map('trim', explode(',', $mapping_raw)) as $pair) {
if(strpos($pair, ':') !== false) {
list($map_cat, $map_dept) = explode(':', $pair, 2);
if(trim($map_cat) === $cat) { $dept = trim($map_dept); break; }
}
}
}
global $wpdb;
$wpdb->insert( $this->table_tickets, array(
'title' => $title, 'category' => $cat, 'priority' => $prio, 'department' => $dept,
'guest_name' => $name, 'guest_email' => $email, 'ticket_hash' => $hash
));
$tid = $wpdb->insert_id;
$wpdb->insert( $this->table_messages, array(
'ticket_id' => $tid, 'sender_name' => $name, 'sender_type' => 'guest', 'message' => $msg, 'file_url' => $file_url
));
$admin_link = admin_url( "admin.php?page=wmt_tickets&action=edit&id={$tid}" );
$guest_link = add_query_arg( array( 'wmt_view' => $tid, 'hash' => $hash ), home_url() );
$admin_email = get_option( 'wmt_admin_email', get_option('admin_email') );
wp_mail( $admin_email, "Neues Ticket #{$tid}: {$title}", "Von: {$name} ({$email})\nLink: {$admin_link}" );
$this->send_discord_notification( "Neues Ticket #{$tid}", "{$title}\nVon: {$name} ({$email})\nPriorität: {$prio}", $admin_link );
$this->send_telegram_notification( "<b>Neues Ticket #{$tid}</b>\n<b>Betreff:</b> {$title}\n<b>Von:</b> {$name} ({$email})\n<b>Priorität:</b> {$prio}\nLink: {$admin_link}" );
$notify_user_ids = get_option( 'wmt_new_ticket_notify_users', array() ); // FIX: Corrected array()
if ( is_array( $notify_user_ids ) && ! empty( $notify_user_ids ) ) {
foreach ( $notify_user_ids as $user_id ) {
$user = get_userdata( $user_id );
if ( $user ) {
$subject = "Neues Ticket #{$tid}: {$title}";
$body = "Hallo {$user->display_name},\n\nein neues Ticket wurde erstellt.\n\nBetreff: {$title}\nVon: {$name} ({$email})\nPriorität: {$prio}\n\nLink zum Ticket: {$admin_link}";
wp_mail( $user->user_email, $subject, $body );
}
}
}
wp_mail( $email, "Ihr Ticket #{$tid} wurde erstellt", "Hallo {$name},\n\nVielen Dank für Ihr Ticket.\nLink: {$guest_link}" );
wp_redirect( add_query_arg( 'wmt_success', '1', wp_get_referer() ) );
exit;
}
public function handle_guest_reply() {
if ( ! isset( $_POST['wmt_reply'] ) || ! wp_verify_nonce( $_POST['wmt_nonce'], 'wmt_guest_reply' ) ) return;
$tid = intval( $_POST['ticket_id'] );
$msg = sanitize_textarea_field( $_POST['message'] );
$file_url = $this->handle_upload('guest_file');
global $wpdb;
$ticket = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->table_tickets WHERE id = %d", $tid ) );
if(!$ticket) return;
$wpdb->insert( $this->table_messages, array(
'ticket_id' => $tid, 'sender_name' => $ticket->guest_name, 'sender_type' => 'guest',
'message' => $msg, 'file_url' => $file_url
));
$admin_link = admin_url( "admin.php?page=wmt_tickets&action=edit&id={$tid}" );
$admin_email = get_option( 'wmt_admin_email', get_option('admin_email') );
wp_mail( $admin_email, "Neue Antwort zu Ticket #{$tid}", "Kunde hat geantwortet.\nLink: {$admin_link}" );
$this->send_discord_notification( "Neue Antwort in Ticket #{$tid}", "Kunde {$ticket->guest_name} hat geantwortet.", $admin_link );
$this->send_telegram_notification( "<b>Neue Antwort in Ticket #{$tid}</b>\nKunde: {$ticket->guest_name}\nLink: {$admin_link}" );
wp_redirect( add_query_arg( array( 'wmt_view' => $tid, 'hash' => $ticket->ticket_hash ), wp_get_referer() ) );
exit;
}
public function handle_guest_lookup() {
if ( ! isset( $_POST['wmt_lookup'] ) ) return;
$email = sanitize_email( $_POST['lookup_email'] );
if( ! is_email($email) ) return;
global $wpdb;
$tickets = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $this->table_tickets WHERE guest_email = %s", $email ) );
if( $tickets ) {
$body = "Hier sind Ihre Tickets:\n\n";
foreach( $tickets as $t ) {
$link = add_query_arg( array( 'wmt_view' => $t->id, 'hash' => $t->ticket_hash ), home_url() );
$body .= "#{$t->id}: {$t->title}\n{$link}\n\n";
}
wp_mail( $email, "Ihre Tickets", $body );
}
wp_redirect( add_query_arg( 'wmt_lookup_sent', '1', wp_get_referer() ) );
exit;
}
}
new WP_Multi_Ticket_Pro();