'Warten auf Daten', 'waiting_routine' => 'Warten auf Daten Routine', 'text_proposal' => 'Textvorschlag hinterlegt', 'submitted' => 'Daten ubermittelt', 'error' => 'Daten fehlerhaft', 'printing' => 'Druck abgeschlossen', 'shipped' => 'Versendet', ]; /** Sub-Auftragsstatus (ohne Routine-spezifische Status) */ const SUB_STATUSES = [ 'waiting' => 'Warten auf Daten', 'submitted' => 'Daten ubermittelt', 'error' => 'Daten fehlerhaft', 'printing' => 'Druck abgeschlossen', 'shipped' => 'Versendet', ]; /** Formate (identisch mit Frontend) */ const FORMATS = [ 'A4' => 'A4 (Hochformat, max. 18 Zeilen)', 'A6 Hochformat' => 'A6 Hochformat (max. 9 Zeilen)', 'A6 Querformat' => 'A6 Querformat (max. 6 Zeilen)', ]; public function __construct() { add_action('admin_menu', [$this, 'add_admin_menu']); add_action('rest_api_init', [$this, 'register_rest_routes']); add_action('admin_post_skrift_job_create', [$this, 'handle_create_job']); add_action('admin_post_skrift_job_delete', [$this, 'handle_delete_job']); add_action('admin_post_skrift_job_trash', [$this, 'handle_trash_job']); add_action('admin_post_skrift_job_restore', [$this, 'handle_restore_job']); add_action('admin_post_skrift_job_purge', [$this, 'handle_purge_job']); add_action('admin_post_skrift_job_bulk', [$this, 'handle_bulk_job']); add_action('admin_post_skrift_job_settings', [$this, 'handle_save_settings']); add_action('admin_post_skrift_job_update_status', [$this, 'handle_update_status']); add_action('admin_post_skrift_sub_order_update_status', [$this, 'handle_update_sub_order_status']); add_action('admin_post_skrift_sub_order_delete', [$this, 'handle_delete_sub_order']); add_action('phpmailer_init', [$this, 'configure_smtp']); add_action('skrift_cleanup_old_jobs', [$this, 'cleanup_old_jobs']); // Cron-Job einplanen falls noch nicht vorhanden if (!wp_next_scheduled('skrift_cleanup_old_jobs')) { wp_schedule_event(time(), 'daily', 'skrift_cleanup_old_jobs'); } } // ─── Plugin-Einstellungen ──────────────────────────────────────────────────── public static function get_plugin_settings(): array { return wp_parse_args(get_option(self::SETTINGS_KEY, []), [ 'notification_email' => '', 'redirect_url' => '', 'portal_url' => '', 'smtp' => [ 'host' => '', 'port' => 465, 'username' => '', 'password' => '', 'from_email' => '', 'from_name' => 'Skrift Handschriftmarketing', ], 'mail_templates' => [ 'waiting' => [ 'subject' => 'Ihr Auftrag {{order_id}} ist bereit - bitte Daten hochladen', 'body' => "Guten Tag,\n\nIhr Auftrag {{order_id}} wurde angelegt. Bitte laden Sie Ihre Daten uber den folgenden Link hoch:\n\n{{link}}\n\nBei Fragen stehen wir Ihnen gerne zur Verfugung.\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], 'waiting_routine' => [ 'subject' => 'Ihr Auftrag {{order_id}} - Daten jederzeit hochladbar', 'body' => "Guten Tag,\n\nIhr Auftrag {{order_id}} wurde als Routine-Auftrag angelegt. Sie konnen uber den folgenden Link jederzeit neue Datensatze hochladen:\n\n{{link}}\n\nJeder Upload wird als separater Unterauftrag erfasst und bearbeitet.\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], 'text_proposal' => [ 'subject' => 'Ihr Textvorschlag zu Auftrag {{order_id}} ist bereit', 'body' => "Guten Tag,\n\nWir haben einen Brieftext-Vorschlag fur Ihren Auftrag {{order_id}} erstellt.\n\nBitte rufen Sie Ihren Auftrag uber folgenden Link auf, pruefen Sie den Text und passen Sie ihn bei Bedarf an:\n\n{{link}}\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], 'submitted' => [ 'subject' => 'Ihre Daten zu Auftrag {{order_id}} sind eingegangen', 'body' => "Guten Tag,\n\nVielen Dank! Wir haben Ihre Daten zu Auftrag {{order_id}} erfolgreich erhalten und machen uns nun an die Arbeit.\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], 'error' => [ 'subject' => 'Hinweis zu Auftrag {{order_id}} - Bitte Daten korrigieren', 'body' => "Guten Tag,\n\nBei der Prufung Ihrer Daten zu Auftrag {{order_id}} ist ein Problem aufgetreten:\n\n{{reason}}\n\nBitte korrigieren Sie Ihre Daten und laden Sie diese erneut hoch:\n\n{{link}}\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], 'printing' => [ 'subject' => 'Auftrag {{order_id}} druckfertig - Ihre Freigabe wird benotigt', 'body' => "Guten Tag,\n\nIhre Briefe zu Auftrag {{order_id}} sind fertig gedruckt und bereit zum Versand.\n\nBitte erteilen Sie die Versandfreigabe in Ihrem Auftragsportal:\n\n{{link}}\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], 'shipped' => [ 'subject' => 'Auftrag {{order_id}} wurde versendet', 'body' => "Guten Tag,\n\nIhre Briefe zu Auftrag {{order_id}} wurden versendet.\n\nVielen Dank fur Ihren Auftrag!\n\nMit freundlichen Grussen\nIhr Skrift-Team", ], ], ]); } // ─── Admin-Menu ────────────────────────────────────────────────────────────── public function add_admin_menu(): void { add_menu_page( 'Skrift Auftraege', 'Skrift Auftraege', 'manage_options', 'skrift-auftraege', [$this, 'render_page'], 'dashicons-edit-page', 30 ); } // ─── REST-Routen ───────────────────────────────────────────────────────────── public function register_rest_routes(): void { register_rest_route('skrift/v1', '/job/verify', [ 'methods' => 'GET', 'callback' => [$this, 'rest_verify_job'], 'permission_callback' => '__return_true', ]); register_rest_route('skrift/v1', '/job/template', [ 'methods' => 'GET', 'callback' => [$this, 'rest_download_template'], 'permission_callback' => '__return_true', ]); register_rest_route('skrift/v1', '/job/notify', [ 'methods' => 'POST', 'callback' => [$this, 'rest_notify_job'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); register_rest_route('skrift/v1', '/job/customer-email', [ 'methods' => 'POST', 'callback' => [$this, 'rest_update_customer_email'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); register_rest_route('skrift/v1', '/job/approve-shipping', [ 'methods' => 'POST', 'callback' => [$this, 'rest_approve_shipping'], 'permission_callback' => '__return_true', ]); register_rest_route('skrift/v1', '/job/upload-file', [ 'methods' => 'POST', 'callback' => [$this, 'rest_upload_file'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); register_rest_route('skrift/v1', '/job/upload-signature', [ 'methods' => 'POST', 'callback' => [$this, 'rest_upload_signature'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); register_rest_route('skrift/v1', '/job/sub-order/notify', [ 'methods' => 'POST', 'callback' => [$this, 'rest_notify_sub_order'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); register_rest_route('skrift/v1', '/job/register-order', [ 'methods' => 'POST', 'callback' => [$this, 'rest_register_order'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); register_rest_route('skrift/v1', '/job/forward-files', [ 'methods' => 'POST', 'callback' => [$this, 'rest_forward_files'], 'permission_callback' => [$this, 'rest_nonce_permission'], ]); } // ─── REST: Auftrag im Docker-Backend registrieren (gedruckte Briefe) ──────── public function rest_register_order($request) { $body = $request->get_json_params(); if (empty($body['orderNumber'])) { return new WP_Error('invalid_request', 'orderNumber fehlt', ['status' => 400]); } if (class_exists('Skrift_Konfigurator_Admin_Settings')) { $settings = Skrift_Konfigurator_Admin_Settings::get_settings(); $api_url = rtrim($settings['backend_connection']['api_url'] ?? '', '/'); $api_token = $settings['backend_connection']['api_token'] ?? ''; } else { $raw = get_option('skrift_konfigurator_settings', []); $api_url = rtrim($raw['backend_connection']['api_url'] ?? '', '/'); $api_token = $raw['backend_connection']['api_token'] ?? ''; } if (empty($api_url)) { return new WP_Error('no_backend', 'Backend-URL nicht konfiguriert', ['status' => 500]); } $response = wp_remote_post( $api_url . '/api/order/register', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-API-Token' => $api_token, ], 'body' => wp_json_encode($body), 'timeout' => 15, 'data_format' => 'body', ] ); if (is_wp_error($response)) { return new WP_Error('backend_error', $response->get_error_message(), ['status' => 502]); } $status = wp_remote_retrieve_response_code($response); $data = json_decode(wp_remote_retrieve_body($response), true); if ($status >= 400) { return new WP_Error('backend_error', $data['message'] ?? 'Backend-Fehler', ['status' => $status]); } return rest_ensure_response($data); } // ─── REST: Hochgeladene Auftragsdateien in den Backend-Auftragsordner kopieren ── // Adresse/Platzhalter-Excel, Briefdokument und Brief-Excel liegen bereits lokal // (rest_upload_file) und werden hier zusätzlich ans Docker-Backend weitergereicht, // damit sie im selben Auftragsordner wie die generierten Umschläge liegen. public function rest_forward_files($request) { $body = $request->get_json_params(); $job_id = sanitize_text_field($body['jobId'] ?? ''); $order_number = sanitize_text_field($body['orderNumber'] ?? ''); $sub_dir = sanitize_file_name($body['subDir'] ?? ''); if (empty($job_id) || empty($order_number)) { return new WP_Error('invalid_request', 'jobId und orderNumber sind erforderlich', ['status' => 400]); } if (class_exists('Skrift_Konfigurator_Admin_Settings')) { $settings = Skrift_Konfigurator_Admin_Settings::get_settings(); $api_url = rtrim($settings['backend_connection']['api_url'] ?? '', '/'); $api_token = $settings['backend_connection']['api_token'] ?? ''; } else { $raw = get_option('skrift_konfigurator_settings', []); $api_url = rtrim($raw['backend_connection']['api_url'] ?? '', '/'); $api_token = $raw['backend_connection']['api_token'] ?? ''; } if (empty($api_url)) { return new WP_Error('no_backend', 'Backend-URL nicht konfiguriert', ['status' => 500]); } $dir = self::get_job_dir($job_id) . (!empty($sub_dir) ? '/' . $sub_dir : ''); if (!is_dir($dir)) { return new WP_REST_Response(['success' => true, 'forwarded' => []], 200); } // fileType (Frontend) => lokaler Dateiname-Präfix (siehe rest_upload_file) $file_types = [ 'adresse' => 'adresse', 'platzhalter' => 'platzhalter', 'brief' => 'brief', 'brief_excel' => 'brief_platzhalter', ]; $forwarded = []; $errors = []; foreach ($file_types as $type => $prefix) { $matches = glob($dir . '/' . $prefix . '.*'); if (empty($matches)) continue; $file_path = $matches[0]; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file_path); finfo_close($finfo); $boundary = wp_generate_password(24, false); $filename = basename($file_path); $mp = "--{$boundary}\r\n"; $mp .= "Content-Disposition: form-data; name=\"orderNumber\"\r\n\r\n{$order_number}\r\n"; $mp .= "--{$boundary}\r\n"; $mp .= "Content-Disposition: form-data; name=\"fileType\"\r\n\r\n{$type}\r\n"; $mp .= "--{$boundary}\r\n"; $mp .= "Content-Disposition: form-data; name=\"file\"; filename=\"" . $filename . "\"\r\n"; $mp .= "Content-Type: " . ($mime ?: 'application/octet-stream') . "\r\n\r\n"; $mp .= file_get_contents($file_path) . "\r\n"; $mp .= "--{$boundary}--\r\n"; $response = wp_remote_post($api_url . '/api/order/files', [ 'headers' => [ 'Content-Type' => 'multipart/form-data; boundary=' . $boundary, 'X-API-Token' => $api_token, ], 'body' => $mp, // Große Brief-PDFs (mehrere Dutzend MB) brauchen länger als 30s für die Weiterleitung. 'timeout' => 180, ]); if (is_wp_error($response)) { $errors[] = "{$type}: " . $response->get_error_message(); continue; } $status = wp_remote_retrieve_response_code($response); if ($status >= 400) { $errors[] = "{$type}: HTTP {$status}"; continue; } $forwarded[] = $type; } return new WP_REST_Response(['success' => true, 'forwarded' => $forwarded, 'errors' => $errors], 200); } // ─── REST: Nonce-Prüfung für schreibende Endpunkte ─────────────────────────── public function rest_nonce_permission($request) { $nonce = $request->get_header('X-WP-Nonce'); if ($nonce && wp_verify_nonce($nonce, 'wp_rest')) { return true; } return new WP_Error('rest_forbidden', 'Ungültiger oder fehlender Nonce.', ['status' => 403]); } // ─── REST: Auftrag verifizieren ────────────────────────────────────────────── public function rest_verify_job($request) { $job_id = sanitize_text_field($request->get_param('id') ?? ''); if (empty($job_id)) { return new WP_Error('missing_id', 'Auftragsnummer fehlt', ['status' => 400]); } $jobs = self::get_jobs(); if (!isset($jobs[$job_id])) { return new WP_Error('not_found', 'Auftrag nicht gefunden', ['status' => 404]); } $job = $jobs[$job_id]; // Unteraufträge aufbereiten $sub_orders_raw = $job['sub_orders'] ?? []; $sub_orders_out = array_values(array_map(function($sub) { return [ 'id' => $sub['id'], 'status' => $sub['status'] ?? 'waiting', 'statusReason' => $sub['status_reason'] ?? '', 'statusUpdatedAt' => $sub['status_updated_at'] ?? '', 'createdAt' => $sub['created_at'] ?? '', 'orderNumber' => $sub['order_number'] ?? '', 'quantity' => (int) ($sub['quantity'] ?? 0), ]; }, $sub_orders_raw)); return new WP_REST_Response([ 'success' => true, 'job' => [ 'id' => $job['id'], 'produkt' => $job['produkt'], 'format' => $job['format'], 'menge' => (int) $job['menge'], 'kuvert' => $job['kuvert'], 'textservice' => (bool) $job['textservice'], 'signatur' => (int) ($job['signatur'] ?? 0), 'redirect_url' => $job['redirect_url'] ?? '', 'status' => $job['status'], 'statusReason' => $job['status_reason'] ?? '', 'statusUpdatedAt' => $job['status_updated_at'] ?? '', 'customerEmail' => $job['customer_email'] ?? '', 'proposedText' => $job['proposed_text'] ?? '', 'shippingToken' => ($job['status'] === 'printing') ? ($job['shipping_token'] ?? '') : '', 'subOrders' => $sub_orders_out, ], ], 200); } // ─── REST: Kunden-E-Mail aktualisieren ─────────────────────────────────────── public function rest_update_customer_email($request) { $job_id = sanitize_text_field($request->get_param('jobId') ?? ''); $email = sanitize_email($request->get_param('email') ?? ''); $jobs = self::get_jobs(); if (empty($job_id) || !isset($jobs[$job_id])) { return new WP_Error('not_found', 'Auftrag nicht gefunden', ['status' => 404]); } if (!empty($email) && !is_email($email)) { return new WP_Error('invalid_email', 'Ungultige E-Mail-Adresse', ['status' => 400]); } $jobs[$job_id]['customer_email'] = $email; update_option(self::JOBS_KEY, $jobs); return new WP_REST_Response(['success' => true], 200); } // ─── REST: Versandfreigabe ─────────────────────────────────────────────────── public function rest_approve_shipping($request) { $job_id = sanitize_text_field($request->get_param('jobId') ?? ''); $token = sanitize_text_field($request->get_param('token') ?? ''); $jobs = self::get_jobs(); if (empty($job_id) || !isset($jobs[$job_id])) { return new WP_Error('not_found', 'Auftrag nicht gefunden', ['status' => 404]); } $job = $jobs[$job_id]; if (empty($token) || $token !== ($job['shipping_token'] ?? '')) { return new WP_Error('invalid_token', 'Ungultiger Freigabe-Token', ['status' => 403]); } if ($job['status'] !== 'printing') { return new WP_REST_Response(['success' => true, 'message' => 'Freigabe bereits erteilt.'], 200); } // Versandfreigabe-Timestamp speichern $jobs[$job_id]['shipping_approved_at'] = current_time('mysql'); update_option(self::JOBS_KEY, $jobs); // Admin benachrichtigen $plugin_settings = self::get_plugin_settings(); $to_admin = !empty($plugin_settings['notification_email']) ? sanitize_email($plugin_settings['notification_email']) : get_option('admin_email'); $subject = "[Skrift] Versandfreigabe erteilt: {$job_id}"; $message = "Der Kunde hat die Versandfreigabe fur Auftrag {$job_id} erteilt.\n\nBitte veranlassen Sie den Versand und setzen Sie den Status auf 'Versendet'."; wp_mail($to_admin, $subject, $message, ['Content-Type: text/plain; charset=UTF-8']); return new WP_REST_Response(['success' => true, 'message' => 'Versandfreigabe wurde erteilt. Wir wurden informiert.'], 200); } // ─── REST: Datei-Upload (Adressdaten / Platzhalter) ───────────────────────── public function rest_upload_file($request) { $job_id = sanitize_text_field($request->get_param('jobId') ?? ''); $file_type = sanitize_text_field($request->get_param('fileType') ?? ''); $sub_dir = sanitize_file_name($request->get_param('subDir') ?? ''); $jobs = self::get_jobs(); if (empty($job_id) || !isset($jobs[$job_id])) { return new WP_Error('not_found', 'Auftrag nicht gefunden', ['status' => 404]); } $allowed_types = ['adresse', 'platzhalter', 'brief', 'brief_excel']; if (!in_array($file_type, $allowed_types, true)) { return new WP_Error('invalid_type', 'Ungültiger Dateityp', ['status' => 400]); } $files = $request->get_file_params(); $file = $files['file'] ?? null; if (!$file || $file['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_error', 'Datei-Upload fehlgeschlagen', ['status' => 400]); } $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); // MIME-Typen je nach Dateityp. Die Prüfung akzeptiert, wenn ENTWEDER der erkannte // MIME-Typ ODER die Dateiendung erlaubt ist. finfo liefert je nach Server für die // gleiche Datei unterschiedliche Werte (z.B. docx = application/zip, manche PDFs = // application/octet-stream). Ohne diese Endungs-Fallback-Prüfung wurden gültige // PDF-/DOC-/DOCX-Uploads still abgelehnt und kamen nie im Docker-Backend an. $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if ($file_type === 'brief') { $allowed_mimes = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', // docx wird oft als zip erkannt 'application/octet-stream', // generischer Fallback mancher Server ]; $allowed_exts = ['pdf', 'doc', 'docx']; if (!in_array($ext, $allowed_exts, true) && !in_array($mime, $allowed_mimes, true)) { return new WP_Error('invalid_mime', 'Nur .pdf, .doc oder .docx erlaubt', ['status' => 400]); } if (!in_array($ext, $allowed_exts, true)) { $ext = 'pdf'; } } else { // Tabellendateien (adresse, platzhalter, brief_excel) $allowed_mimes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'text/csv', 'text/plain', 'application/csv', 'application/zip', // xlsx wird oft als zip erkannt 'application/octet-stream', ]; $allowed_exts = ['xlsx', 'xls', 'csv']; if (!in_array($ext, $allowed_exts, true) && !in_array($mime, $allowed_mimes, true)) { return new WP_Error('invalid_mime', 'Nur .xlsx, .xls oder .csv erlaubt', ['status' => 400]); } if (!in_array($ext, $allowed_exts, true)) { $ext = 'xlsx'; } } // Unteraufträge: eigenes Unterverzeichnis je Bestellnummer, damit Uploads eines // neuen Unterauftrags die Dateien des vorherigen nicht überschreiben. $dir = self::get_job_dir($job_id) . (!empty($sub_dir) ? '/' . $sub_dir : ''); if (!wp_mkdir_p($dir)) { return new WP_Error('dir_error', 'Verzeichnis konnte nicht erstellt werden', ['status' => 500]); } self::protect_directory($dir); $filename = ($file_type === 'brief_excel' ? 'brief_platzhalter' : $file_type) . '.' . $ext; $dest_path = $dir . '/' . $filename; if (!move_uploaded_file($file['tmp_name'], $dest_path)) { return new WP_Error('save_error', 'Datei konnte nicht gespeichert werden', ['status' => 500]); } return new WP_REST_Response(['success' => true, 'saved' => $filename], 200); } // ─── REST: Signatur hochladen ──────────────────────────────────────────────── public function rest_upload_signature($request) { $job_id = sanitize_text_field($request->get_param('jobId') ?? ''); $index = max(0, (int) ($request->get_param('index') ?? 0)); $svg = $request->get_param('svg') ?? ''; $jobs = self::get_jobs(); if (empty($job_id) || !isset($jobs[$job_id])) { return new WP_Error('not_found', 'Auftrag nicht gefunden', ['status' => 404]); } if (empty($svg) || strpos(trim($svg), ' 400]); } // Nur SVG-Tags erlauben – rudimentäre Bereinigung if (preg_match('/

Skrift Kundenauftraege

Auftrag erfolgreich angelegt.

Auftrag geloscht.

Auftrag in den Papierkorb verschoben.

Auftrag wiederhergestellt.

Auftrag endgültig gelöscht.

Massenoperation abgeschlossen.

Unterauftrag gelöscht.

Diese Auftragsnummer existiert bereits.

Einstellungen gespeichert.

Status aktualisiert.

Neuen Auftrag anlegen

Bestehende Auftraege

empty($j['deleted_at'])); $trashed_jobs = array_filter($jobs, fn($j) => !empty($j['deleted_at'])); $kuvert_labels = ['keine' => 'Keine', 'adresse' => 'Adresse', 'individueller_text' => 'Ind. Text']; ?>
Aufträge aktiv

