From 656b5042e45146ab0b405eab78ea85360687a146 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Thu, 12 Mar 2026 12:52:54 +0200 Subject: [PATCH] =?UTF-8?q?Asiakaspalvelu:=20alinavi-uudelleenj=C3=A4rjest?= =?UTF-8?q?ely=20+=20tikettityyppien=20hallinta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vastauspohjat, Säännöt ja Asetukset siirretty omiksi alinaveikseen tikettilistan overlay-napeista. Säännöt-välilehdelle lisätty tikettityyppien hallinta (lisää/poista). Tyypit tallennetaan tietokantaan yrityskohtaisesti ja populoidaan dynaamisesti kaikkiin dropdown-valikoihin. Co-Authored-By: Claude Opus 4.6 --- api.php | 57 +++++++++- db.php | 58 +++++++++++ index.html | 297 ++++++++++++++++++++++++++--------------------------- script.js | 162 +++++++++++++++++------------ style.css | 17 +++ 5 files changed, 377 insertions(+), 214 deletions(-) diff --git a/api.php b/api.php index cf98656..8b424a2 100644 --- a/api.php +++ b/api.php @@ -3256,7 +3256,7 @@ switch ($action) { $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $type = $input['type'] ?? ''; - $validTypes = ['laskutus', 'tekniikka', 'vika', 'muu']; + $validTypes = array_map(fn($t) => $t['value'], dbLoadTicketTypes($companyId)); if (!in_array($type, $validTypes)) { http_response_code(400); echo json_encode(['error' => 'Virheellinen tyyppi']); @@ -3511,6 +3511,61 @@ switch ($action) { echo json_encode(['success' => true]); break; + case 'ticket_types': + requireAuth(); + $companyId = requireCompany(); + echo json_encode(dbLoadTicketTypes($companyId)); + break; + + case 'ticket_type_save': + requireAuth(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $value = preg_replace('/[^a-z0-9_-]/', '', strtolower(trim($input['value'] ?? ''))); + $label = trim($input['label'] ?? ''); + if (!$value || !$label) { + http_response_code(400); + echo json_encode(['error' => 'Tunnus ja nimi vaaditaan']); + break; + } + dbSaveTicketType($companyId, [ + 'id' => $input['id'] ?? null, + 'value' => $value, + 'label' => $label, + 'color' => $input['color'] ?? '', + 'sort_order' => intval($input['sort_order'] ?? 0), + ]); + dbAddLog($companyId, currentUser(), 'config_update', '', '', 'Tikettityyppi: ' . $label); + echo json_encode(dbLoadTicketTypes($companyId)); + break; + + case 'ticket_type_delete': + requireAuth(); + $companyId = requireCompany(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $value = $input['value'] ?? ''; + if (!$value) { + http_response_code(400); + echo json_encode(['error' => 'Tyyppi puuttuu']); + break; + } + // Tarkista onko käytössä + $tickets = dbLoadTickets($companyId); + $inUse = 0; + foreach ($tickets as $t) { + if (($t['type'] ?? '') === $value) $inUse++; + } + if ($inUse > 0) { + http_response_code(400); + echo json_encode(['error' => "Tyyppiä käytetään {$inUse} tiketissä, ei voi poistaa"]); + break; + } + dbDeleteTicketType($companyId, $value); + echo json_encode(dbLoadTicketTypes($companyId)); + break; + case 'ticket_priority': requireAuth(); $companyId = requireCompanyOrParam(); diff --git a/db.php b/db.php index 8b4262c..410c02c 100644 --- a/db.php +++ b/db.php @@ -346,6 +346,18 @@ function initDatabase(): void { INDEX idx_company (company_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + "CREATE TABLE IF NOT EXISTS ticket_types ( + id VARCHAR(20) PRIMARY KEY, + company_id VARCHAR(50) NOT NULL, + value VARCHAR(50) NOT NULL, + label VARCHAR(100) NOT NULL, + color VARCHAR(20) DEFAULT '', + sort_order INT DEFAULT 0, + UNIQUE KEY uk_company_value (company_id, value), + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + INDEX idx_company (company_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + "CREATE TABLE IF NOT EXISTS customer_priority_emails ( id INT AUTO_INCREMENT PRIMARY KEY, company_id VARCHAR(50) NOT NULL, @@ -1582,6 +1594,52 @@ function dbDeleteTicketRule(string $ruleId): void { _dbExecute("DELETE FROM ticket_rules WHERE id = ?", [$ruleId]); } +// ==================== TIKETTITYYPIT ==================== + +function dbLoadTicketTypes(string $companyId): array { + $types = _dbFetchAll("SELECT * FROM ticket_types WHERE company_id = ? ORDER BY sort_order, label", [$companyId]); + // Jos ei tyyppejä, luo oletukset + if (empty($types)) { + $defaults = [ + ['value' => 'laskutus', 'label' => 'Laskutus', 'sort_order' => 1], + ['value' => 'tekniikka', 'label' => 'Tekniikka', 'sort_order' => 2], + ['value' => 'vika', 'label' => 'Vika', 'sort_order' => 3], + ['value' => 'abuse', 'label' => 'Abuse', 'sort_order' => 4], + ['value' => 'muu', 'label' => 'Muu', 'sort_order' => 5], + ]; + foreach ($defaults as $d) { + dbSaveTicketType($companyId, $d); + } + $types = _dbFetchAll("SELECT * FROM ticket_types WHERE company_id = ? ORDER BY sort_order, label", [$companyId]); + } + foreach ($types as &$t) { + $t['sort_order'] = (int)($t['sort_order'] ?? 0); + unset($t['company_id']); + } + return $types; +} + +function dbSaveTicketType(string $companyId, array $type): void { + $id = $type['id'] ?? generateId(); + _dbExecute(" + INSERT INTO ticket_types (id, company_id, value, label, color, sort_order) + VALUES (:id, :company_id, :value, :label, :color, :sort_order) + ON DUPLICATE KEY UPDATE + label = VALUES(label), color = VALUES(color), sort_order = VALUES(sort_order) + ", [ + 'id' => $id, + 'company_id' => $companyId, + 'value' => $type['value'] ?? '', + 'label' => $type['label'] ?? '', + 'color' => $type['color'] ?? '', + 'sort_order' => $type['sort_order'] ?? 0, + ]); +} + +function dbDeleteTicketType(string $companyId, string $value): void { + _dbExecute("DELETE FROM ticket_types WHERE company_id = ? AND value = ?", [$companyId, $value]); +} + // ==================== YRITYKSEN API-ASETUKSET ==================== function dbGetCompanyConfig(string $companyId): array { diff --git a/index.html b/index.html index 6731f85..0f27d2b 100644 --- a/index.html +++ b/index.html @@ -1066,6 +1066,9 @@
+ + +
@@ -1079,11 +1082,6 @@
Suljetut - - -
- - - - - - - - - + +
+
+
+
+

Automaattisäännöt

+ +
+

Säännöt soveltuvat automaattisesti uusiin tiketteihin haettaessa sähköposteja. Ensimmäinen täsmäävä sääntö voittaa.

+
+
+ + + + +
+

Tikettityypit

+

Hallitse yrityksen tikettityyppejä. Käytössä olevia tyyppejä ei voi poistaa.

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

Vastauspohjat

+ +
+

Yrityksen yhteiset vastauspohjat tiketteihin. Valittavissa vastauslomakkeen valikosta kaikille käyttäjille.

+
+
+ + +
+
+ + +
+
+
+

Sähköpostiallekirjoitukset

+

Allekirjoitus liitetään automaattisesti sähköpostivastausten loppuun.

+
+
+ +
+

Postilaatikoiden näkyvyys

+

Poista rasti postilaatikoista joiden tikettejä et halua nähdä.

+
+
+ +
+ +
+
+
+
diff --git a/script.js b/script.js index 7b58eb1..ae1c080 100644 --- a/script.js +++ b/script.js @@ -301,11 +301,8 @@ function switchToTab(target, subTab) { if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); - if (subTab === 'ohjeet') { - switchSupportSubTab('support-ohjeet'); - } else { - switchSupportSubTab('support-tickets'); - } + const supportSubMap = { ohjeet: 'support-ohjeet', saannot: 'support-saannot', vastauspohjat: 'support-vastauspohjat', asetukset: 'support-asetukset' }; + switchSupportSubTab(supportSubMap[subTab] || 'support-tickets'); } if (target === 'documents') { if (subTab && subTab !== 'kokoukset') { @@ -1344,7 +1341,7 @@ const ticketStatusLabels = { suljettu: 'Suljettu', }; -const ticketTypeLabels = { +let ticketTypeLabels = { laskutus: 'Laskutus', tekniikka: 'Tekniikka', vika: 'Vika', @@ -1520,10 +1517,9 @@ async function showTicketDetail(id, companyId = '') {