Struktur: Docker/ (skrift-backend, signature-service), Webseite/ (skrift-configurator, skrift-job-manager), Produktion/ (skrift-produktion, AutoRecover). Aufgeräumt: signature-to-svg entfernt, Backend-PayPal-Service entfernt (PayPal läuft client-seitig im Configurator), Dev-/Test-Artefakte und Deployment-Dokus bereinigt, .dockerignore repariert. CLAUDE.md mit Architektur ergänzt. Secrets (.env, config.json) via .gitignore ausgeschlossen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2163 lines
105 KiB
PHP
2163 lines
105 KiB
PHP
<?php
|
||
/**
|
||
* Kundenauftrag-Verwaltung – Admin-Seite und REST-Endpunkte
|
||
*/
|
||
|
||
if (!defined('ABSPATH')) { exit; }
|
||
|
||
final class Skrift_Job_Manager {
|
||
|
||
const JOBS_KEY = 'skrift_konfigurator_job_templates';
|
||
const SETTINGS_KEY = 'skrift_job_manager_settings';
|
||
|
||
/** Auftragsstatus */
|
||
const STATUSES = [
|
||
'waiting' => '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), '<svg') !== 0) {
|
||
return new WP_Error('invalid_svg', 'Ungültige SVG-Daten', ['status' => 400]);
|
||
}
|
||
|
||
// Nur SVG-Tags erlauben – rudimentäre Bereinigung
|
||
if (preg_match('/<script/i', $svg) || preg_match('/javascript:/i', $svg)) {
|
||
return new WP_Error('invalid_svg', 'Unsichere SVG-Inhalte', ['status' => 400]);
|
||
}
|
||
|
||
$dir = self::get_job_dir($job_id);
|
||
if (!wp_mkdir_p($dir)) {
|
||
return new WP_Error('dir_error', 'Verzeichnis konnte nicht erstellt werden', ['status' => 500]);
|
||
}
|
||
self::protect_directory($dir);
|
||
|
||
$filename = 'signatur-' . $index . '.svg';
|
||
file_put_contents($dir . '/' . $filename, $svg);
|
||
|
||
return new WP_REST_Response(['success' => true, 'saved' => $filename], 200);
|
||
}
|
||
|
||
// ─── REST: Unterauftrag anlegen und Admin benachrichtigen ────────────────────
|
||
|
||
public function rest_notify_sub_order($request) {
|
||
$job_id = sanitize_text_field($request->get_param('jobId') ?? '');
|
||
$order_number = sanitize_text_field($request->get_param('orderNumber') ?? '');
|
||
$quantity = max(0, (int) ($request->get_param('quantity') ?? 0));
|
||
$count_de = $request->get_param('countDE');
|
||
$count_abroad = $request->get_param('countAbroad');
|
||
$brief_text = sanitize_textarea_field($request->get_param('briefText') ?? '');
|
||
|
||
$jobs = self::get_jobs();
|
||
if (empty($job_id) || !isset($jobs[$job_id])) {
|
||
return new WP_Error('not_found', 'Auftrag nicht gefunden', ['status' => 404]);
|
||
}
|
||
|
||
if ($jobs[$job_id]['status'] !== 'waiting_routine') {
|
||
return new WP_Error('invalid_status', 'Unteraufträge nur bei Status „Warten auf Daten Routine" möglich', ['status' => 400]);
|
||
}
|
||
|
||
// Sub-Order-ID generieren: PARENT-01, -02, …
|
||
$existing = $jobs[$job_id]['sub_orders'] ?? [];
|
||
$next_num = str_pad(count($existing) + 1, 2, '0', STR_PAD_LEFT);
|
||
$sub_id = $job_id . '-' . $next_num;
|
||
|
||
$sub_order = [
|
||
'id' => $sub_id,
|
||
'status' => 'submitted',
|
||
'status_reason' => '',
|
||
'status_updated_at'=> current_time('mysql'),
|
||
'order_number' => $order_number,
|
||
'quantity' => $quantity,
|
||
'created_at' => current_time('mysql'),
|
||
];
|
||
|
||
$jobs[$job_id]['sub_orders'][] = $sub_order;
|
||
update_option(self::JOBS_KEY, $jobs);
|
||
|
||
// Brieftext speichern
|
||
if (!empty($brief_text)) {
|
||
$dir = self::get_job_dir($job_id) . '/' . sanitize_file_name($sub_id);
|
||
wp_mkdir_p($dir);
|
||
self::protect_directory($dir);
|
||
file_put_contents($dir . '/brieftext.txt', $brief_text);
|
||
}
|
||
|
||
// Admin benachrichtigen
|
||
$plugin_settings = self::get_plugin_settings();
|
||
$to = !empty($plugin_settings['notification_email'])
|
||
? sanitize_email($plugin_settings['notification_email'])
|
||
: get_option('admin_email');
|
||
|
||
$job = $jobs[$job_id];
|
||
$lines = [
|
||
'Ein neuer Unterauftrag wurde übermittelt.',
|
||
'',
|
||
'------------------------------------',
|
||
"Hauptauftrag: {$job_id}",
|
||
"Unterauftrag-ID: {$sub_id}",
|
||
"Bestellnummer: {$order_number}",
|
||
"Produkt: {$job['produkt']}",
|
||
"Menge: {$quantity}",
|
||
];
|
||
|
||
if ($count_de !== null && $count_abroad !== null) {
|
||
$lines[] = " Deutschland: {$count_de}";
|
||
$lines[] = " Drittlaender: {$count_abroad}";
|
||
}
|
||
|
||
$lines[] = '------------------------------------';
|
||
$lines[] = 'Übermittelt am: ' . date_i18n('d.m.Y') . ' um ' . date_i18n('H:i') . ' Uhr';
|
||
$lines[] = '';
|
||
$lines[] = '-- Skrift Auftragsmanager';
|
||
|
||
$sent = wp_mail($to, "[Skrift] Neuer Unterauftrag: {$sub_id}", implode("\r\n", $lines), ['Content-Type: text/plain; charset=UTF-8']);
|
||
|
||
// Kunden-E-Mail senden
|
||
$customer_email = sanitize_email($job['customer_email'] ?? '');
|
||
if (!empty($customer_email) && is_email($customer_email)) {
|
||
$settings = self::get_plugin_settings();
|
||
$templates = $settings['mail_templates'] ?? [];
|
||
$template = $templates['submitted'] ?? null;
|
||
if ($template && !empty($template['subject'])) {
|
||
$portal_url = !empty($settings['portal_url']) ? $settings['portal_url'] : site_url('/');
|
||
$order_link = add_query_arg('nr', rawurlencode($job_id), $portal_url);
|
||
$subject = str_replace('{{order_id}}', $sub_id, $template['subject']);
|
||
$body_raw = str_replace(['{{order_id}}', '{{link}}'], [$sub_id, esc_url($order_link)], $template['body']);
|
||
$html_body = nl2br(htmlspecialchars($body_raw, ENT_QUOTES, 'UTF-8'));
|
||
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Arial,sans-serif;font-size:14px;line-height:1.6;color:#333;max-width:560px;margin:0 auto;padding:24px;">'
|
||
. '<div style="border-bottom:2px solid #1e3a6a;padding-bottom:12px;margin-bottom:20px;"><strong style="font-size:20px;color:#1e3a6a;">SKRIFT</strong></div>'
|
||
. $html_body . '</body></html>';
|
||
wp_mail($customer_email, $subject, $html, ['Content-Type: text/html; charset=UTF-8', 'Cc: hello@skrift.de']);
|
||
}
|
||
}
|
||
|
||
return new WP_REST_Response(['success' => true, 'subOrderId' => $sub_id, 'mailSent' => $sent], 200);
|
||
}
|
||
|
||
// ─── Admin: Unterauftrag-Status aktualisieren ─────────────────────────────────
|
||
|
||
public function handle_update_sub_order_status(): void {
|
||
if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.');
|
||
check_admin_referer('skrift_sub_order_update_status');
|
||
|
||
$job_id = sanitize_text_field($_POST['job_id'] ?? '');
|
||
$sub_id = sanitize_text_field($_POST['sub_id'] ?? '');
|
||
$new_status= sanitize_text_field($_POST['new_status'] ?? '');
|
||
$reason = sanitize_textarea_field($_POST['status_reason'] ?? '');
|
||
|
||
if (!array_key_exists($new_status, self::SUB_STATUSES)) {
|
||
wp_redirect(add_query_arg(['sk_notice' => 'error', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege')));
|
||
exit;
|
||
}
|
||
|
||
$jobs = self::get_jobs();
|
||
if (!isset($jobs[$job_id])) {
|
||
wp_redirect(add_query_arg(['sk_notice' => 'error', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege')));
|
||
exit;
|
||
}
|
||
|
||
$sub_orders = &$jobs[$job_id]['sub_orders'];
|
||
foreach ($sub_orders as &$sub) {
|
||
if ($sub['id'] === $sub_id) {
|
||
$sub['status'] = $new_status;
|
||
$sub['status_reason'] = $reason;
|
||
$sub['status_updated_at'] = current_time('mysql');
|
||
break;
|
||
}
|
||
}
|
||
unset($sub);
|
||
|
||
update_option(self::JOBS_KEY, $jobs);
|
||
|
||
wp_redirect(add_query_arg(['sk_notice' => 'status_updated', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege')));
|
||
exit;
|
||
}
|
||
|
||
// ─── Admin: Unterauftrag löschen ───────────────────────────────────────────
|
||
|
||
public function handle_delete_sub_order(): void {
|
||
if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.');
|
||
check_admin_referer('skrift_sub_order_delete');
|
||
|
||
$job_id = sanitize_text_field($_POST['job_id'] ?? '');
|
||
$sub_id = sanitize_text_field($_POST['sub_id'] ?? '');
|
||
|
||
$jobs = self::get_jobs();
|
||
if (!isset($jobs[$job_id])) {
|
||
wp_redirect(add_query_arg(['sk_notice' => 'error', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege')));
|
||
exit;
|
||
}
|
||
|
||
$jobs[$job_id]['sub_orders'] = array_values(array_filter(
|
||
$jobs[$job_id]['sub_orders'] ?? [],
|
||
fn($s) => $s['id'] !== $sub_id
|
||
));
|
||
|
||
update_option(self::JOBS_KEY, $jobs);
|
||
wp_redirect(add_query_arg(['sk_notice' => 'sub_deleted', 'sk_tab' => 'auftraege'], admin_url('admin.php?page=skrift-auftraege')));
|
||
exit;
|
||
}
|
||
|
||
// ─── REST: Admin-Benachrichtigung ────────────────────────────────────────────
|
||
|
||
public function rest_notify_job($request) {
|
||
$job_id = sanitize_text_field($request->get_param('jobId') ?? '');
|
||
$order_number = sanitize_text_field($request->get_param('orderNumber') ?? '');
|
||
$quantity = max(0, (int) ($request->get_param('quantity') ?? 0));
|
||
$count_de = $request->get_param('countDE');
|
||
$count_abroad = $request->get_param('countAbroad');
|
||
$brief_text = sanitize_textarea_field($request->get_param('briefText') ?? '');
|
||
|
||
$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];
|
||
$plugin_settings = self::get_plugin_settings();
|
||
$to = !empty($plugin_settings['notification_email'])
|
||
? sanitize_email($plugin_settings['notification_email'])
|
||
: get_option('admin_email');
|
||
|
||
$kuvert_labels = [
|
||
'keine' => 'Keine',
|
||
'adresse' => 'Adresse',
|
||
'individueller_text' => 'Individueller Text',
|
||
];
|
||
$kuvert_label = $kuvert_labels[$job['kuvert']] ?? $job['kuvert'];
|
||
|
||
$lines = [];
|
||
$lines[] = 'Ein neuer Kundenauftrag wurde erfolgreich ubermittelt.';
|
||
$lines[] = '';
|
||
$lines[] = '------------------------------------';
|
||
$lines[] = "Auftragsnummer: {$job_id}";
|
||
$lines[] = "Bestellnummer: {$order_number}";
|
||
$lines[] = "Produkt: {$job['produkt']}";
|
||
if (!empty($job['format'])) { $lines[] = "Format: {$job['format']}"; }
|
||
$lines[] = "Kuvert: {$kuvert_label}";
|
||
$lines[] = '------------------------------------';
|
||
$lines[] = "Gesamtmenge: {$quantity}";
|
||
|
||
if ($count_de !== null && $count_abroad !== null) {
|
||
$lines[] = " Deutschland: {$count_de}";
|
||
$lines[] = " Drittlaender: {$count_abroad}";
|
||
} else {
|
||
$lines[] = ' (Laenderaufschluesselung nicht verfugbar)';
|
||
}
|
||
|
||
$lines[] = '------------------------------------';
|
||
$lines[] = 'Ubermittelt am: ' . date_i18n('d.m.Y') . ' um ' . date_i18n('H:i') . ' Uhr';
|
||
$lines[] = '';
|
||
$lines[] = '-- Skrift Auftragsmanager';
|
||
|
||
$sent = wp_mail($to, "[Skrift] Neuer Auftrag: {$job_id}", implode("\r\n", $lines), ['Content-Type: text/plain; charset=UTF-8']);
|
||
|
||
// Brieftext als Textdatei speichern
|
||
if (!empty($brief_text)) {
|
||
$dir = self::get_job_dir($job_id);
|
||
wp_mkdir_p($dir);
|
||
self::protect_directory($dir);
|
||
file_put_contents($dir . '/brieftext.txt', $brief_text);
|
||
}
|
||
|
||
// Bestellnummer im Auftrag speichern
|
||
$jobs = self::get_jobs();
|
||
if (isset($jobs[$job_id])) {
|
||
$jobs[$job_id]['order_number'] = $order_number;
|
||
update_option(self::JOBS_KEY, $jobs);
|
||
}
|
||
|
||
// Status auf 'submitted' setzen und Kunden-E-Mail senden
|
||
self::update_status($job_id, 'submitted');
|
||
|
||
return new WP_REST_Response(['success' => true, 'mailSent' => $sent], 200);
|
||
}
|
||
|
||
// ─── REST: Excel-Template herunterladen ──────────────────────────────────────
|
||
|
||
public function rest_download_template($request) {
|
||
$type = sanitize_text_field($request->get_param('type') ?? '');
|
||
$menge = max(1, (int) ($request->get_param('menge') ?? 10));
|
||
|
||
switch ($type) {
|
||
case 'adresse-standard':
|
||
$headers = ['Vorname', 'Nachname', 'Strasse', 'Hausnummer', 'PLZ', 'Ort', 'Land'];
|
||
$example = ['Max', 'Mustermann', 'Musterstrasse', '1', '12345', 'Berlin', 'Deutschland'];
|
||
$filename = 'adressliste-standard.xlsx';
|
||
break;
|
||
case 'adresse-frei':
|
||
$headers = ['Zeile 1', 'Zeile 2', 'Zeile 3', 'Zeile 4', 'Zeile 5'];
|
||
$example = ['Firma Muster GmbH', 'Frau Maxima Mustermann', 'Musterstrasse 1', '12345 Berlin', 'Deutschland'];
|
||
$filename = 'adressliste-freitext.xlsx';
|
||
break;
|
||
case 'individueller-text':
|
||
$headers = ['Kuverttext'];
|
||
$example = ['Sehr geehrte Damen und Herren, herzliche Grusse aus dem Hause Skrift'];
|
||
$filename = 'kuvert-individueller-text.xlsx';
|
||
break;
|
||
default:
|
||
return new WP_Error('invalid_type', 'Ungultiger Template-Typ', ['status' => 400]);
|
||
}
|
||
|
||
$rows = [$headers, $example];
|
||
for ($i = 1; $i < $menge; $i++) { $rows[] = array_fill(0, count($headers), ''); }
|
||
|
||
$xlsx_data = self::create_xlsx($rows);
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||
header('Content-Length: ' . strlen($xlsx_data));
|
||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||
echo $xlsx_data; // phpcs:ignore
|
||
exit;
|
||
}
|
||
|
||
// ─── Admin: Seite rendern ────────────────────────────────────────────────────
|
||
|
||
public function render_page(): void {
|
||
if (!current_user_can('manage_options')) return;
|
||
|
||
$jobs = self::get_jobs();
|
||
$global = self::get_plugin_settings();
|
||
$notice = $_GET['sk_notice'] ?? '';
|
||
$active_tab = isset($_GET['sk_tab']) ? sanitize_text_field($_GET['sk_tab']) : 'auftraege';
|
||
$base_url = admin_url('admin.php?page=skrift-auftraege');
|
||
?>
|
||
<style>
|
||
.sk-job-wrap { max-width: 1200px; }
|
||
.sk-tab-nav { display:flex; gap:0; margin:0 0 20px; border-bottom:1px solid #ccd0d4; }
|
||
.sk-tab-nav a { padding:8px 16px; text-decoration:none; color:#23282d; border:1px solid transparent; border-bottom:none; margin-bottom:-1px; }
|
||
.sk-tab-nav a.active { background:#fff; border-color:#ccd0d4; color:#000; font-weight:600; }
|
||
.sk-job-section { background:#fff; padding:20px; margin:0 0 20px; border:1px solid #ccd0d4; box-shadow:0 1px 1px rgba(0,0,0,.04); }
|
||
.sk-job-section h2 { margin-top:0; padding-bottom:10px; border-bottom:1px solid #eee; }
|
||
.sk-job-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px 24px; }
|
||
.sk-field-row { margin-bottom:0; }
|
||
.sk-field-row label { display:block; font-weight:600; margin-bottom:4px; font-size:13px; }
|
||
.sk-field-row input:not([type="checkbox"]), .sk-field-row select, .sk-field-row textarea { width:100%; box-sizing:border-box; }
|
||
.sk-field-row--full { grid-column:1/-1; }
|
||
.sk-job-table { width:100%; border-collapse:collapse; margin-top:10px; font-size:13px; }
|
||
.sk-job-table th, .sk-job-table td { padding:8px 10px; text-align:left; border-bottom:1px solid #ddd; vertical-align:top; }
|
||
.sk-job-table th { background:#f0f0f1; font-weight:600; white-space:nowrap; }
|
||
.sk-job-table tr:hover { background:#f9f9f9; }
|
||
.sk-badge { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; font-weight:600; white-space:nowrap; }
|
||
.sk-badge-waiting { background:#fff3cd; color:#856404; }
|
||
.sk-badge-waiting-routine { background:#ffe0b2; color:#7c4a00; }
|
||
.sk-badge-text-proposal { background:#e8d5f5; color:#5b21b6; }
|
||
.sk-badge-submitted { background:#d1ecf1; color:#0c5460; }
|
||
.sk-badge-error { background:#f8d7da; color:#721c24; }
|
||
.sk-badge-printing { background:#d4edda; color:#155724; }
|
||
.sk-badge-shipped { background:#cce5ff; color:#004085; }
|
||
.sk-copy-btn { cursor:pointer; background:none; border:none; color:#0073aa; font-size:12px; padding:0 4px; }
|
||
.sk-copy-btn:hover { text-decoration:underline; }
|
||
.sk-status-form { display:flex; flex-direction:column; gap:4px; min-width:160px; }
|
||
.sk-status-row { display:flex; gap:4px; align-items:center; }
|
||
.sk-mail-template-wrap { border:1px solid #ddd; padding:16px; margin-bottom:16px; background:#fafafa; }
|
||
.sk-mail-template-wrap h3 { margin:0 0 12px; font-size:14px; }
|
||
.sk-mail-template-wrap .sk-mt-badge { font-size:11px; padding:2px 8px; border-radius:12px; font-weight:600; margin-left:8px; }
|
||
.sk-email-cell { font-size:12px; color:#555; max-width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||
.sk-email-cell.empty { color:#bbb; font-style:italic; }
|
||
.sk-sub-status-form { display:flex; gap:4px; align-items:center; }
|
||
/* ─ Todo-Hervorhebung ─ */
|
||
tr.sk-todo { background:#f0fdf4 !important; }
|
||
tr.sk-todo:hover { background:#dcfce7 !important; }
|
||
/* ─ Aufklapp-Button ─ */
|
||
.sk-expand-btn { background:none; border:none; cursor:pointer; font-size:12px; padding:0 3px 0 0; color:#888; line-height:1; transition:transform .15s; display:inline-block; vertical-align:middle; }
|
||
.sk-expand-btn.is-open { transform:rotate(90deg); }
|
||
/* ─ Aufgeklappte Kind-Zeilen ─ */
|
||
.sk-child-row td { padding:3px 8px 3px 38px !important; font-size:11px; color:#555; background:#f8f9fa; border-bottom:none !important; }
|
||
.sk-child-row.sk-child-last td { border-bottom:2px solid #ddd !important; }
|
||
/* ─ Standalone Sub-Aufträge ─ */
|
||
.sk-standalone-sub td { font-size:12px; background:#f7f7f7; }
|
||
.sk-standalone-sub:hover td { background:#f0f0f0 !important; }
|
||
/* ─ Bulk-Action Leiste ─ */
|
||
.sk-bulk-bar { display:flex; align-items:center; gap:8px; margin-bottom:14px; padding:8px 12px; background:#f0f0f1; border:1px solid #ddd; border-radius:4px; }
|
||
/* ─ Status-Gruppen ─ */
|
||
.sk-job-group { border:1px solid #ddd; margin-bottom:10px; border-radius:4px; overflow:hidden; }
|
||
.sk-group-summary { padding:10px 16px; cursor:pointer; display:flex; align-items:center; gap:10px; background:#f8f9fa; font-weight:600; font-size:14px; list-style:none; user-select:none; }
|
||
.sk-group-summary::-webkit-details-marker { display:none; }
|
||
details[open] > .sk-group-summary { border-bottom:1px solid #ddd; }
|
||
.sk-group-count { background:#6c757d; color:#fff; border-radius:10px; padding:1px 8px; font-size:11px; font-weight:700; margin-left:auto; }
|
||
/* ─ Papierkorb-Icon ─ */
|
||
.sk-trash-btn { padding:3px 5px !important; min-height:0 !important; background:none !important; border:none !important; box-shadow:none !important; color:#999 !important; }
|
||
.sk-trash-btn:hover { color:#dc3232 !important; background:none !important; }
|
||
</style>
|
||
|
||
<script>
|
||
function skiftShowReason(sel) {
|
||
var jobId = sel.dataset.jobId;
|
||
var extraRow = document.getElementById('sk-extra-' + jobId);
|
||
var errorDiv = document.getElementById('sk-extra-error-' + jobId);
|
||
var proposalDiv = document.getElementById('sk-extra-proposal-' + jobId);
|
||
var showError = sel.value === 'error';
|
||
var showProposal = sel.value === 'text_proposal';
|
||
var showExtra = showError || showProposal;
|
||
if (extraRow) extraRow.style.display = showExtra ? '' : 'none';
|
||
if (errorDiv) errorDiv.style.display = showError ? '' : 'none';
|
||
if (proposalDiv) proposalDiv.style.display = showProposal ? '' : 'none';
|
||
}
|
||
function skiftShowSubReason(sel, subId) {
|
||
// Kein Erweiterungs-Row bei Sub-Aufträgen notwendig – Fehlergrund wird nach Speichern angezeigt
|
||
}
|
||
function skiftCreateStatusChange(sel) {
|
||
var proposalWrap = document.getElementById('sk-create-proposal-wrap');
|
||
var errorWrap = document.getElementById('sk-create-error-wrap');
|
||
if (proposalWrap) proposalWrap.style.display = sel.value === 'text_proposal' ? '' : 'none';
|
||
if (errorWrap) errorWrap.style.display = sel.value === 'error' ? '' : 'none';
|
||
}
|
||
|
||
// ─ Aufklappen / Todo-Hervorhebung ─────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
var SEEN_KEY = 'sk_seen_jobs';
|
||
function getSeen() {
|
||
try { return JSON.parse(localStorage.getItem(SEEN_KEY) || '[]'); } catch(e) { return []; }
|
||
}
|
||
function markSeen(id) {
|
||
var seen = getSeen();
|
||
if (seen.indexOf(id) === -1) { seen.push(id); localStorage.setItem(SEEN_KEY, JSON.stringify(seen)); }
|
||
}
|
||
|
||
var seen = getSeen();
|
||
|
||
// Todo-Hervorhebung: grün wenn noch nicht gesehen
|
||
document.querySelectorAll('[data-sk-todo="1"]').forEach(function(row) {
|
||
var id = row.dataset.skTodoId;
|
||
if (seen.indexOf(id) === -1) {
|
||
row.classList.add('sk-todo');
|
||
}
|
||
row.addEventListener('click', function(e) {
|
||
if (e.target.closest('form, input, select, button, a')) return;
|
||
markSeen(id);
|
||
row.classList.remove('sk-todo');
|
||
});
|
||
});
|
||
|
||
// Expand-Buttons für Unteraufträge
|
||
document.querySelectorAll('.sk-expand-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
var target = document.getElementById(btn.dataset.target);
|
||
if (!target) return;
|
||
var open = target.style.display !== 'none';
|
||
target.style.display = open ? 'none' : '';
|
||
btn.classList.toggle('is-open', !open);
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<div class="wrap sk-job-wrap">
|
||
<h1>Skrift Kundenauftraege</h1>
|
||
|
||
<?php if ($notice === 'created') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Auftrag erfolgreich angelegt.</p></div>
|
||
<?php elseif ($notice === 'deleted') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Auftrag geloscht.</p></div>
|
||
<?php elseif ($notice === 'trashed') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Auftrag in den Papierkorb verschoben.</p></div>
|
||
<?php elseif ($notice === 'restored') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Auftrag wiederhergestellt.</p></div>
|
||
<?php elseif ($notice === 'purged') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Auftrag endgültig gelöscht.</p></div>
|
||
<?php elseif ($notice === 'bulk_done') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Massenoperation abgeschlossen.</p></div>
|
||
<?php elseif ($notice === 'sub_deleted') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Unterauftrag gelöscht.</p></div>
|
||
<?php elseif ($notice === 'duplicate') : ?>
|
||
<div class="notice notice-error is-dismissible"><p>Diese Auftragsnummer existiert bereits.</p></div>
|
||
<?php elseif ($notice === 'settings_saved') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Einstellungen gespeichert.</p></div>
|
||
<?php elseif ($notice === 'status_updated') : ?>
|
||
<div class="notice notice-success is-dismissible"><p>Status aktualisiert.</p></div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Tabs -->
|
||
<div class="sk-tab-nav">
|
||
<a href="<?php echo esc_url(add_query_arg('sk_tab', 'auftraege', $base_url)); ?>"
|
||
class="<?php echo $active_tab === 'auftraege' ? 'active' : ''; ?>">Auftraege (<?php echo count($jobs); ?>)</a>
|
||
<a href="<?php echo esc_url(add_query_arg('sk_tab', 'einstellungen', $base_url)); ?>"
|
||
class="<?php echo $active_tab === 'einstellungen' ? 'active' : ''; ?>">Einstellungen</a>
|
||
</div>
|
||
|
||
<?php if ($active_tab === 'auftraege') : ?>
|
||
|
||
<!-- Neuen Auftrag anlegen -->
|
||
<div class="sk-job-section">
|
||
<h2>Neuen Auftrag anlegen</h2>
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||
<input type="hidden" name="action" value="skrift_job_create">
|
||
<?php wp_nonce_field('skrift_job_create'); ?>
|
||
<div class="sk-job-grid">
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_id">Auftragsnummer <span style="color:red">*</span></label>
|
||
<input type="text" id="job_id" name="job_id" placeholder="z.B. AUF-2026-001" required>
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_customer_email">E-Mail des Kunden</label>
|
||
<input type="email" id="job_customer_email" name="job_customer_email" placeholder="kunde@beispiel.de">
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_produkt">Produktart <span style="color:red">*</span></label>
|
||
<select id="job_produkt" name="job_produkt" required>
|
||
<option value="">– bitte wahlen –</option>
|
||
<option value="Handgeschriebene Briefe">Handgeschriebene Briefe</option>
|
||
<option value="Handgeschriebene Postkarten">Handgeschriebene Postkarten</option>
|
||
<option value="Gedruckte Briefe">Gedruckte Briefe</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_format">Format</label>
|
||
<select id="job_format" name="job_format">
|
||
<option value="">– bitte wahlen –</option>
|
||
<?php foreach (self::FORMATS as $val => $lbl) : ?>
|
||
<option value="<?php echo esc_attr($val); ?>"><?php echo esc_html($lbl); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_menge">Menge</label>
|
||
<input type="number" id="job_menge" name="job_menge" min="1" value="1">
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_kuvert">Kuvertbeschriftung</label>
|
||
<select id="job_kuvert" name="job_kuvert">
|
||
<option value="keine">Keine</option>
|
||
<option value="adresse">Adresse</option>
|
||
<option value="individueller_text">Individueller Text</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_textservice">Textservice</label>
|
||
<select id="job_textservice" name="job_textservice">
|
||
<option value="0">Nein – Kunde liefert den Text</option>
|
||
<option value="1">Ja – Skrift schreibt den Text</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="sk-field-row">
|
||
<label for="job_signatur">Signaturfelder</label>
|
||
<select id="job_signatur" name="job_signatur">
|
||
<option value="0">Keine Signatur</option>
|
||
<option value="1">1 Signatur</option>
|
||
<option value="2">2 Signaturen</option>
|
||
<option value="3">3 Signaturen</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="sk-field-row sk-field-row--full">
|
||
<label for="job_initial_status">Anfangsstatus</label>
|
||
<select id="job_initial_status" name="job_initial_status" onchange="skiftCreateStatusChange(this)" style="max-width:calc(50% - 12px);">
|
||
<?php foreach (self::STATUSES as $skey => $slbl) : ?>
|
||
<option value="<?php echo esc_attr($skey); ?>" <?php selected('waiting', $skey); ?>><?php echo esc_html($slbl); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
|
||
<div id="sk-create-proposal-wrap" class="sk-field-row sk-field-row--full" style="display:none;">
|
||
<label for="job_initial_proposed_text">Textvorschlag</label>
|
||
<textarea id="job_initial_proposed_text" name="job_initial_proposed_text" rows="5"
|
||
placeholder="Brieftext-Vorschlag für den Kunden…"
|
||
style="font-family:monospace;"></textarea>
|
||
</div>
|
||
|
||
<div id="sk-create-error-wrap" class="sk-field-row sk-field-row--full" style="display:none;">
|
||
<label for="job_initial_reason">Fehlergrund</label>
|
||
<textarea id="job_initial_reason" name="job_initial_reason" rows="3"
|
||
placeholder="Beschreiben Sie den Fehler für den Kunden…"></textarea>
|
||
</div>
|
||
|
||
</div>
|
||
<div style="margin-top:20px;">
|
||
<?php submit_button('Auftrag anlegen', 'primary', 'submit', false); ?>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Bestehende Auftraege -->
|
||
<div class="sk-job-section">
|
||
<h2>Bestehende Auftraege</h2>
|
||
|
||
<?php
|
||
$active_jobs = array_filter($jobs, fn($j) => 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'];
|
||
?>
|
||
|
||
<!-- Bulk-Action Form (hält nur Checkboxen + Leiste) -->
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" id="sk-bulk-form">
|
||
<input type="hidden" name="action" value="skrift_job_bulk">
|
||
<?php wp_nonce_field('skrift_job_bulk'); ?>
|
||
<div class="sk-bulk-bar">
|
||
<label style="display:flex;align-items:center;gap:5px;cursor:pointer;white-space:nowrap;">
|
||
<input type="checkbox" id="sk-select-all"
|
||
onchange="document.querySelectorAll('.sk-job-cb').forEach(cb=>cb.checked=this.checked)">
|
||
Alle
|
||
</label>
|
||
<select name="bulk_action" style="font-size:13px;">
|
||
<option value="">Aktion wählen</option>
|
||
<option value="trash">In Papierkorb verschieben</option>
|
||
</select>
|
||
<button type="submit" class="button button-small">Anwenden</button>
|
||
<span style="margin-left:auto;font-size:12px;color:#666;"><?php echo count($active_jobs); ?> Aufträge aktiv</span>
|
||
</div>
|
||
</form>
|
||
|
||
<?php if (empty($active_jobs) && empty($trashed_jobs)) : ?>
|
||
<p style="color:#666;">Noch keine Auftraege angelegt.</p>
|
||
<?php else : ?>
|
||
|
||
<?php
|
||
// Sub-Aufträge aller aktiven Jobs flach sammeln
|
||
$all_subs = [];
|
||
foreach ($active_jobs as $pjob) {
|
||
foreach ($pjob['sub_orders'] ?? [] as $sub) {
|
||
$all_subs[] = ['sub' => $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);
|
||
?>
|
||
<details <?php echo ($gcount > 0 && $group['open']) ? 'open' : ''; ?> class="sk-job-group" id="sk-group-<?php echo esc_attr($group_key); ?>">
|
||
<summary class="sk-group-summary">
|
||
<?php echo esc_html($group['label']); ?>
|
||
<span class="sk-group-count"><?php echo $gcount; ?></span>
|
||
</summary>
|
||
|
||
<?php if ($gcount === 0) : ?>
|
||
<p style="color:#999;padding:10px 16px;margin:0;font-size:13px;">Keine Aufträge in dieser Kategorie.</p>
|
||
<?php else : ?>
|
||
<table class="sk-job-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:28px;"></th>
|
||
<th>Auftragsnummer</th>
|
||
<th>Produkt / Format</th>
|
||
<th>Menge</th>
|
||
<th>Kuvert</th>
|
||
<th>Sig.</th>
|
||
<th>E-Mail Kunde</th>
|
||
<th>Angelegt</th>
|
||
<th>Status</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($group_jobs as $job) :
|
||
$status = $job['status'] ?? 'waiting';
|
||
$cust_email = $job['customer_email'] ?? '';
|
||
$sub_orders = $job['sub_orders'] ?? [];
|
||
$has_subs = !empty($sub_orders);
|
||
$is_todo = in_array($status, ['submitted']) || !empty($job['shipping_approved_at']);
|
||
?>
|
||
<tr data-sk-todo-id="<?php echo esc_attr($job['id']); ?>"
|
||
<?php echo $is_todo ? 'data-sk-todo="1"' : ''; ?>>
|
||
<td><input type="checkbox" form="sk-bulk-form" class="sk-job-cb" name="job_ids[]" value="<?php echo esc_attr($job['id']); ?>"></td>
|
||
<td>
|
||
<?php if ($has_subs) : ?>
|
||
<button class="sk-expand-btn" data-target="sk-ch-<?php echo esc_attr($job['id']); ?>" title="Unteraufträge anzeigen">▶</button>
|
||
<?php endif; ?>
|
||
<strong><?php echo esc_html($job['id']); ?></strong>
|
||
<button class="sk-copy-btn"
|
||
onclick="navigator.clipboard.writeText('<?php echo esc_js($job['id']); ?>').then(()=>this.textContent='ok').catch(()=>{})"
|
||
title="Kopieren">[kopieren]</button>
|
||
</td>
|
||
<td>
|
||
<?php echo esc_html($job['produkt']); ?>
|
||
<?php if (!empty($job['format'])) : ?>
|
||
<br><small style="color:#666;"><?php echo esc_html($job['format']); ?></small>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php echo esc_html($job['menge']); ?></td>
|
||
<td><?php echo esc_html($kuvert_labels[$job['kuvert']] ?? $job['kuvert']); ?></td>
|
||
<td><?php echo $job['signatur'] > 0 ? esc_html($job['signatur']) : '–'; ?></td>
|
||
<td>
|
||
<?php if ($cust_email) : ?>
|
||
<span class="sk-email-cell" title="<?php echo esc_attr($cust_email); ?>"><?php echo esc_html($cust_email); ?></span>
|
||
<?php else : ?>
|
||
<span class="sk-email-cell empty">–</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td style="white-space:nowrap;"><?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($job['created_at']))); ?></td>
|
||
<td>
|
||
<form id="sk-sf-<?php echo esc_attr($job['id']); ?>"
|
||
method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
|
||
class="sk-status-form">
|
||
<input type="hidden" name="action" value="skrift_job_update_status">
|
||
<input type="hidden" name="job_id" value="<?php echo esc_attr($job['id']); ?>">
|
||
<?php wp_nonce_field('skrift_job_update_status'); ?>
|
||
<div class="sk-status-row">
|
||
<select name="new_status"
|
||
onchange="skiftShowReason(this)"
|
||
data-job-id="<?php echo esc_attr($job['id']); ?>"
|
||
style="font-size:12px;max-width:150px;">
|
||
<?php foreach (self::STATUSES as $skey => $slbl) : ?>
|
||
<option value="<?php echo esc_attr($skey); ?>" <?php selected($status, $skey); ?>><?php echo esc_html($slbl); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="button button-primary">Anwenden</button>
|
||
</div>
|
||
</form>
|
||
<?php
|
||
if (!empty($job['shipping_approved_at'])) : ?>
|
||
<div style="margin-top:4px;font-size:11px;color:#666;">
|
||
Versandfreigabe am <?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($job['shipping_approved_at']))); ?>
|
||
</div>
|
||
<?php else :
|
||
$first_sub_date = null;
|
||
foreach ($sub_orders as $sub) {
|
||
if (!empty($sub['created_at'])) { $first_sub_date = $sub['created_at']; break; }
|
||
}
|
||
if ($first_sub_date) : ?>
|
||
<div style="margin-top:4px;font-size:11px;color:#666;">
|
||
Übermittelt am <?php echo esc_html(date_i18n('d.m.Y H:i', strtotime($first_sub_date))); ?>
|
||
</div>
|
||
<?php endif;
|
||
endif; ?>
|
||
</td>
|
||
<td>
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
|
||
onsubmit="return confirm('Auftrag <?php echo esc_js($job['id']); ?> in den Papierkorb?')">
|
||
<input type="hidden" name="action" value="skrift_job_trash">
|
||
<input type="hidden" name="job_id" value="<?php echo esc_attr($job['id']); ?>">
|
||
<?php wp_nonce_field('skrift_job_trash'); ?>
|
||
<button type="submit" class="button sk-trash-btn" title="In Papierkorb">
|
||
<span class="dashicons dashicons-trash" style="font-size:16px;width:16px;height:16px;line-height:1.3;"></span>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<!-- Aufklapp-Zeile fur Fehlergrund / Textvorschlag -->
|
||
<tr id="sk-extra-<?php echo esc_attr($job['id']); ?>"
|
||
style="<?php echo ($status === 'error' || $status === 'text_proposal') ? '' : 'display:none;'; ?>">
|
||
<td colspan="10" style="padding:0 10px 14px;background:#fafafa;border-top:none;">
|
||
<div id="sk-extra-error-<?php echo esc_attr($job['id']); ?>"
|
||
style="<?php echo $status === 'error' ? '' : 'display:none;'; ?>">
|
||
<label style="font-size:12px;font-weight:600;margin:8px 0 4px;display:block;color:#721c24;">Fehlergrund fur den Kunden:</label>
|
||
<textarea name="status_reason"
|
||
form="sk-sf-<?php echo esc_attr($job['id']); ?>"
|
||
rows="3"
|
||
placeholder="Beschreiben Sie den Fehler…"
|
||
style="width:100%;font-size:13px;padding:8px;box-sizing:border-box;resize:vertical;border:1px solid #f5c6cb;border-radius:3px;background:#fff;"><?php echo esc_textarea($job['status_reason'] ?? ''); ?></textarea>
|
||
</div>
|
||
<div id="sk-extra-proposal-<?php echo esc_attr($job['id']); ?>"
|
||
style="<?php echo $status === 'text_proposal' ? '' : 'display:none;'; ?>">
|
||
<label style="font-size:12px;font-weight:600;margin:8px 0 4px;display:block;color:#5b21b6;">Textvorschlag fur den Kunden:</label>
|
||
<textarea name="proposed_text"
|
||
form="sk-sf-<?php echo esc_attr($job['id']); ?>"
|
||
rows="6"
|
||
placeholder="Brieftext-Vorschlag fur den Kunden eingeben…"
|
||
style="width:100%;font-size:13px;padding:8px;box-sizing:border-box;resize:vertical;font-family:monospace;border:1px solid #d8b4fe;border-radius:3px;background:#fff;"><?php echo esc_textarea($job['proposed_text'] ?? ''); ?></textarea>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php if ($has_subs) : ?>
|
||
<tr class="sk-child-rows" id="sk-ch-<?php echo esc_attr($job['id']); ?>" style="display:none;">
|
||
<td colspan="10" style="padding:0;background:#f4f6f8;border-top:none;">
|
||
<?php
|
||
$sub_status_labels = self::SUB_STATUSES;
|
||
foreach ($sub_orders as $idx => $sub) :
|
||
$slbl = $sub_status_labels[$sub['status'] ?? 'waiting'] ?? ($sub['status'] ?? '');
|
||
$sdate = !empty($sub['created_at']) ? date_i18n('d.m.Y H:i', strtotime($sub['created_at'])) : '';
|
||
?>
|
||
<div style="display:flex;align-items:center;gap:16px;padding:4px 12px 4px 38px;font-size:12px;color:#444;<?php echo $idx > 0 ? 'border-top:1px solid #e8e8e8;' : ''; ?>">
|
||
<span>↳ <strong><?php echo esc_html($sub['id']); ?></strong></span>
|
||
<span style="color:#666;"><?php echo esc_html($slbl); ?></span>
|
||
<?php if ($sdate) : ?><span style="color:#999;"><?php echo esc_html($sdate); ?></span><?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
|
||
<?php foreach ($group_subs as $entry) :
|
||
$sub = $entry['sub'];
|
||
$parent_id = $entry['parent_id'];
|
||
$pjob = $entry['parent_job'];
|
||
$sub_status = $sub['status'] ?? 'waiting';
|
||
$is_sub_todo = in_array($sub_status, ['submitted']);
|
||
$sub_email = $pjob['customer_email'] ?? '';
|
||
$sub_form_id = 'sk-sf-sub-' . esc_attr($sub['id']);
|
||
?>
|
||
<tr data-sk-todo-id="<?php echo esc_attr($sub['id']); ?>"
|
||
<?php echo $is_sub_todo ? 'data-sk-todo="1"' : ''; ?>>
|
||
<td><input type="checkbox" form="sk-bulk-form" class="sk-job-cb" name="sub_ids[]" value="<?php echo esc_attr($parent_id . ':' . $sub['id']); ?>"></td>
|
||
<td>
|
||
<strong><?php echo esc_html($sub['id']); ?></strong>
|
||
<button class="sk-copy-btn"
|
||
onclick="navigator.clipboard.writeText('<?php echo esc_js($sub['id']); ?>').then(()=>this.textContent='ok').catch(()=>{})"
|
||
title="Kopieren">[kopieren]</button>
|
||
<br><small style="color:#888;font-size:11px;">↳ <?php echo esc_html($parent_id); ?></small>
|
||
</td>
|
||
<td>
|
||
<?php echo esc_html($pjob['produkt'] ?? '–'); ?>
|
||
<?php if (!empty($pjob['format'])) : ?>
|
||
<br><small style="color:#666;"><?php echo esc_html($pjob['format']); ?></small>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php echo esc_html($sub['quantity'] ?? '–'); ?></td>
|
||
<td><?php echo esc_html($kuvert_labels[$pjob['kuvert'] ?? ''] ?? ($pjob['kuvert'] ?? '–')); ?></td>
|
||
<td><?php echo ($pjob['signatur'] ?? 0) > 0 ? esc_html($pjob['signatur']) : '–'; ?></td>
|
||
<td>
|
||
<?php if ($sub_email) : ?>
|
||
<span class="sk-email-cell" title="<?php echo esc_attr($sub_email); ?>"><?php echo esc_html($sub_email); ?></span>
|
||
<?php else : ?>
|
||
<span class="sk-email-cell empty">–</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td style="white-space:nowrap;">
|
||
<?php echo !empty($sub['created_at']) ? esc_html(date_i18n('d.m.Y H:i', strtotime($sub['created_at']))) : '–'; ?>
|
||
</td>
|
||
<td>
|
||
<form id="<?php echo $sub_form_id; ?>"
|
||
method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
|
||
class="sk-status-form">
|
||
<input type="hidden" name="action" value="skrift_sub_order_update_status">
|
||
<input type="hidden" name="job_id" value="<?php echo esc_attr($parent_id); ?>">
|
||
<input type="hidden" name="sub_id" value="<?php echo esc_attr($sub['id']); ?>">
|
||
<?php wp_nonce_field('skrift_sub_order_update_status'); ?>
|
||
<div class="sk-status-row">
|
||
<select name="new_status"
|
||
onchange="skiftShowSubReason(this,'<?php echo esc_js($sub['id']); ?>')"
|
||
style="font-size:12px;max-width:150px;">
|
||
<?php foreach (self::SUB_STATUSES as $skey => $slbl) : ?>
|
||
<option value="<?php echo esc_attr($skey); ?>" <?php selected($sub_status, $skey); ?>><?php echo esc_html($slbl); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit" class="button button-primary">Anwenden</button>
|
||
</div>
|
||
</form>
|
||
</td>
|
||
<td>
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
|
||
onsubmit="return confirm('Unterauftrag <?php echo esc_js($sub['id']); ?> löschen?')">
|
||
<input type="hidden" name="action" value="skrift_sub_order_delete">
|
||
<input type="hidden" name="job_id" value="<?php echo esc_attr($parent_id); ?>">
|
||
<input type="hidden" name="sub_id" value="<?php echo esc_attr($sub['id']); ?>">
|
||
<?php wp_nonce_field('skrift_sub_order_delete'); ?>
|
||
<button type="submit" class="button sk-trash-btn" title="Löschen">
|
||
<span class="dashicons dashicons-trash" style="font-size:16px;width:16px;height:16px;line-height:1.3;"></span>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php if ($sub_status === 'error' && !empty($sub['status_reason'])) : ?>
|
||
<tr style="background:#fff5f5;">
|
||
<td colspan="10" style="padding:4px 10px 10px 38px;">
|
||
<label style="font-size:12px;font-weight:600;color:#721c24;">Fehlergrund:</label>
|
||
<div style="font-size:12px;color:#721c24;"><?php echo esc_html($sub['status_reason']); ?></div>
|
||
</td>
|
||
</tr>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</details>
|
||
<?php endforeach; ?>
|
||
|
||
<!-- Papierkorb -->
|
||
<?php if (!empty($trashed_jobs)) : ?>
|
||
<details id="sk-trash-group" style="margin-top:16px;border:1px solid #ddd;border-radius:4px;overflow:hidden;">
|
||
<summary class="sk-group-summary" style="background:#fff5f5;">
|
||
<span class="dashicons dashicons-trash" style="color:#dc2626;font-size:16px;width:16px;height:16px;line-height:1.4;margin-right:2px;"></span>
|
||
Papierkorb
|
||
<span class="sk-group-count" style="background:#dc2626;"><?php echo count($trashed_jobs); ?></span>
|
||
</summary>
|
||
<table class="sk-job-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Auftragsnummer</th>
|
||
<th>Produkt</th>
|
||
<th>Gelöscht am</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($trashed_jobs as $job) : ?>
|
||
<tr>
|
||
<td><strong><?php echo esc_html($job['id']); ?></strong></td>
|
||
<td><?php echo esc_html($job['produkt']); ?></td>
|
||
<td><?php echo esc_html(!empty($job['deleted_at']) ? date_i18n('d.m.Y H:i', strtotime($job['deleted_at'])) : '–'); ?></td>
|
||
<td style="display:flex;gap:6px;flex-wrap:wrap;">
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||
<input type="hidden" name="action" value="skrift_job_restore">
|
||
<input type="hidden" name="job_id" value="<?php echo esc_attr($job['id']); ?>">
|
||
<?php wp_nonce_field('skrift_job_restore'); ?>
|
||
<button type="submit" class="button button-small">↩ Wiederherstellen</button>
|
||
</form>
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"
|
||
onsubmit="return confirm('Auftrag <?php echo esc_js($job['id']); ?> endgültig löschen?')">
|
||
<input type="hidden" name="action" value="skrift_job_purge">
|
||
<input type="hidden" name="job_id" value="<?php echo esc_attr($job['id']); ?>">
|
||
<?php wp_nonce_field('skrift_job_purge'); ?>
|
||
<button type="submit" class="button button-small button-link-delete">Endgültig löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</details>
|
||
<?php endif; ?>
|
||
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php elseif ($active_tab === 'einstellungen') : ?>
|
||
|
||
<!-- Globale Einstellungen -->
|
||
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
||
<input type="hidden" name="action" value="skrift_job_settings">
|
||
<?php wp_nonce_field('skrift_job_settings'); ?>
|
||
|
||
<div class="sk-job-section">
|
||
<h2>Globale Einstellungen</h2>
|
||
<div class="sk-job-grid">
|
||
<div class="sk-field-row">
|
||
<label for="job_global_notification_email">Benachrichtigungs-E-Mail (Admin)</label>
|
||
<input type="email" id="job_global_notification_email" name="job_global_notification_email"
|
||
value="<?php echo esc_attr($global['notification_email']); ?>"
|
||
placeholder="<?php echo esc_attr(get_option('admin_email')); ?>">
|
||
<small>Eingehende Auftraege werden hier gemeldet. Leer = Admin-E-Mail.</small>
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="job_global_portal_url">Kunden-Portal-URL</label>
|
||
<input type="url" id="job_global_portal_url" name="job_global_portal_url"
|
||
value="<?php echo esc_attr($global['portal_url']); ?>"
|
||
placeholder="https://example.com/auftrag/">
|
||
<small>Seite mit dem Shortcode [skrift_kundenauftrag] – wird in E-Mails verlinkt.</small>
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="job_global_redirect_url">Weiterleitungs-URL nach Abschluss</label>
|
||
<input type="url" id="job_global_redirect_url" name="job_global_redirect_url"
|
||
value="<?php echo esc_attr($global['redirect_url']); ?>"
|
||
placeholder="https://example.com/danke">
|
||
<small>Kunde wird nach Datenubergabe hierhin weitergeleitet (mit ?orderNumber=…).</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SMTP-Einstellungen -->
|
||
<div class="sk-job-section">
|
||
<h2>SMTP-Einstellungen</h2>
|
||
<p style="color:#666;margin-top:-5px;">
|
||
Ausgehender Mailversand fur Kunden-E-Mails. Wenn kein Host angegeben ist, verwendet WordPress den Server-Standard (oft unzuverlassig).
|
||
</p>
|
||
<div class="sk-job-grid">
|
||
<div class="sk-field-row">
|
||
<label for="smtp_host">Postausgangsserver (Host)</label>
|
||
<input type="text" id="smtp_host" name="smtp[host]"
|
||
value="<?php echo esc_attr($global['smtp']['host'] ?? ''); ?>"
|
||
placeholder="z.B. mxe977.netcup.net">
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="smtp_port">Port</label>
|
||
<input type="number" id="smtp_port" name="smtp[port]"
|
||
value="<?php echo esc_attr($global['smtp']['port'] ?? 465); ?>"
|
||
min="1" max="65535" style="max-width:120px;">
|
||
<small>465 = SSL/TLS · 587 = STARTTLS</small>
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="smtp_username">Benutzername</label>
|
||
<input type="text" id="smtp_username" name="smtp[username]"
|
||
value="<?php echo esc_attr($global['smtp']['username'] ?? ''); ?>"
|
||
placeholder="noreply@example.com">
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="smtp_password">Passwort</label>
|
||
<input type="password" id="smtp_password" name="smtp[password]"
|
||
value="<?php echo esc_attr($global['smtp']['password'] ?? ''); ?>">
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="smtp_from_email">Absender-E-Mail (optional)</label>
|
||
<input type="email" id="smtp_from_email" name="smtp[from_email]"
|
||
value="<?php echo esc_attr($global['smtp']['from_email'] ?? ''); ?>"
|
||
placeholder="noreply@example.com">
|
||
<small>Leer = SMTP-Benutzername wird als Absenderadresse genutzt.</small>
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label for="smtp_from_name">Absender-Name</label>
|
||
<input type="text" id="smtp_from_name" name="smtp[from_name]"
|
||
value="<?php echo esc_attr($global['smtp']['from_name'] ?? 'Skrift Handschriftmarketing'); ?>">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mail-Templates -->
|
||
<div class="sk-job-section">
|
||
<h2>E-Mail-Vorlagen an Kunden</h2>
|
||
<p style="color:#666;margin-top:-5px;">
|
||
Verfugbare Platzhalter: <code>{{order_id}}</code>, <code>{{link}}</code>,
|
||
<code>{{reason}}</code> (nur "Fehlerhaft"), <code>{{approve_link_button}}</code> (nur "Druck abgeschlossen").<br>
|
||
Farbige Hervorhebung: <code>[rot]Ihr Text hier[/rot]</code> → erscheint rot & fett im E-Mail.
|
||
</p>
|
||
|
||
<?php
|
||
$templates = $global['mail_templates'] ?? [];
|
||
$tpl_labels = [
|
||
'waiting' => ['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' => ''];
|
||
?>
|
||
<div class="sk-mail-template-wrap">
|
||
<h3>
|
||
<span class="sk-badge <?php echo esc_attr($tinfo['badge']); ?>"><?php echo esc_html($tinfo['label']); ?></span>
|
||
<small style="font-weight:normal;color:#666;margin-left:8px;"><?php echo esc_html($tinfo['note']); ?></small>
|
||
</h3>
|
||
<div class="sk-field-row">
|
||
<label>Betreff</label>
|
||
<input type="text" name="mail_templates[<?php echo esc_attr($tkey); ?>][subject]"
|
||
value="<?php echo esc_attr($tpl['subject']); ?>">
|
||
</div>
|
||
<div class="sk-field-row">
|
||
<label>Text</label>
|
||
<textarea name="mail_templates[<?php echo esc_attr($tkey); ?>][body]"
|
||
rows="6" style="font-family:monospace;"><?php echo esc_textarea($tpl['body']); ?></textarea>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<?php submit_button('Einstellungen speichern', 'primary', 'submit', false); ?>
|
||
</form>
|
||
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
// ─── Admin: Auftrag erstellen ────────────────────────────────────────────────
|
||
|
||
public function handle_create_job(): void {
|
||
if (!current_user_can('manage_options')) wp_die('Keine Berechtigung.');
|
||
check_admin_referer('skrift_job_create');
|
||
|
||
$job_id = sanitize_text_field($_POST['job_id'] ?? '');
|
||
$produkt = sanitize_text_field($_POST['job_produkt'] ?? '');
|
||
$format = sanitize_text_field($_POST['job_format'] ?? '');
|
||
$menge = max(1, (int) ($_POST['job_menge'] ?? 1));
|
||
$kuvert = sanitize_text_field($_POST['job_kuvert'] ?? 'keine');
|
||
$textservice = ($_POST['job_textservice'] ?? '0') === '1';
|
||
$signatur = max(0, min(3, (int) ($_POST['job_signatur'] ?? 0)));
|
||
$customer_email = sanitize_email($_POST['job_customer_email'] ?? '');
|
||
|
||
$initial_status = sanitize_text_field($_POST['job_initial_status'] ?? 'waiting');
|
||
$initial_proposed_text = sanitize_textarea_field($_POST['job_initial_proposed_text'] ?? '');
|
||
$initial_reason = sanitize_textarea_field($_POST['job_initial_reason'] ?? '');
|
||
if (!array_key_exists($initial_status, self::STATUSES)) {
|
||
$initial_status = 'waiting';
|
||
}
|
||
|
||
$jobs = self::get_jobs();
|
||
|
||
if (isset($jobs[$job_id])) {
|
||
wp_redirect(add_query_arg(['sk_notice' => '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 = '<a href="' . esc_url($approve_link) . '" '
|
||
. 'style="display:inline-block;padding:12px 28px;background:#1e3a6a;color:#ffffff;text-decoration:none;'
|
||
. 'border-radius:4px;font-weight:600;font-size:14px;margin:8px 0;">'
|
||
. 'Versandfreigabe erteilen</a>';
|
||
}
|
||
|
||
// 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',
|
||
'<span style="color:#dc2626;font-weight:700;">$1</span>',
|
||
$html_body
|
||
);
|
||
|
||
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body '
|
||
. 'style="font-family:Arial,sans-serif;font-size:14px;line-height:1.6;color:#333;max-width:560px;margin:0 auto;padding:24px;">'
|
||
. '<div style="border-bottom:2px solid #1e3a6a;padding-bottom:12px;margin-bottom:20px;">'
|
||
. '<strong style="font-size:20px;color:#1e3a6a;letter-spacing:1px;">SKRIFT</strong>'
|
||
. '</div>'
|
||
. $html_body
|
||
. '<div style="margin-top:32px;padding-top:16px;border-top:1px solid #eee;font-size:12px;color:#999;">'
|
||
. 'Skrift Handschrift-Service | Diese E-Mail wurde automatisch generiert.'
|
||
. '</div></body></html>';
|
||
|
||
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<FilesMatch \"^$\">\n Order allow,deny\n Allow from all\n</FilesMatch>\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 '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
||
</Types>';
|
||
}
|
||
|
||
private static function xlsx_rels(): string {
|
||
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||
</Relationships>';
|
||
}
|
||
|
||
private static function xlsx_workbook(): string {
|
||
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||
<sheets><sheet name="Tabelle1" sheetId="1" r:id="rId1"/></sheets>
|
||
</workbook>';
|
||
}
|
||
|
||
private static function xlsx_workbook_rels(): string {
|
||
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||
</Relationships>';
|
||
}
|
||
|
||
private static function xlsx_styles(): string {
|
||
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||
<fonts count="2">
|
||
<font><sz val="11"/><name val="Calibri"/></font>
|
||
<font><b/><sz val="11"/><name val="Calibri"/></font>
|
||
</fonts>
|
||
<fills count="2">
|
||
<fill><patternFill patternType="none"/></fill>
|
||
<fill><patternFill patternType="gray125"/></fill>
|
||
</fills>
|
||
<borders count="1">
|
||
<border><left/><right/><top/><bottom/><diagonal/></border>
|
||
</borders>
|
||
<cellStyleXfs count="1">
|
||
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
|
||
</cellStyleXfs>
|
||
<cellXfs count="2">
|
||
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
|
||
<xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0"/>
|
||
</cellXfs>
|
||
</styleSheet>';
|
||
}
|
||
|
||
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 .= "<c r=\"{$ref}\"{$style} t=\"inlineStr\"><is><t>{$escaped}</t></is></c>";
|
||
$col_idx++;
|
||
}
|
||
$rows_xml .= "<row r=\"{$row_num}\">{$cells_xml}</row>";
|
||
$row_num++;
|
||
}
|
||
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||
<sheetData>' . $rows_xml . '</sheetData>
|
||
</worksheet>';
|
||
}
|
||
|
||
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();
|