Noch keine Auftraege angelegt.

$sub, 'parent_id' => $pjob['id'], 'parent_job' => $pjob]; } } $status_groups = [ 'pending' => [ 'label' => 'Warten auf Daten / Offen', 'statuses' => ['waiting', 'waiting_routine', 'text_proposal', 'error'], 'sub_statuses' => ['waiting', 'error'], 'open' => true, ], 'submitted' => [ 'label' => 'Daten übermittelt', 'statuses' => ['submitted'], 'sub_statuses' => ['submitted'], 'open' => true, ], 'printing' => [ 'label' => 'Druck abgeschlossen', 'statuses' => ['printing'], 'sub_statuses' => ['printing'], 'open' => true, ], 'shipped' => [ 'label' => 'Versandt', 'statuses' => ['shipped'], 'sub_statuses' => ['shipped'], 'open' => false, ], ]; foreach ($status_groups as $group_key => $group) : $group_jobs = array_values(array_filter($active_jobs, fn($j) => in_array($j['status'] ?? 'waiting', $group['statuses'], true))); $group_subs = array_values(array_filter($all_subs, fn($s) => in_array($s['sub']['status'] ?? 'waiting', $group['sub_statuses'], true))); $gcount = count($group_jobs) + count($group_subs); ?>
0 && $group['open']) ? 'open' : ''; ?> class="sk-job-group" id="sk-group-">

