diff --git a/module/sitemap b/module/sitemap new file mode 160000 index 0000000..2511d8f --- /dev/null +++ b/module/sitemap @@ -0,0 +1 @@ +Subproject commit 2511d8f82ed15071f7acd00d43faf259f0056b1c diff --git a/module/ticketcenter/CLAUDE.md b/module/ticketcenter/CLAUDE.md new file mode 100644 index 0000000..36a8fdd --- /dev/null +++ b/module/ticketcenter/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the **Ticket Center** module — a ticket management/support system — for the AWO Hamburg intranet. It is a traditional PHP application with no build system: files are served directly. + +## No Build System + +There is no `package.json`, `Makefile`, or test framework. No compilation or build step is needed. CSS/JS files are served as-is with `filemtime()`-based cache busting. + +## Architecture + +### Module Structure + +This module lives inside a larger **mysyde** platform. It depends on: +- Common platform functions (e.g. `encryptId`/`decryptId`, translation registry, permission helpers) +- Shared DB connection config at `mysyde/dc.config.php` (dev) or `mysyde/dc-server.config.php` (prod) +- Environment variables via `vlucas/phpdotenv` loaded from `/config/.env` and `/vendor/autoload.php` + +### Request Flow + +1. URL: `/module/ticketcenter/` +2. Main controller: `views/ticketcenter/show_ticketcenter.inc.php` — handles all ticket CRUD, state transitions, file uploads, and page routing (956 lines) +3. List view: `views/ticketcenter/show_ticketcenter_listform.inc.php` — renders ticket table via Grid.js +4. Detail/edit view: `views/ticketcenter/show_ticketcenter_cardform.inc.php` +5. Create view: `views/ticketcenter/show_ticketcenter_create_cardform.inc.php` +6. JSON API: `services/ticketcenter.api.php` — POST endpoint used by Grid.js for ticket list data +7. AJAX endpoints: `views/ticketcenter/show_ticketcenter_ajax.php` (category hierarchy), `show_ticketcenter_category_desc_ajax.php` + +### Admin Sub-views + +- `views/category/` — hierarchical category CRUD +- `views/process/` — ticket workflow process definitions +- `views/status/` — ticket status definitions +- `views/settings/` — system settings UI + +### Frontend Stack + +- **Grid.js** (`js/gridjs.umd.js`) — data table with server-side pagination/filtering via the JSON API +- **Select2** — enhanced dropdowns (loaded via CDN) +- **Semantic UI** — UI components (loaded via CDN) +- **FontAwesome** — icons (CDN) +- Vanilla JS + jQuery; no bundler or transpiler + +### Database Tables + +| Table | Purpose | +|---|---| +| `main_tickets` | Core ticket records | +| `main_tickets_messages` | Ticket conversation/comments | +| `main_tickets_category` | Hierarchical category definitions | +| `main_tickets_status` | Status definitions | +| `main_tickets_process` | Workflow process definitions | +| `main_tickets_notification` | Notification tracking | +| `main_contact` / `main_contact_department` | User/department info | +| `main_tickets_category_{mandant,department,contact}` | Visibility/access control per category | + +### Ticket Lifecycle + +Tickets move through status IDs: **Draft → 2 (Open) → 3 (In Progress) → 4 (Closed)** + +### List View Filters (cues) + +The `cue` parameter drives 8 ticket views: created by me, assigned to me, pool, archive, all, escalated, unassigned, insurance claims. + +### File Uploads + +Attachments are stored at `userdata/Tickets/{ticket_id}/`. + +## Language + +All UI text is in **German**. Button labels, validation messages, and field names are German throughout. + +## Key Conventions + +- PHP templating via `include`/`require` — no MVC framework +- Both MySQLi and PDO are used in different files +- IDs are encrypted/decrypted before being exposed to the frontend (`encryptId`/`decryptId`) +- Role-based access is enforced via mandant/department/contact associations on categories diff --git a/module/ticketcenter/views/ticketcenter/show_ticketcenter.inc.php b/module/ticketcenter/views/ticketcenter/show_ticketcenter.inc.php index 87fdaa6..66e6636 100644 --- a/module/ticketcenter/views/ticketcenter/show_ticketcenter.inc.php +++ b/module/ticketcenter/views/ticketcenter/show_ticketcenter.inc.php @@ -26,9 +26,9 @@ ini_set('display_startup_errors', 1); window.location.href='/intranet/de/ticketcenter/?action=edit&id=" . $encId . "';"; } @@ -66,16 +72,16 @@ switch ($_GET['action']) { save_new_ticket_individual_state(); save_category(true); if (isset($_POST['input_id']) && is_numeric($_POST['input_id'])) { + $ticket_id = (int)$_POST['input_id']; $insurance_claim = isset($_POST['input_insurance_case']) ? 1 : 0; $stmt = mysqli_prepare($GLOBALS['mysql_con'], "UPDATE main_tickets SET insurance_claim = ? WHERE id = ?"); if ($stmt) { - $input_id = (int)$_POST['input_id']; - mysqli_stmt_bind_param($stmt, "ii", $insurance_claim, $input_id); + mysqli_stmt_bind_param($stmt, "ii", $insurance_claim, $ticket_id); mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); } - } - if (isset($_POST['input_id']) && is_numeric($_POST['input_id'])) { + $recipients = get_ticket_direct_recipients($ticket_id); + make_notification($recipients, 'Ihr Ticket wurde aktualisiert', build_ticket_notification_email($ticket_id, 'Ihr Ticket wurde aktualisiert')); $encId = encryptId($_POST['input_id']); echo ""; } @@ -85,12 +91,19 @@ switch ($_GET['action']) { break; } +// ============================================================================= +// TICKET CRUD +// Kernoperationen: Anzeigen, Erstellen, Aktualisieren, Schließen, Löschen. +// ============================================================================= + +// Lädt ein Ticket aus der DB und zeigt die Detailansicht (cardform) an. +// Markiert dabei alle Benachrichtigungen des aktuellen Nutzers als gelesen. function edit_ticket($_id = "") { if ((int) $_id > 0) { $input_id = $_id; } - + if ($input_id <> '') { $query = "SELECT main_tickets.*,main_tickets_category.description AS 'category_description',receiver.name as 'receiver',sender.name as 'sender',main_tickets_state.description as 'state' FROM main_tickets LEFT JOIN main_contact AS receiver ON receiver.id = main_tickets.receiver_id @@ -98,7 +111,6 @@ function edit_ticket($_id = "") LEFT JOIN main_tickets_category ON main_tickets.main_ticket_category_id = main_tickets_category.id LEFT JOIN main_tickets_state ON main_tickets.current_state = main_tickets_state.id WHERE main_tickets.id = '" . $input_id . "'"; - // $query = "SELECT * FROM main_tickets WHERE id = '".$input_id."'"; $result = @mysqli_query($GLOBALS['mysql_con'], $query); $ticket = @mysqli_fetch_array($result); } @@ -107,7 +119,8 @@ function edit_ticket($_id = "") require_once("show_ticketcenter_cardform.inc.php"); } -// Speichert einen neuen oder aktualisierten ticket in der Datenbank und leitet je nach Bedarf entweder zum Listenformular zurück oder schließt das Bearbeitungsformular. +// Erstellt ein neues Ticket (INSERT) und benachrichtigt alle Verantwortlichen der Kategorie. +// Bei vorhandener input_id wird die Funktion ohne Aktion durchlaufen (Legacy-Pfad). function save_ticket($close = FALSE) { $modified_date = new DateTime(); @@ -118,28 +131,24 @@ function save_ticket($close = FALSE) } else { $sender_id = $GLOBALS['main_contact']['id']; - // if($_POST['input_new_ticket_sender'] != ""){ - // $sender_id = $_POST['input_new_ticket_sender']; - // } $location = isset($_POST["input_location"]) ? $_POST["input_location"] : ""; $address = isset($_POST["input_address"]) ? $_POST["input_address"] : ""; $facility = isset($_POST["input_facility"]) ? $_POST["input_facility"] : ""; - // last element of array $_POST["input_category"] $category = $_POST["input_category"]; $lastCategory = end($category); $insurance_claim = $_POST["input_insurance_case"] == "on" ? 1 : 0; $query = "INSERT INTO main_tickets ( - ticket_no, - description, + ticket_no, + description, main_ticket_category_id, - service_item_no, - last_action_date, - last_opening_date, - creation_date, - individual_state, - sender_id, - cost_center, + service_item_no, + last_action_date, + last_opening_date, + creation_date, + individual_state, + sender_id, + cost_center, link, location, address, @@ -149,13 +158,13 @@ function save_ticket($close = FALSE) '" . $_POST["input_ticket_no"] . "', '" . strip_tags($_POST["input_description"]) . "', '" . $lastCategory . "', - '" . $_POST["input_service_item"] . "', + '" . $_POST["input_service_item"] . "', NOW(), NOW(), NOW(), 2, '" . $sender_id . "', - '" . $_POST["input_cost_center"] . "', + '" . $_POST["input_cost_center"] . "', '" . $_POST["input_link"] . "', '" . $location . "', '" . $address . "', @@ -166,12 +175,14 @@ function save_ticket($close = FALSE) $message = "success"; } - mysqli_query($GLOBALS['mysql_con'], $query); if ($inserted == TRUE) { $input_id = mysqli_insert_id($GLOBALS['mysql_con']); + $recipients = get_ticket_recipients($input_id); + $subject = 'Neues Ticket | ' . $_POST['input_ticket_no']; + make_notification($recipients, $subject, build_ticket_notification_email($input_id, 'Ein neues Ticket wurde erstellt')); $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( 'Eröffnet von', @@ -218,9 +229,7 @@ function save_ticket($close = FALSE) '" . $type . "' )"; - // var_dump($query_insert_message);die(); @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); - } if ($close == TRUE) { @@ -229,22 +238,14 @@ function save_ticket($close = FALSE) echo " - "; -} - -function save_new_ticket_receiver() -{ - // Überprüfe, ob die POST-Daten vorhanden sind - if (isset($_POST["input_id"]) && isset($_POST["input_new_ticket_receiver"])) { - // Hole die POST-Daten und bereinige sie mit mysqli_real_escape_string - $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); - $new_receiver_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_new_ticket_receiver"]); - - // Überprüfe, ob die IDs gültig sind (keine leeren Werte oder ungültigen Zeichen) - if (!empty($input_id) && !empty($new_receiver_id) && is_numeric($input_id) && is_numeric($new_receiver_id)) { - // Aktuellen Empfänger laden - $qryCurrent = "SELECT receiver_id FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; - $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); - $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; - $current_receiver_id = $rowCurrent ? (string)$rowCurrent['receiver_id'] : null; - // Nur wenn sich der Empfänger geändert hat, speichern und in die History schreiben - if ($current_receiver_id !== (string)$new_receiver_id) { - // Baue die Abfrage mit einem Prepared Statement - $query = "UPDATE main_tickets SET receiver_id = ? WHERE id = ?"; - $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); - - if ($stmt) { - // Binde die Parameter (int, int) und führe die Abfrage aus - mysqli_stmt_bind_param($stmt, "ii", $new_receiver_id, $input_id); - mysqli_stmt_execute($stmt); - - // History-Eintrag nur bei Änderung - $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( - 'Weitergeleitet an', - '', - NOW(), - '" . $input_id . "', - '" . $new_receiver_id . "', - 'history' - )"; - @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); - - // Benachrichtigung nur bei Änderung - update_notification($input_id, 'state', "Der Status Bearbeiter wurde Ihnen im Ticket zugeteilt"); - - mysqli_stmt_close($stmt); - } - } - } - } - // edit_ticket($_POST["input_id"]); -} - -function save_new_due_date() { - // Überprüfe, ob die POST-Daten vorhanden sind - if (isset($_POST["input_id"]) && isset($_POST["input_due_date"])) { - // Hole die POST-Daten und bereinige sie mit mysqli_real_escape_string - $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); - $new_due_date= mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_due_date"]); - - // Überprüfe, ob die Werte gültig sind - if (!empty($input_id) && $new_due_date !== '') { - // Aktuelles Fälligkeitsdatum laden - $qryCurrent = "SELECT due_date FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; - $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); - $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; - $current_due_date = $rowCurrent ? (string)$rowCurrent['due_date'] : null; - - // Nur bei Änderung updaten - if ($current_due_date !== (string)$new_due_date) { - // Baue die Abfrage mit einem Prepared Statement - $query = "UPDATE main_tickets SET due_date = ? WHERE id = ?"; - $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); - - if ($stmt) { - mysqli_stmt_bind_param($stmt, "si", $new_due_date, $input_id); - mysqli_stmt_execute($stmt); - mysqli_stmt_close($stmt); - } - } - } - } - // Keine History hier; nur ändern, wenn sich der Wert unterscheidet -} - -function save_category($silent = false) -{ - // Überprüfe, ob die POST-Daten vorhanden sind - if (isset($_POST["input_id"]) && isset($_POST["input_category"])) { - // Hole die POST-Daten und bereinige sie mit mysqli_real_escape_string - $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); - $category = $_POST["input_category"]; - $lastCategory = end($category); - - // Aktuelle Werte laden - $qryCurrent = "SELECT main_ticket_category_id FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; - $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); - $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; - - $changed = false; - if ($rowCurrent) { - $cur_cat = (string)$rowCurrent['main_ticket_category_id']; - if ($cur_cat !== (string)$lastCategory) { - $changed = true; - } - } else { - $changed = true; // Ticket nicht gefunden -> versuche zu speichern - } - - if ($changed) { - $query = "UPDATE main_tickets SET main_ticket_category_id = ? WHERE id = ?"; - $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); - if ($stmt) { - mysqli_stmt_bind_param($stmt, "ii", $lastCategory, $input_id); - mysqli_stmt_execute($stmt); - mysqli_stmt_close($stmt); - } - - // History-Eintrag - $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( - 'Kategorie geändert', - '', - NOW(), - '" . $input_id . "', - '" . $GLOBALS["main_contact"]['id'] . "', - 'history' - )"; - @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); - - // Benachrichtigung bei Änderung - update_notification($input_id, 'state'); - } - } - if (!$silent && isset($_POST['input_id'])) { - edit_ticket($_POST["input_id"]); - } -} - -function save_new_ticket_individual_state($silent = false) -{ - // Überprüfe, ob die POST-Daten vorhanden sind - if (isset($_POST["input_id"]) && isset($_POST["input_main_tickets_status"])) { - // Hole die POST-Daten und bereinige sie mit mysqli_real_escape_string - $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); - $input_main_tickets_status = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_main_tickets_status"]); - - // Überprüfe, ob die IDs gültig sind (keine leeren Werte oder ungültigen Zeichen) - if (!empty($input_id) && !empty($input_main_tickets_status) && is_numeric($input_id) && is_numeric($input_main_tickets_status)) { - // Aktuellen Status laden - $qryCurrent = "SELECT individual_state FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; - $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); - $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; - $current_status = $rowCurrent ? (string)$rowCurrent['individual_state'] : null; - - // Nur bei Änderung speichern und in die History schreiben - if ($current_status !== (string)$input_main_tickets_status) { - // Baue die Abfrage mit einem Prepared Statement - $query = "UPDATE main_tickets SET individual_state = ? WHERE id = ?"; - $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); - - if ($stmt) { - // Binde die Parameter (int, int) und führe die Abfrage aus - mysqli_stmt_bind_param($stmt, "ii", $input_main_tickets_status, $input_id); - mysqli_stmt_execute($stmt); - mysqli_stmt_close($stmt); - } - - // Statusbeschreibung holen und History schreiben - $queryDesc = "SELECT description FROM main_tickets_status WHERE id = " . (int)$input_main_tickets_status; - $result = @mysqli_query($GLOBALS['mysql_con'], $queryDesc); - if ($row = @mysqli_fetch_array($result)) { - $status_description = $row['description']; - $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( - 'Status geändert zu $status_description', - '', - NOW(), - '" . $input_id . "', - '" . $GLOBALS["main_contact"]['id'] . "', - 'history' - )"; - @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); - } - - // Benachrichtigung nur bei Änderung - update_notification($input_id, 'state'); - } - } - } - if (!$silent && isset($_POST["input_id"])) { - edit_ticket($_POST["input_id"]); - } - - -} - -function save_ticket_supplieres() -{ - // Überprüfe, ob die erforderlichen POST-Daten vorhanden sind - if (isset($_POST["input_id"]) && isset($_POST["supplier_ids"])) { - // Bereinige die POST-Daten - $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); - $supplier_ids_str = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["supplier_ids"]); - - // Überprüfe, ob die Ticket-ID gültig ist - if (!empty($input_id) && is_numeric($input_id)) { - // 1. Lösche alle bestehenden Lieferantenverknüpfungen für dieses Ticket - $deleteQuery = "DELETE FROM main_ticket_suplier WHERE main_ticket_id = ?"; - $stmt = mysqli_prepare($GLOBALS['mysql_con'], $deleteQuery); - if ($stmt) { - mysqli_stmt_bind_param($stmt, "i", $input_id); - mysqli_stmt_execute($stmt); - mysqli_stmt_close($stmt); - } - - // 2. Füge die neuen Lieferantenverknüpfungen ein - // Der hidden Input enthält die IDs als komma-separierten String - $supplier_ids = array_filter(explode(',', $supplier_ids_str), function ($value) { - return is_numeric($value) && !empty($value); - }); - if (!empty($supplier_ids)) { - $insertQuery = "INSERT INTO main_ticket_suplier (main_ticket_id, main_ticket_suplier_id) VALUES (?, ?)"; - $stmt = mysqli_prepare($GLOBALS['mysql_con'], $insertQuery); - if ($stmt) { - foreach ($supplier_ids as $supplier_id) { - mysqli_stmt_bind_param($stmt, "ii", $input_id, $supplier_id); - mysqli_stmt_execute($stmt); - } - mysqli_stmt_close($stmt); - } - } - } - } - edit_ticket($_POST["input_id"]); -} - -function load_messages($ticket_id) -{ - $query = "SELECT main_tickets_messages.*, main_contact.name FROM main_tickets_messages LEFT JOIN main_contact ON main_contact.id = main_tickets_messages.main_contact_id WHERE main_tickets_id = $ticket_id ORDER BY datetime"; - $result = @mysqli_query($GLOBALS['mysql_con'], $query); - - while ($row = @mysqli_fetch_array($result)) { - $date = new DateTime($row["datetime"]); - $formatted_date = $date->format('d.m.Y H:i'); - - $outbound = ($GLOBALS['main_contact']['id'] == $row["main_contact_id"]) ? "outbound" : ""; - if ($row['type'] == 'history') { - echo "
" . $row['message'] . " " . $row['name'] . " am " . $formatted_date . " Uhr
"; - } else { - ?> - - + $(document).ready(function() { + window.location.href = '/intranet/de/ticketcenter/?cue=4000'; + }); + + "; +} + +// ============================================================================= +// TEILSPEICHERUNGEN +// Einzelne Felder des Tickets speichern — werden aus den Router-Cases aufgerufen. +// Jede Funktion prüft ob sich der Wert geändert hat, bevor sie schreibt. +// ============================================================================= + +// Speichert einen neuen Bearbeiter (receiver_id). Schreibt nur bei Änderung einen History-Eintrag. +function save_new_ticket_receiver() +{ + if (isset($_POST["input_id"]) && isset($_POST["input_new_ticket_receiver"])) { + $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); + $new_receiver_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_new_ticket_receiver"]); + + if (!empty($input_id) && !empty($new_receiver_id) && is_numeric($input_id) && is_numeric($new_receiver_id)) { + $qryCurrent = "SELECT receiver_id FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; + $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); + $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; + $current_receiver_id = $rowCurrent ? (string)$rowCurrent['receiver_id'] : null; + + if ($current_receiver_id !== (string)$new_receiver_id) { + $query = "UPDATE main_tickets SET receiver_id = ? WHERE id = ?"; + $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); + + if ($stmt) { + mysqli_stmt_bind_param($stmt, "ii", $new_receiver_id, $input_id); + mysqli_stmt_execute($stmt); + + $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( + 'Weitergeleitet an', + '', + NOW(), + '" . $input_id . "', + '" . $new_receiver_id . "', + 'history' + )"; + @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); + + mysqli_stmt_close($stmt); + } + } + } + } +} + +// Speichert das Fälligkeitsdatum (due_date). Schreibt nur bei Änderung. +function save_new_due_date() { + if (isset($_POST["input_id"]) && isset($_POST["input_due_date"])) { + $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); + $new_due_date= mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_due_date"]); + + if (!empty($input_id) && $new_due_date !== '') { + $qryCurrent = "SELECT due_date FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; + $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); + $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; + $current_due_date = $rowCurrent ? (string)$rowCurrent['due_date'] : null; + + if ($current_due_date !== (string)$new_due_date) { + $query = "UPDATE main_tickets SET due_date = ? WHERE id = ?"; + $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); + + if ($stmt) { + mysqli_stmt_bind_param($stmt, "si", $new_due_date, $input_id); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); + } + } + } + } +} + +// Speichert die Kategorie (main_ticket_category_id). Schreibt nur bei Änderung einen History-Eintrag. +// Mit $silent = true wird kein Redirect zur Detailansicht ausgelöst. +function save_category($silent = false) +{ + if (isset($_POST["input_id"]) && isset($_POST["input_category"])) { + $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); + $category = $_POST["input_category"]; + $lastCategory = end($category); + + $qryCurrent = "SELECT main_ticket_category_id FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; + $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); + $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; + + $changed = false; + if ($rowCurrent) { + $cur_cat = (string)$rowCurrent['main_ticket_category_id']; + if ($cur_cat !== (string)$lastCategory) { + $changed = true; + } + } else { + $changed = true; + } + + if ($changed) { + $query = "UPDATE main_tickets SET main_ticket_category_id = ? WHERE id = ?"; + $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); + if ($stmt) { + mysqli_stmt_bind_param($stmt, "ii", $lastCategory, $input_id); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); + } + + $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( + 'Kategorie geändert', + '', + NOW(), + '" . $input_id . "', + '" . $GLOBALS["main_contact"]['id'] . "', + 'history' + )"; + @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); + } + } + if (!$silent && isset($_POST['input_id'])) { + edit_ticket($_POST["input_id"]); + } +} + +// Speichert den individuellen Status (individual_state). Schreibt nur bei Änderung +// einen History-Eintrag mit der Statusbezeichnung. +// Mit $silent = true wird kein Redirect zur Detailansicht ausgelöst. +function save_new_ticket_individual_state($silent = false) +{ + if (isset($_POST["input_id"]) && isset($_POST["input_main_tickets_status"])) { + $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); + $input_main_tickets_status = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_main_tickets_status"]); + + if (!empty($input_id) && !empty($input_main_tickets_status) && is_numeric($input_id) && is_numeric($input_main_tickets_status)) { + $qryCurrent = "SELECT individual_state FROM main_tickets WHERE id = " . (int)$input_id . " LIMIT 1"; + $resCurrent = @mysqli_query($GLOBALS['mysql_con'], $qryCurrent); + $rowCurrent = $resCurrent ? @mysqli_fetch_assoc($resCurrent) : null; + $current_status = $rowCurrent ? (string)$rowCurrent['individual_state'] : null; + + if ($current_status !== (string)$input_main_tickets_status) { + $query = "UPDATE main_tickets SET individual_state = ? WHERE id = ?"; + $stmt = mysqli_prepare($GLOBALS['mysql_con'], $query); + + if ($stmt) { + mysqli_stmt_bind_param($stmt, "ii", $input_main_tickets_status, $input_id); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); + } + + $queryDesc = "SELECT description FROM main_tickets_status WHERE id = " . (int)$input_main_tickets_status; + $result = @mysqli_query($GLOBALS['mysql_con'], $queryDesc); + if ($row = @mysqli_fetch_array($result)) { + $status_description = $row['description']; + $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( + 'Status geändert zu $status_description', + '', + NOW(), + '" . $input_id . "', + '" . $GLOBALS["main_contact"]['id'] . "', + 'history' + )"; + @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); + } + } + } + } + if (!$silent && isset($_POST["input_id"])) { + edit_ticket($_POST["input_id"]); + } +} + +// Ersetzt alle Lieferantenverknüpfungen eines Tickets (DELETE + INSERT). +function save_ticket_supplieres() +{ + if (isset($_POST["input_id"]) && isset($_POST["supplier_ids"])) { + $input_id = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["input_id"]); + $supplier_ids_str = mysqli_real_escape_string($GLOBALS['mysql_con'], $_POST["supplier_ids"]); + + if (!empty($input_id) && is_numeric($input_id)) { + $deleteQuery = "DELETE FROM main_ticket_suplier WHERE main_ticket_id = ?"; + $stmt = mysqli_prepare($GLOBALS['mysql_con'], $deleteQuery); + if ($stmt) { + mysqli_stmt_bind_param($stmt, "i", $input_id); + mysqli_stmt_execute($stmt); + mysqli_stmt_close($stmt); + } + + $supplier_ids = array_filter(explode(',', $supplier_ids_str), function ($value) { + return is_numeric($value) && !empty($value); + }); + if (!empty($supplier_ids)) { + $insertQuery = "INSERT INTO main_ticket_suplier (main_ticket_id, main_ticket_suplier_id) VALUES (?, ?)"; + $stmt = mysqli_prepare($GLOBALS['mysql_con'], $insertQuery); + if ($stmt) { + foreach ($supplier_ids as $supplier_id) { + mysqli_stmt_bind_param($stmt, "ii", $input_id, $supplier_id); + mysqli_stmt_execute($stmt); + } + mysqli_stmt_close($stmt); + } + } + } + } + edit_ticket($_POST["input_id"]); +} + +// ============================================================================= +// CHAT / NACHRICHTEN +// Senden und Rendern von Ticket-Nachrichten inkl. Datei-Uploads. +// ============================================================================= + +// Speichert eine neue Nachricht (Text, Bild oder Datei) im Chat des Tickets. +// Lädt die Datei in /userdata/Tickets/{ticket_id}/ hoch, falls vorhanden. +function send_message() +{ + $ticket_id = $_POST["ticket_id"]; + $chat_message = $_POST["send_message"]; + $ticket_message_sender = $GLOBALS['main_contact']['id']; + + if (isset($_FILES['files']) && $_FILES['files']['error'] === UPLOAD_ERR_OK) { + $baseDir = realpath(__DIR__ . '/../../../../'); + $uploadDir = $baseDir . '/userdata/Tickets/' . $ticket_id . '/'; + if (!file_exists($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + + $allowedMimeTypes = ['image/jpeg', 'image/png']; + $fileName = basename($_FILES['files']['name']); + $fileTmpName = $_FILES['files']['tmp_name']; + $targetPath = $uploadDir . $fileName; + $fileMimeType = mime_content_type($fileTmpName); + $content = $_FILES['files']['name']; + + if (in_array($fileMimeType, $allowedMimeTypes)) { + if (move_uploaded_file($fileTmpName, $targetPath)) { + $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( + '" . $chat_message . "', + '" . $content . "', + NOW(), + '" . $ticket_id . "', + '" . $ticket_message_sender . "', + 'image' + )"; + } + } else { + if (move_uploaded_file($fileTmpName, $targetPath)) { + $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( + '" . $chat_message . "', + '" . $content . "', + NOW(), + '" . $ticket_id . "', + '" . $ticket_message_sender . "', + 'file' + )"; + } + } + } else { + $query_insert_message = "INSERT INTO main_tickets_messages(message, content, datetime, main_tickets_id, main_contact_id, type) VALUES( + '" . $chat_message . "', + '', + NOW(), + '" . $ticket_id . "', + '" . $ticket_message_sender . "', + 'message' + )"; + } + + @mysqli_query($GLOBALS['mysql_con'], $query_insert_message); +} + +// Gibt alle Nachrichten eines Tickets als HTML aus. +// History-Einträge werden als einzeilige Statusmeldung dargestellt, +// Nachrichten mit Bild/Datei-Unterstützung als Chat-Bubble. +function load_messages($ticket_id) +{ + $query = "SELECT main_tickets_messages.*, main_contact.name FROM main_tickets_messages LEFT JOIN main_contact ON main_contact.id = main_tickets_messages.main_contact_id WHERE main_tickets_id = $ticket_id ORDER BY datetime"; + $result = @mysqli_query($GLOBALS['mysql_con'], $query); + + while ($row = @mysqli_fetch_array($result)) { + $date = new DateTime($row["datetime"]); + $formatted_date = $date->format('d.m.Y H:i'); + + $outbound = ($GLOBALS['main_contact']['id'] == $row["main_contact_id"]) ? "outbound" : ""; + if ($row['type'] == 'history') { + echo "" . $row['message'] . " " . $row['name'] . " am " . $formatted_date . " Uhr
"; + } else { + ?> + + $row['name'], 'email' => $row['email']]; + } + return $recipients; +} + +// Bestimmt die Empfänger einer Chat-Nachricht anhand der Absenderrolle: +// - Schreibt der Ersteller → Bearbeiter wird benachrichtigt +// - Schreibt der Bearbeiter → Ersteller wird benachrichtigt +// - Schreibt ein Dritter → beide werden benachrichtigt +// - Sind Ersteller und Bearbeiter dieselbe Person → eine E-Mail an diese Person (außer sie schreibt selbst) +function get_message_recipients(int $ticket_id): array +{ + $con = $GLOBALS['mysql_con']; + $writer_id = (int)($GLOBALS['main_contact']['id'] ?? 0); + + $res = @mysqli_query($con, "SELECT sender_id, receiver_id FROM main_tickets WHERE id = $ticket_id LIMIT 1"); + $ticket = $res ? @mysqli_fetch_assoc($res) : null; + if (!$ticket) return []; + + $sender_id = (int)$ticket['sender_id']; + $receiver_id = (int)$ticket['receiver_id']; + + if ($sender_id === $receiver_id) { + // Ersteller und Bearbeiter sind dieselbe Person + $ids = $writer_id === $sender_id ? [] : [$sender_id]; + } elseif ($writer_id === $sender_id) { + // Ersteller schreibt → Bearbeiter benachrichtigen + $ids = [$receiver_id]; + } elseif ($writer_id === $receiver_id) { + // Bearbeiter schreibt → Ersteller benachrichtigen + $ids = [$sender_id]; + } else { + // Dritter schreibt → beide benachrichtigen + $ids = [$sender_id, $receiver_id]; + } + + $ids = array_values(array_unique(array_filter($ids, fn($id) => $id > 0))); + if (empty($ids)) return []; + + $in = implode(',', $ids); + $res = @mysqli_query($con, "SELECT name, email FROM main_contact WHERE id IN ($in) AND email != '' AND email IS NOT NULL"); + + $recipients = []; + while ($row = @mysqli_fetch_assoc($res)) { + $recipients[] = ['name' => $row['name'], 'email' => $row['email']]; + } + return $recipients; +} + +// Gibt Sender und Receiver eines Tickets zurück. +// Wenn beide identisch sind, wird eine E-Mail gesendet (auch an den aktuellen Nutzer). +// Wenn sie verschieden sind, wird der aktuelle Nutzer ausgeschlossen. +// Verwendung: Benachrichtigungen bei Statusänderungen, Schließen, Speichern. +function get_ticket_direct_recipients(int $ticket_id): array +{ + $con = $GLOBALS['mysql_con']; + $current = (int)($GLOBALS['main_contact']['id'] ?? 0); + + $res = @mysqli_query($con, "SELECT sender_id, receiver_id FROM main_tickets WHERE id = $ticket_id LIMIT 1"); + $ticket = $res ? @mysqli_fetch_assoc($res) : null; + if (!$ticket) return []; + + $sender_id = (int)$ticket['sender_id']; + $receiver_id = (int)$ticket['receiver_id']; + + if ($sender_id === $receiver_id) { + // Beide identisch — eine E-Mail, kein Ausschluss + $ids = [$sender_id]; + } else { + // Verschiedene Personen — aktuellen Nutzer ausschließen + $ids = array_filter( + [$sender_id, $receiver_id], + fn($id) => $id > 0 && $id !== $current + ); + } + + $ids = array_values(array_unique($ids)); + if (empty($ids)) return []; + + $in = implode(',', $ids); + $res = @mysqli_query($con, "SELECT name, email FROM main_contact WHERE id IN ($in) AND email != '' AND email IS NOT NULL"); + + $recipients = []; + while ($row = @mysqli_fetch_assoc($res)) { + $recipients[] = ['name' => $row['name'], 'email' => $row['email']]; + } + return $recipients; +} + +// Gibt alle Empfänger zurück, die über ein neues Ticket informiert werden sollen. +// Basiert auf der Kategorie-Zugriffskontrolle (Mandant / Abteilung / direkte Kontakte) +// sowie Sender und Receiver des Tickets. Verwendet dieselbe Logik wie user_can_see_ticket() in der API. +function get_ticket_recipients(int $ticket_id): array +{ + $con = $GLOBALS['mysql_con']; + $current_contact_id = (int)($GLOBALS['main_contact']['id'] ?? 0); + + $res = @mysqli_query($con, "SELECT sender_id, receiver_id, main_ticket_category_id + FROM main_tickets WHERE id = $ticket_id LIMIT 1"); + $ticket = $res ? @mysqli_fetch_assoc($res) : null; + if (!$ticket) return []; + + $category_id = (int)$ticket['main_ticket_category_id']; + $sender_id = (int)$ticket['sender_id']; + $receiver_id = (int)$ticket['receiver_id']; + + $contact_ids = []; + $mandant_ids = []; + $dept_ids = []; + + $res = @mysqli_query($con, "SELECT main_contact_id FROM main_tickets_category_contact WHERE main_tickets_category_id = $category_id"); + while ($row = @mysqli_fetch_assoc($res)) $contact_ids[] = (int)$row['main_contact_id']; + + $res = @mysqli_query($con, "SELECT main_mandant_id FROM main_tickets_category_mandant WHERE main_tickets_category_id = $category_id"); + while ($row = @mysqli_fetch_assoc($res)) $mandant_ids[] = (int)$row['main_mandant_id']; + + $res = @mysqli_query($con, "SELECT main_department_id FROM main_tickets_category_department WHERE main_tickets_category_id = $category_id"); + while ($row = @mysqli_fetch_assoc($res)) $dept_ids[] = (int)$row['main_department_id']; + + $category_contact_ids = array_merge([], $contact_ids); + + if (!empty($mandant_ids) || !empty($dept_ids)) { + $conditions = []; + + if (!empty($mandant_ids) && empty($dept_ids)) { + $in = implode(',', $mandant_ids); + $conditions[] = "mcd.main_mandant_id IN ($in)"; + } elseif (empty($mandant_ids) && !empty($dept_ids)) { + $in = implode(',', $dept_ids); + $conditions[] = "mcd.main_department_id IN ($in)"; + } else { + $pairs = []; + foreach ($mandant_ids as $m) { + foreach ($dept_ids as $d) { + $pairs[] = "(mcd.main_mandant_id = $m AND mcd.main_department_id = $d)"; + } + } + $conditions[] = '(' . implode(' OR ', $pairs) . ')'; + } + + $where = implode(' AND ', $conditions); + $res = @mysqli_query($con, "SELECT DISTINCT mcd.main_contact_id + FROM main_contact_department mcd + WHERE $where"); + while ($row = @mysqli_fetch_assoc($res)) { + $category_contact_ids[] = (int)$row['main_contact_id']; + } + } + + $all_ids = array_unique(array_merge($category_contact_ids, [$sender_id, $receiver_id])); + $all_ids = array_filter($all_ids, fn($id) => $id > 0 && $id !== $current_contact_id); + $all_ids = array_values($all_ids); + + if (empty($all_ids)) return []; + + $in = implode(',', $all_ids); + $res = @mysqli_query($con, "SELECT id, name, email FROM main_contact + WHERE id IN ($in) AND email != '' AND email IS NOT NULL"); + + $recipients = []; + while ($row = @mysqli_fetch_assoc($res)) { + $recipients[] = [ + 'name' => $row['name'], + 'email' => $row['email'], + ]; + } + + return $recipients; +} + +// Erstellt den HTML-Body für eine Ticket-Benachrichtigungs-E-Mail +// mit Ticketnummer, Beschreibung und Link zur Detailansicht. +function build_ticket_notification_email(int $ticket_id, string $heading, string $message_text = ''): string +{ + $res = @mysqli_query($GLOBALS['mysql_con'], "SELECT ticket_no, description FROM main_tickets WHERE id = $ticket_id LIMIT 1"); + $ticket = $res ? @mysqli_fetch_assoc($res) : []; + + $ticket_no = htmlspecialchars($ticket['ticket_no'] ?? ''); + $description = htmlspecialchars($ticket['description'] ?? ''); + $link = 'https://' . $_SERVER['HTTP_HOST'] . '/intranet/de/ticketcenter/?action=edit&id=' . encryptId($ticket_id); + + $message_block = $message_text + ? "" . nl2br($message_text) . "
" + : ''; + + return " + +
+
|
|