Keine Aufträge in dieser Kategorie.

> >
Auftragsnummer Produkt / Format Menge Kuvert Sig. E-Mail Kunde Angelegt Status

0 ? esc_html($job['signatur']) : '–'; ?> –
Versandfreigabe am
Übermittelt am

↳

0 ? esc_html($pjob['signatur']) : '–'; ?> –
Papierkorb
Auftragsnummer Produkt Gelöscht am Aktionen

Globale Einstellungen

Eingehende Auftraege werden hier gemeldet. Leer = Admin-E-Mail.
Seite mit dem Shortcode [skrift_kundenauftrag] – wird in E-Mails verlinkt.
Kunde wird nach Datenubergabe hierhin weitergeleitet (mit ?orderNumber=…).

SMTP-Einstellungen

Ausgehender Mailversand fur Kunden-E-Mails. Wenn kein Host angegeben ist, verwendet WordPress den Server-Standard (oft unzuverlassig).

465 = SSL/TLS  ·  587 = STARTTLS
Leer = SMTP-Benutzername wird als Absenderadresse genutzt.

E-Mail-Vorlagen an Kunden

Verfugbare Platzhalter: {{order_id}}, {{link}}, {{reason}} (nur "Fehlerhaft"), {{approve_link_button}} (nur "Druck abgeschlossen").
Farbige Hervorhebung: [rot]Ihr Text hier[/rot] → erscheint rot & fett im E-Mail.

['label' => 'Warten auf Daten', 'badge' => 'sk-badge-waiting', 'note' => 'Wird beim Anlegen des Auftrags gesendet (wenn E-Mail hinterlegt)'], 'waiting_routine' => ['label' => 'Warten auf Daten Routine','badge' => 'sk-badge-waiting-routine', 'note' => 'Wird gesendet wenn Status auf Routine gesetzt wird'], 'text_proposal' => ['label' => 'Textvorschlag hinterlegt','badge' => 'sk-badge-text-proposal', 'note' => 'Wird gesendet wenn Sie einen Textvorschlag hinterlegen'], 'submitted' => ['label' => 'Daten ubermittelt', 'badge' => 'sk-badge-submitted', 'note' => 'Wird nach Datenubergabe durch den Kunden gesendet'], 'error' => ['label' => 'Daten fehlerhaft', 'badge' => 'sk-badge-error', 'note' => 'Wird gesendet wenn Sie den Status auf Fehlerhaft setzen'], 'printing' => ['label' => 'Druck abgeschlossen', 'badge' => 'sk-badge-printing', 'note' => 'Enthalt Versandfreigabe-Button ({{approve_link_button}})'], 'shipped' => ['label' => 'Versendet', 'badge' => 'sk-badge-shipped', 'note' => 'Abschluss-Benachrichtigung'], ]; foreach ($tpl_labels as $tkey => $tinfo) : $tpl = $templates[$tkey] ?? ['subject' => '', 'body' => '']; ?>

'duplicate', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } $jobs[$job_id] = [ 'id' => $job_id, 'produkt' => $produkt, 'format' => $format, 'menge' => $menge, 'kuvert' => $kuvert, 'textservice' => $textservice, 'signatur' => $signatur, 'sub_orders' => [], 'customer_email' => $customer_email, 'status' => $initial_status, 'status_reason' => ($initial_status === 'error') ? $initial_reason : '', 'status_updated_at' => current_time('mysql'), 'shipping_token' => ($initial_status === 'printing') ? wp_generate_password(32, false) : '', 'proposed_text' => ($initial_status === 'text_proposal') ? $initial_proposed_text : '', 'print_approved_at' => ($initial_status === 'submitted') ? current_time('mysql') : '', 'shipping_approved_at' => '', 'created_at' => current_time('mysql'), ]; update_option(self::JOBS_KEY, $jobs); // Kunden-E-Mail senden (fur den gewahlten Anfangsstatus) if (!empty($customer_email) && is_email($customer_email)) { self::send_customer_email($job_id, $initial_status, $initial_reason); } wp_redirect(add_query_arg(['sk_notice' => 'created', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Status aktualisieren ───────────────────────────────────────────── public function handle_update_status(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_update_status'); $job_id = sanitize_text_field($_POST['job_id'] ?? ''); $new_status = sanitize_text_field($_POST['new_status'] ?? ''); $reason = sanitize_textarea_field($_POST['status_reason'] ?? ''); $proposed_text = sanitize_textarea_field($_POST['proposed_text'] ?? ''); if (!array_key_exists($new_status, self::STATUSES)) { wp_redirect(add_query_arg(['sk_notice' => 'error', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } self::update_status($job_id, $new_status, $reason, $proposed_text); wp_redirect(add_query_arg(['sk_notice' => 'status_updated', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Auftrag loschen ─────────────────────────────────────────────────── public function handle_delete_job(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_delete'); $job_id = sanitize_text_field($_POST['job_id'] ?? ''); $jobs = self::get_jobs(); unset($jobs[$job_id]); update_option(self::JOBS_KEY, $jobs); wp_redirect(add_query_arg(['sk_notice' => 'deleted', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Auftrag in Papierkorb verschieben ──────────────────────────────── public function handle_trash_job(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_trash'); $job_id = sanitize_text_field($_POST['job_id'] ?? ''); $jobs = self::get_jobs(); if (isset($jobs[$job_id])) { $jobs[$job_id]['deleted_at'] = current_time('mysql'); update_option(self::JOBS_KEY, $jobs); } wp_redirect(add_query_arg(['sk_notice' => 'trashed', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Auftrag aus Papierkorb wiederherstellen ────────────────────────── public function handle_restore_job(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_restore'); $job_id = sanitize_text_field($_POST['job_id'] ?? ''); $jobs = self::get_jobs(); if (isset($jobs[$job_id])) { $jobs[$job_id]['deleted_at'] = null; update_option(self::JOBS_KEY, $jobs); } wp_redirect(add_query_arg(['sk_notice' => 'restored', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Auftrag endgültig löschen ──────────────────────────────────────── public function handle_purge_job(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_purge'); $job_id = sanitize_text_field($_POST['job_id'] ?? ''); $jobs = self::get_jobs(); $dir = self::get_job_dir($job_id); if (is_dir($dir)) { self::delete_directory($dir); } unset($jobs[$job_id]); update_option(self::JOBS_KEY, $jobs); wp_redirect(add_query_arg(['sk_notice' => 'purged', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Massenoperation ─────────────────────────────────────────────────── public function handle_bulk_job(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_bulk'); $action = sanitize_text_field($_POST['bulk_action'] ?? ''); $job_ids = array_map('sanitize_text_field', (array) ($_POST['job_ids'] ?? [])); $sub_ids = array_map('sanitize_text_field', (array) ($_POST['sub_ids'] ?? [])); $jobs = self::get_jobs(); // Oberaufträge foreach ($job_ids as $job_id) { if (!isset($jobs[$job_id])) continue; if ($action === 'trash') { $jobs[$job_id]['deleted_at'] = current_time('mysql'); } } // Unteraufträge (Format: "parent_id:sub_id") foreach ($sub_ids as $entry) { $parts = explode(':', $entry, 2); if (count($parts) !== 2) continue; [$parent_id, $sub_id] = $parts; if (!isset($jobs[$parent_id])) continue; if ($action === 'trash') { $jobs[$parent_id]['sub_orders'] = array_values(array_filter( $jobs[$parent_id]['sub_orders'] ?? [], fn($s) => $s['id'] !== $sub_id )); } } update_option(self::JOBS_KEY, $jobs); wp_redirect(add_query_arg(['sk_notice' => 'bulk_done', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Admin: Globale Einstellungen speichern ────────────────────────────────── public function handle_save_settings(): void { if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.'); check_admin_referer('skrift_job_settings'); $raw_templates = $_POST['mail_templates'] ?? []; $templates = []; foreach (array_keys(self::STATUSES) as $skey) { $templates[$skey] = [ 'subject' => sanitize_text_field($raw_templates[$skey]['subject'] ?? ''), 'body' => sanitize_textarea_field($raw_templates[$skey]['body'] ?? ''), ]; } $smtp_raw = $_POST['smtp'] ?? []; $smtp = [ 'host' => sanitize_text_field($smtp_raw['host'] ?? ''), 'port' => max(1, (int) ($smtp_raw['port'] ?? 465)), 'username' => sanitize_text_field($smtp_raw['username'] ?? ''), 'password' => sanitize_text_field($smtp_raw['password'] ?? ''), 'from_email' => sanitize_email($smtp_raw['from_email'] ?? ''), 'from_name' => sanitize_text_field($smtp_raw['from_name'] ?? 'Skrift Handschriftmarketing'), ]; update_option(self::SETTINGS_KEY, [ 'notification_email' => sanitize_email($_POST['job_global_notification_email'] ?? ''), 'portal_url' => esc_url_raw($_POST['job_global_portal_url'] ?? ''), 'redirect_url' => esc_url_raw($_POST['job_global_redirect_url'] ?? ''), 'smtp' => $smtp, 'mail_templates' => $templates, ]); wp_redirect(add_query_arg(['sk_notice' => 'settings_saved', 'sk_tab' => 'einstellungen'], admin_url('admin.php?page=skrift-auftraege'))); exit; } // ─── Hilfsmethoden ─────────────────────────────────────────────────────────── public static function get_jobs(): array { $jobs = get_option(self::JOBS_KEY, []); foreach ($jobs as &$job) { // Backward-Kompatibilitat: alte Status-Werte normalisieren if (!isset($job['status'])) $job['status'] = 'waiting'; if ($job['status'] === 'open') $job['status'] = 'waiting'; if ($job['status'] === 'completed') $job['status'] = 'submitted'; if (!isset($job['customer_email'])) $job['customer_email'] = ''; if (!isset($job['status_reason'])) $job['status_reason'] = ''; if (!isset($job['status_updated_at'])) $job['status_updated_at'] = ''; if (!isset($job['shipping_token'])) $job['shipping_token'] = ''; if (!isset($job['proposed_text'])) $job['proposed_text'] = ''; if (!isset($job['print_approved_at'])) $job['print_approved_at'] = ''; if (!isset($job['shipping_approved_at'])) $job['shipping_approved_at'] = ''; if (!isset($job['signatur'])) $job['signatur'] = 0; if (!isset($job['sub_orders'])) $job['sub_orders'] = []; if (!isset($job['order_number'])) $job['order_number'] = ''; } return $jobs; } /** * Status eines Auftrags aktualisieren und Kunden-E-Mail senden. */ public static function update_status(string $job_id, string $new_status, string $reason = '', string $proposed_text = ''): bool { $jobs = self::get_jobs(); if (!isset($jobs[$job_id])) return false; $jobs[$job_id]['status'] = $new_status; $jobs[$job_id]['status_reason'] = $reason; $jobs[$job_id]['status_updated_at'] = current_time('mysql'); // Textvorschlag speichern if ($new_status === 'text_proposal') { $jobs[$job_id]['proposed_text'] = $proposed_text; } // Druckfreigabe-Timestamp: Kunde hat Daten ubermittelt → Druck kann starten if ($new_status === 'submitted' && empty($jobs[$job_id]['print_approved_at'])) { $jobs[$job_id]['print_approved_at'] = current_time('mysql'); } // Shipping-Token fur Versandfreigabe if ($new_status === 'printing' && empty($jobs[$job_id]['shipping_token'])) { $jobs[$job_id]['shipping_token'] = wp_generate_password(32, false); } update_option(self::JOBS_KEY, $jobs); // Kunden-E-Mail versenden self::send_customer_email($job_id, $new_status, $reason); return true; } /** Backward-Compat-Alias */ public static function mark_completed(string $job_id): void { self::update_status($job_id, 'submitted'); } /** * SMTP-Versand via phpmailer_init konfigurieren. */ public function configure_smtp($phpmailer): void { $settings = self::get_plugin_settings(); $smtp = $settings['smtp'] ?? []; if (empty($smtp['host'])) return; $port = (int) ($smtp['port'] ?? 465); $phpmailer->isSMTP(); $phpmailer->Host = $smtp['host']; $phpmailer->SMTPAuth = true; $phpmailer->Username = $smtp['username']; $phpmailer->Password = $smtp['password']; $phpmailer->SMTPSecure = ($port === 587) ? 'tls' : 'ssl'; // 587=STARTTLS, 465=SSL/TLS $phpmailer->Port = $port; $phpmailer->CharSet = 'UTF-8'; $fromEmail = !empty($smtp['from_email']) ? $smtp['from_email'] : $smtp['username']; $fromName = !empty($smtp['from_name']) ? $smtp['from_name'] : 'Skrift'; $phpmailer->setFrom($fromEmail, $fromName); } /** * Kunden-E-Mail anhand eines Templates senden. */ private static function send_customer_email(string $job_id, string $status, string $reason = ''): void { $jobs = self::get_jobs(); if (!isset($jobs[$job_id])) return; $job = $jobs[$job_id]; $to = sanitize_email($job['customer_email'] ?? ''); if (empty($to) || !is_email($to)) return; $settings = self::get_plugin_settings(); $templates = $settings['mail_templates'] ?? []; $template = $templates[$status] ?? null; if (!$template || empty($template['subject'])) return; // Portal-URL aufbauen $portal_url = !empty($settings['portal_url']) ? $settings['portal_url'] : site_url('/'); $order_link = add_query_arg('nr', rawurlencode($job_id), $portal_url); // Versandfreigabe-Link (nur fur 'printing') $approve_link = ''; $approve_link_button = ''; if ($status === 'printing' && !empty($job['shipping_token'])) { $approve_link = add_query_arg([ 'nr' => rawurlencode($job_id), 'sk_approve' => rawurlencode($job['shipping_token']), ], $portal_url); $approve_link_button = '' . 'Versandfreigabe erteilen'; } // Template-Text: {{approve_link_button}} erst durch Platzhalter ersetzen (vor htmlspecialchars) $body_raw = $template['body']; $body_raw = str_replace('{{approve_link_button}}', '[__APPROVE_BTN__]', $body_raw); // Restliche Platzhalter ersetzen $body_raw = str_replace( ['{{order_id}}', '{{link}}', '{{reason}}', '{{approve_link}}'], [esc_html($job_id), esc_url($order_link), esc_html($reason), esc_url($approve_link ?: $order_link)], $body_raw ); $subject = str_replace('{{order_id}}', $job_id, $template['subject']); // HTML-Body bauen $html_body = nl2br(htmlspecialchars($body_raw, ENT_QUOTES, 'UTF-8')); // Approve-Button als echtes HTML einfugen $html_body = str_replace('[__APPROVE_BTN__]', $approve_link_button, $html_body); // [rot]...[/rot] Tags als roten, fetten Text rendern (direkt im Template schreibbar) $html_body = preg_replace( '/\[rot\](.*?)\[\/rot\]/s', '$1', $html_body ); $html = '' . '
' . 'SKRIFT' . '
' . $html_body . '
' . 'Skrift Handschrift-Service | Diese E-Mail wurde automatisch generiert.' . '
'; wp_mail($to, $subject, $html, ['Content-Type: text/html; charset=UTF-8', 'Cc: hello@skrift.de']); } // ─── Datei-Verwaltung ──────────────────────────────────────────────────────── /** * Gibt das Verzeichnis für Auftragsdateien zurück (kein Trailing-Slash). */ public static function get_job_dir(string $job_id): string { $upload = wp_upload_dir(); return $upload['basedir'] . '/skrift-auftraege/' . sanitize_file_name($job_id); } /** * Legt eine .htaccess an, die direkten HTTP-Zugriff auf das Verzeichnis sperrt. */ private static function protect_directory(string $dir): void { $htaccess = $dir . '/.htaccess'; if (!file_exists($htaccess)) { file_put_contents($htaccess, "Deny from all\n\n Order allow,deny\n Allow from all\n\n"); } } /** * Löscht ein Verzeichnis mit allem Inhalt rekursiv. */ private static function delete_directory(string $dir): void { if (!is_dir($dir)) return; foreach (array_diff((array) scandir($dir), ['.', '..']) as $entry) { $path = $dir . '/' . $entry; is_dir($path) ? self::delete_directory($path) : unlink($path); } rmdir($dir); } /** * Cron-Callback: Aufträge im Status "versendet" nach 3 Monaten löschen. */ public function cleanup_old_jobs(): void { $jobs = self::get_jobs(); $cutoff = strtotime('-3 months'); $changed = false; foreach ($jobs as $job_id => $job) { if (($job['status'] ?? '') !== 'shipped') continue; // Zeitstempel des Versand-Status als Referenz $shipped_ts = !empty($job['status_updated_at']) ? strtotime($job['status_updated_at']) : (!empty($job['created_at']) ? strtotime($job['created_at']) : 0); if ($shipped_ts > 0 && $shipped_ts < $cutoff) { // Dateien löschen $dir = self::get_job_dir($job_id); if (is_dir($dir)) { self::delete_directory($dir); } // Auftrag aus der Liste entfernen unset($jobs[$job_id]); $changed = true; } } if ($changed) { update_option(self::JOBS_KEY, $jobs); } } // ─── xlsx-Generator ─────────────────────────────────────────────────────────── private static function create_xlsx(array $rows): string { if (!class_exists('ZipArchive')) return ''; $tmp = tempnam(sys_get_temp_dir(), 'skrift_xlsx_'); $zip = new ZipArchive(); $zip->open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE); $zip->addFromString('[Content_Types].xml', self::xlsx_content_types()); $zip->addFromString('_rels/.rels', self::xlsx_rels()); $zip->addFromString('xl/workbook.xml', self::xlsx_workbook()); $zip->addFromString('xl/_rels/workbook.xml.rels', self::xlsx_workbook_rels()); $zip->addFromString('xl/worksheets/sheet1.xml', self::xlsx_sheet($rows)); $zip->addFromString('xl/styles.xml', self::xlsx_styles()); $zip->close(); $content = file_get_contents($tmp); unlink($tmp); return $content; } private static function xlsx_content_types(): string { return ' '; } private static function xlsx_rels(): string { return ' '; } private static function xlsx_workbook(): string { return ' '; } private static function xlsx_workbook_rels(): string { return ' '; } private static function xlsx_styles(): string { return ' '; } private static function xlsx_sheet(array $rows): string { $rows_xml = ''; $row_num = 1; foreach ($rows as $row) { $cells_xml = ''; $col_idx = 0; foreach ($row as $cell) { $col = self::col_letter($col_idx); $ref = $col . $row_num; $escaped = htmlspecialchars((string) $cell, ENT_XML1, 'UTF-8'); $style = ($row_num === 1) ? ' s="1"' : ''; $cells_xml .= "{$escaped}"; $col_idx++; } $rows_xml .= "{$cells_xml}"; $row_num++; } return ' ' . $rows_xml . ' '; } private static function col_letter(int $n): string { $letter = ''; do { $letter = chr(65 + ($n % 26)) . $letter; $n = intval($n / 26) - 1; } while ($n >= 0); return $letter; } } new Skrift_Job_Manager();