diff --git a/api.php b/api.php index 61bfccd..996c34d 100644 --- a/api.php +++ b/api.php @@ -233,6 +233,99 @@ function sendMail(string $to, string $subject, string $htmlBody): bool { return mail($to, $subject, $htmlBody, $headers, '-f ' . MAIL_FROM); } +// ==================== ZAMMAD CLIENT ==================== + +class ZammadClient { + private string $url; + private string $token; + + public function __construct(string $url, string $token) { + $this->url = rtrim($url, '/'); + $this->token = $token; + } + + private function request(string $method, string $endpoint, ?array $data = null): array { + $url = $this->url . '/api/v1/' . ltrim($endpoint, '/'); + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => [ + 'Authorization: Token token=' . $this->token, + 'Content-Type: application/json', + ], + ]); + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } elseif ($method === 'PUT') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode >= 400) { + $err = json_decode($response, true); + throw new \RuntimeException('Zammad API error (' . $httpCode . '): ' . ($err['error'] ?? $response)); + } + return json_decode($response, true) ?: []; + } + + /** Hae tikettejä (search API palauttaa kaikki joihin on oikeus) */ + public function getTickets(array $groupIds = [], int $page = 1, int $perPage = 100): array { + if (!empty($groupIds)) { + $query = implode(' OR ', array_map(fn($id) => 'group_id:' . $id, $groupIds)); + } else { + $query = '*'; + } + return $this->request('GET', 'tickets/search?query=' . urlencode($query) . '&per_page=' . $perPage . '&page=' . $page . '&expand=true'); + } + + /** Hae yksittäinen tiketti */ + public function getTicket(int $id): array { + return $this->request('GET', 'tickets/' . $id . '?expand=true'); + } + + /** Hae tiketin artikkelit */ + public function getArticles(int $ticketId): array { + return $this->request('GET', 'ticket_articles/by_ticket/' . $ticketId . '?expand=true'); + } + + /** Lähetä vastaus tikettiin */ + public function createArticle(int $ticketId, string $body, string $to = '', string $subject = '', string $type = 'email'): array { + $data = [ + 'ticket_id' => $ticketId, + 'body' => $body, + 'content_type' => 'text/html', + 'type' => $type, + 'internal' => false, + 'sender' => 'Agent', + ]; + if ($to) $data['to'] = $to; + if ($subject) $data['subject'] = $subject; + return $this->request('POST', 'ticket_articles', $data); + } + + /** Päivitä tiketin tila */ + public function updateTicket(int $ticketId, array $fields): array { + return $this->request('PUT', 'tickets/' . $ticketId, $fields); + } + + /** Hae ryhmät */ + public function getGroups(): array { + return $this->request('GET', 'groups?expand=true'); + } + + /** Testaa yhteys */ + public function testConnection(): array { + $user = $this->request('GET', 'users/me'); + $groups = $this->getGroups(); + return ['user' => $user['login'] ?? '?', 'groups' => count($groups), 'ok' => true]; + } +} + // ==================== IMAP CLIENT (socket-pohjainen, ei vaadi php-imap) ==================== class ImapClient { @@ -4875,6 +4968,241 @@ switch ($action) { } break; + // ==================== INTEGRAATIOT ==================== + + case 'integrations': + requireCompany(); + $integrations = dbLoadIntegrations($companyId); + // Piilota tokenit/salasanat + foreach ($integrations as &$integ) { + if ($integ['config']) { + $cfg = is_string($integ['config']) ? json_decode($integ['config'], true) : $integ['config']; + if (isset($cfg['token'])) $cfg['token'] = str_repeat('*', 8); + $integ['config'] = $cfg; + } + } + echo json_encode($integrations); + break; + + case 'integration_save': + requireCompany(); + $type = $input['type'] ?? ''; + $enabled = (bool)($input['enabled'] ?? false); + $config = $input['config'] ?? []; + if (!$type) { http_response_code(400); echo json_encode(['error' => 'Tyyppi puuttuu']); break; } + + // Jos token on maskattua (********), säilytä vanha + if (isset($config['token']) && preg_match('/^\*+$/', $config['token'])) { + $old = dbGetIntegration($companyId, $type); + if ($old && isset($old['config']['token'])) { + $config['token'] = $old['config']['token']; + } + } + + dbSaveIntegration($companyId, $type, $enabled, $config); + echo json_encode(['ok' => true]); + break; + + case 'integration_test': + requireCompany(); + $type = $input['type'] ?? $_GET['type'] ?? ''; + $integ = dbGetIntegration($companyId, $type); + if (!$integ) { http_response_code(404); echo json_encode(['error' => 'Integraatiota ei löydy']); break; } + + try { + if ($type === 'zammad') { + $cfg = $integ['config']; + $z = new ZammadClient($cfg['url'], $cfg['token']); + $result = $z->testConnection(); + echo json_encode($result); + } else { + echo json_encode(['error' => 'Tuntematon tyyppi']); + } + } catch (\Throwable $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + break; + + case 'zammad_groups': + requireCompany(); + $integ = dbGetIntegration($companyId, 'zammad'); + if (!$integ || !$integ['enabled']) { http_response_code(400); echo json_encode(['error' => 'Zammad ei käytössä']); break; } + try { + $z = new ZammadClient($integ['config']['url'], $integ['config']['token']); + echo json_encode($z->getGroups()); + } catch (\Throwable $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + break; + + case 'zammad_sync': + requireCompany(); + $integ = dbGetIntegration($companyId, 'zammad'); + if (!$integ || !$integ['enabled']) { http_response_code(400); echo json_encode(['error' => 'Zammad ei käytössä']); break; } + + try { + $cfg = $integ['config']; + $z = new ZammadClient($cfg['url'], $cfg['token']); + $groupIds = $cfg['group_ids'] ?? []; + $syncedGroupNames = $cfg['group_names'] ?? []; + + // Hae tikettejä Zammadista + $allTickets = []; + $page = 1; + do { + $batch = $z->getTickets($groupIds, $page, 100); + if (empty($batch)) break; + $allTickets = array_merge($allTickets, $batch); + $page++; + } while (count($batch) >= 100 && $page <= 10); + + $created = 0; + $updated = 0; + $messagesAdded = 0; + + // Zammad state → intran status + $stateMap = [ + 'new' => 'uusi', 'open' => 'avoin', 'pending reminder' => 'odottaa', + 'pending close' => 'odottaa', 'closed' => 'suljettu', + 'merged' => 'suljettu', 'removed' => 'suljettu', + ]; + // Zammad priority → intran priority + $priorityMap = [ + '1 low' => 'matala', '2 normal' => 'normaali', '3 high' => 'korkea', + ]; + + foreach ($allTickets as $zt) { + $zammadId = (int)$zt['id']; + $existing = dbGetTicketByZammadId($companyId, $zammadId); + + // Mäppää Zammad-ryhmä → intran tikettityyppi + $group = $zt['group'] ?? ''; + $type = 'muu'; + if (stripos($group, 'support') !== false || stripos($group, 'tech') !== false) $type = 'tekniikka'; + elseif (stripos($group, 'sales') !== false) $type = 'laskutus'; + elseif (stripos($group, 'abuse') !== false) $type = 'abuse'; + elseif (stripos($group, 'domain') !== false) $type = 'muu'; + elseif (stripos($group, 'noc') !== false) $type = 'vika'; + + $status = $stateMap[strtolower($zt['state'] ?? '')] ?? 'uusi'; + $priority = $priorityMap[strtolower($zt['priority'] ?? '')] ?? 'normaali'; + + if (!$existing) { + // Uusi tiketti + $ticketId = substr(uniqid(), -8) . bin2hex(random_bytes(2)); + $now = date('Y-m-d H:i:s'); + _dbExecute( + "INSERT INTO tickets (id, company_id, subject, from_email, from_name, status, type, priority, zammad_ticket_id, ticket_number, created, updated) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [$ticketId, $companyId, $zt['title'] ?? '', $zt['customer'] ?? '', $zt['customer'] ?? '', + $status, $type, $priority, $zammadId, (int)($zt['number'] ?? 0), + $zt['created_at'] ? date('Y-m-d H:i:s', strtotime($zt['created_at'])) : $now, + $zt['updated_at'] ? date('Y-m-d H:i:s', strtotime($zt['updated_at'])) : $now] + ); + $created++; + } else { + $ticketId = $existing['id']; + // Päivitä status/priority + _dbExecute( + "UPDATE tickets SET status = ?, type = ?, priority = ?, subject = ?, updated = ? WHERE id = ?", + [$status, $type, $priority, $zt['title'] ?? '', date('Y-m-d H:i:s', strtotime($zt['updated_at'] ?? 'now')), $ticketId] + ); + $updated++; + } + + // Synkkaa artikkelit → viestit + try { + $articles = $z->getArticles($zammadId); + foreach ($articles as $art) { + if (($art['internal'] ?? false)) continue; // Ohita sisäiset muistiinpanot + $artId = (int)$art['id']; + $existingMsg = dbGetMessageByZammadArticleId($ticketId, $artId); + if ($existingMsg) continue; + + $msgId = substr(uniqid(), -8) . bin2hex(random_bytes(2)); + $msgType = ($art['sender'] ?? '') === 'Customer' ? 'incoming' : 'outgoing'; + $body = $art['body'] ?? ''; + // Strippaa HTML tagit jos content_type on text/html + if (($art['content_type'] ?? '') === 'text/html') { + $body = strip_tags($body, '

    1. '); + } + + _dbExecute( + "INSERT INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, message_id, zammad_article_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + [$msgId, $ticketId, $msgType, + $art['from'] ?? '', $art['from'] ?? '', + $body, + $art['created_at'] ? date('Y-m-d H:i:s', strtotime($art['created_at'])) : date('Y-m-d H:i:s'), + $art['message_id'] ?? '', + $artId] + ); + $messagesAdded++; + } + } catch (\Throwable $e) { + // Artikkelihaku epäonnistui — jatka silti + error_log("Zammad articles error for ticket $zammadId: " . $e->getMessage()); + } + } + + echo json_encode([ + 'ok' => true, + 'tickets_found' => count($allTickets), + 'created' => $created, + 'updated' => $updated, + 'messages_added' => $messagesAdded, + ]); + } catch (\Throwable $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + break; + + case 'zammad_reply': + requireCompany(); + $ticketId = $input['ticket_id'] ?? ''; + $body = $input['body'] ?? ''; + if (!$ticketId || !$body) { http_response_code(400); echo json_encode(['error' => 'Tiketti ja viesti vaaditaan']); break; } + + $ticket = _dbFetchRow("SELECT * FROM tickets WHERE id = ? AND company_id = ?", [$ticketId, $companyId]); + if (!$ticket || !$ticket['zammad_ticket_id']) { + http_response_code(400); + echo json_encode(['error' => 'Tiketti ei ole Zammad-tiketti']); + break; + } + + $integ = dbGetIntegration($companyId, 'zammad'); + if (!$integ || !$integ['enabled']) { http_response_code(400); echo json_encode(['error' => 'Zammad ei käytössä']); break; } + + try { + $z = new ZammadClient($integ['config']['url'], $integ['config']['token']); + $result = $z->createArticle( + (int)$ticket['zammad_ticket_id'], + $body, + $ticket['from_email'] ?? '', + $ticket['subject'] ?? '' + ); + + // Tallenna myös paikalliseen tietokantaan + $msgId = substr(uniqid(), -8) . bin2hex(random_bytes(2)); + _dbExecute( + "INSERT INTO ticket_messages (id, ticket_id, type, from_email, from_name, body, timestamp, zammad_article_id) + VALUES (?, ?, 'outgoing', ?, ?, ?, ?, ?)", + [$msgId, $ticketId, + $result['from'] ?? '', $result['from'] ?? '', + $body, date('Y-m-d H:i:s'), + (int)($result['id'] ?? 0)] + ); + + echo json_encode(['ok' => true, 'message_id' => $msgId]); + } catch (\Throwable $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + break; + default: http_response_code(404); echo json_encode(['error' => 'Tuntematon toiminto']); diff --git a/db.php b/db.php index b203a76..ad963d0 100644 --- a/db.php +++ b/db.php @@ -602,6 +602,19 @@ function initDatabase(): void { FOREIGN KEY (laitetila_id) REFERENCES laitetilat(id) ON DELETE CASCADE, INDEX idx_laitetila (laitetila_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", + + "CREATE TABLE IF NOT EXISTS integrations ( + id VARCHAR(20) PRIMARY KEY, + company_id VARCHAR(50) NOT NULL, + type VARCHAR(50) NOT NULL, + enabled BOOLEAN DEFAULT FALSE, + config JSON, + created DATETIME, + updated DATETIME, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + UNIQUE KEY uk_company_type (company_id, type), + INDEX idx_company (company_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", ]; foreach ($tables as $i => $sql) { @@ -644,6 +657,8 @@ function initDatabase(): void { "ALTER TABLE ticket_rules ADD COLUMN enabled BOOLEAN DEFAULT TRUE AFTER auto_close_days", "ALTER TABLE ticket_rules ADD COLUMN set_priority VARCHAR(20) DEFAULT '' AFTER type_set", "ALTER TABLE ticket_rules ADD COLUMN set_tags VARCHAR(255) DEFAULT '' AFTER set_priority", + "ALTER TABLE tickets ADD COLUMN zammad_ticket_id INT DEFAULT NULL AFTER mailbox_id", + "ALTER TABLE ticket_messages ADD COLUMN zammad_article_id INT DEFAULT NULL AFTER message_id", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } @@ -2223,3 +2238,42 @@ function dbUpdateConnection(int $connectionId, array $data): void { $connectionId ]); } + +// ==================== INTEGRAATIOT ==================== + +function dbLoadIntegrations(string $companyId): array { + return _dbFetchAll("SELECT * FROM integrations WHERE company_id = ? ORDER BY type", [$companyId]); +} + +function dbGetIntegration(string $companyId, string $type): ?array { + $row = _dbFetchRow("SELECT * FROM integrations WHERE company_id = ? AND type = ?", [$companyId, $type]); + if ($row && $row['config']) { + $row['config'] = json_decode($row['config'], true) ?: []; + } + return $row; +} + +function dbSaveIntegration(string $companyId, string $type, bool $enabled, array $config): void { + $existing = _dbFetchRow("SELECT id FROM integrations WHERE company_id = ? AND type = ?", [$companyId, $type]); + $now = date('Y-m-d H:i:s'); + if ($existing) { + _dbExecute( + "UPDATE integrations SET enabled = ?, config = ?, updated = ? WHERE company_id = ? AND type = ?", + [$enabled ? 1 : 0, json_encode($config), $now, $companyId, $type] + ); + } else { + $id = substr(uniqid(), -8) . bin2hex(random_bytes(2)); + _dbExecute( + "INSERT INTO integrations (id, company_id, type, enabled, config, created, updated) VALUES (?, ?, ?, ?, ?, ?, ?)", + [$id, $companyId, $type, $enabled ? 1 : 0, json_encode($config), $now, $now] + ); + } +} + +function dbGetTicketByZammadId(string $companyId, int $zammadId): ?array { + return _dbFetchRow("SELECT * FROM tickets WHERE company_id = ? AND zammad_ticket_id = ?", [$companyId, $zammadId]); +} + +function dbGetMessageByZammadArticleId(string $ticketId, int $articleId): ?array { + return _dbFetchRow("SELECT * FROM ticket_messages WHERE ticket_id = ? AND zammad_article_id = ?", [$ticketId, $articleId]); +} diff --git a/index.html b/index.html index bb45a1f..ab423b3 100644 --- a/index.html +++ b/index.html @@ -1517,6 +1517,43 @@
+ +
+

Integraatiot

+

Ota käyttöön ja hallitse ulkoisia integraatioita moduuleittain.

+
+
+ + + +

Telegram-hälytykset

diff --git a/script.js b/script.js index 44e7975..83fb567 100644 --- a/script.js +++ b/script.js @@ -1332,6 +1332,7 @@ document.getElementById('profile-form').addEventListener('submit', async (e) => let tickets = []; let currentTicketId = null; +let currentTicketData = null; let ticketReplyType = 'reply'; const ticketStatusLabels = { @@ -1512,6 +1513,7 @@ async function showTicketDetail(id, companyId = '') { currentTicketCompanyId = companyId; const ticket = await apiCall('ticket_detail&id=' + encodeURIComponent(id) + ticketCompanyParam()); currentTicketId = id; + currentTicketData = ticket; // Header document.getElementById('ticket-detail-header').innerHTML = ` @@ -1881,19 +1883,26 @@ document.getElementById('btn-send-reply').addEventListener('click', async () => btn.textContent = 'Lähetetään...'; try { - const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply'; - const payload = { id: currentTicketId, body }; - if (ticketReplyType !== 'note') { - const mbSel = document.getElementById('reply-mailbox-select'); - const toFld = document.getElementById('reply-to'); - const ccFld = document.getElementById('reply-cc'); - const useSig = document.getElementById('reply-use-signature'); - if (mbSel) payload.mailbox_id = mbSel.value; - if (toFld && toFld.value.trim()) payload.to = toFld.value.trim(); - if (ccFld) payload.cc = ccFld.value.trim(); - if (useSig && !useSig.checked) payload.no_signature = true; + // Tarkista onko Zammad-tiketti + const isZammadTicket = currentTicketData?.zammad_ticket_id; + if (isZammadTicket && ticketReplyType !== 'note') { + // Lähetä Zammad API:n kautta + await apiCall('zammad_reply' + ticketCompanyParam(), 'POST', { ticket_id: currentTicketId, body }); + } else { + const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply'; + const payload = { id: currentTicketId, body }; + if (ticketReplyType !== 'note') { + const mbSel = document.getElementById('reply-mailbox-select'); + const toFld = document.getElementById('reply-to'); + const ccFld = document.getElementById('reply-cc'); + const useSig = document.getElementById('reply-use-signature'); + if (mbSel) payload.mailbox_id = mbSel.value; + if (toFld && toFld.value.trim()) payload.to = toFld.value.trim(); + if (ccFld) payload.cc = ccFld.value.trim(); + if (useSig && !useSig.checked) payload.no_signature = true; + } + await apiCall(action + ticketCompanyParam(), 'POST', payload); } - await apiCall(action + ticketCompanyParam(), 'POST', payload); // Reload the detail view await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e) { @@ -2352,6 +2361,10 @@ async function loadSettings() { document.getElementById('settings-telegram-chat').value = config.telegram_chat_id || ''; } catch (e) { console.error(e); } + // Integraatiot + loadIntegrations(); + loadZammadConfig(); + // Vastauspohjat loadTemplates(); } @@ -2422,6 +2435,173 @@ document.getElementById('btn-test-api').addEventListener('click', async () => { } catch (e) { result.textContent = 'Virhe: ' + e.message; } }); +// ==================== INTEGRAATIOT ==================== + +const INTEGRATION_TYPES = { + zammad: { name: 'Zammad', icon: '📧', desc: 'Synkronoi tiketit Zammad-helpdeskistä (O365-sähköpostit)' }, +}; + +async function loadIntegrations() { + const container = document.getElementById('integrations-list'); + if (!container) return; + try { + const integrations = await apiCall('integrations'); + renderIntegrations(integrations); + } catch (e) { + console.error('loadIntegrations:', e); + } +} + +function renderIntegrations(integrations) { + const container = document.getElementById('integrations-list'); + const enabledMap = {}; + integrations.forEach(i => { enabledMap[i.type] = i.enabled; }); + + let html = ''; + for (const [type, meta] of Object.entries(INTEGRATION_TYPES)) { + const enabled = enabledMap[type] || false; + html += ` +
+ +
`; + } + container.innerHTML = html; + + // Toggle-napit + container.querySelectorAll('.integration-enabled').forEach(cb => { + cb.addEventListener('change', async () => { + const type = cb.dataset.type; + const configCard = document.getElementById(type + '-config-card'); + if (configCard) configCard.style.display = cb.checked ? '' : 'none'; + // Tallenna toggle-tila + try { + const existing = await apiCall('integrations'); + const old = existing.find(i => i.type === type); + const config = old?.config || {}; + await apiCall('integration_save', 'POST', { type, enabled: cb.checked, config }); + } catch (e) { console.error(e); } + }); + }); + + // Näytä config-kortit käytössä oleville + integrations.forEach(i => { + const card = document.getElementById(i.type + '-config-card'); + if (card) card.style.display = i.enabled ? '' : 'none'; + }); +} + +async function loadZammadConfig() { + try { + const integrations = await apiCall('integrations'); + const zammad = integrations.find(i => i.type === 'zammad'); + if (zammad && zammad.config) { + document.getElementById('zammad-url').value = zammad.config.url || ''; + document.getElementById('zammad-token').value = zammad.config.token || ''; + // Renderöi ryhmät jos tallennettu + if (zammad.config.group_ids && zammad.config.group_names) { + renderZammadGroupCheckboxes( + zammad.config.group_names.map((name, i) => ({ + id: zammad.config.group_ids[i], + name: name, + })), + zammad.config.group_ids + ); + } + } + } catch (e) { console.error(e); } +} + +function renderZammadGroupCheckboxes(groups, selectedIds = []) { + const container = document.getElementById('zammad-groups-list'); + if (!groups.length) { container.innerHTML = 'Ei ryhmiä.'; return; } + container.innerHTML = groups.map(g => ` + + `).join(''); +} + +// Zammad — Tallenna +document.getElementById('btn-save-zammad')?.addEventListener('click', async () => { + const url = document.getElementById('zammad-url').value.trim(); + const token = document.getElementById('zammad-token').value.trim(); + if (!url || !token) { alert('URL ja token vaaditaan'); return; } + + const groupCbs = document.querySelectorAll('.zammad-group-cb:checked'); + const groupIds = Array.from(groupCbs).map(cb => cb.value); + const groupNames = Array.from(groupCbs).map(cb => cb.dataset.name); + + try { + await apiCall('integration_save', 'POST', { + type: 'zammad', + enabled: document.querySelector('.integration-enabled[data-type="zammad"]')?.checked || false, + config: { url, token, group_ids: groupIds, group_names: groupNames }, + }); + alert('Zammad-asetukset tallennettu!'); + } catch (e) { alert('Virhe: ' + e.message); } +}); + +// Zammad — Testaa yhteys +document.getElementById('btn-test-zammad')?.addEventListener('click', async () => { + const result = document.getElementById('zammad-test-result'); + result.style.display = 'block'; + result.style.background = '#f8f9fb'; + result.textContent = 'Testataan yhteyttä...'; + try { + const res = await apiCall('integration_test', 'POST', { type: 'zammad' }); + result.style.background = '#d4edda'; + result.textContent = `✅ Yhteys OK! Käyttäjä: ${res.user}, Ryhmiä: ${res.groups}`; + } catch (e) { + result.style.background = '#f8d7da'; + result.textContent = '❌ ' + e.message; + } +}); + +// Zammad — Lataa ryhmät +document.getElementById('btn-load-zammad-groups')?.addEventListener('click', async () => { + try { + const groups = await apiCall('zammad_groups'); + const activeGroups = groups.filter(g => g.active); + + // Hae tallennetut valitut ryhmät + const integrations = await apiCall('integrations'); + const zammad = integrations.find(i => i.type === 'zammad'); + const selectedIds = zammad?.config?.group_ids || []; + + renderZammadGroupCheckboxes(activeGroups, selectedIds); + } catch (e) { alert('Virhe: ' + e.message); } +}); + +// Zammad — Synkronoi nyt +document.getElementById('btn-sync-zammad')?.addEventListener('click', async () => { + const result = document.getElementById('zammad-sync-result'); + result.style.display = 'block'; + result.style.background = '#f8f9fb'; + result.innerHTML = '⏳ Synkronoidaan...'; + try { + const res = await apiCall('zammad_sync', 'POST', {}); + result.style.background = '#d4edda'; + result.innerHTML = `✅ Synkronointi valmis!
+ Tikettejä löytyi: ${res.tickets_found}
+ Uusia tikettejä: ${res.created}
+ Päivitettyjä: ${res.updated}
+ Uusia viestejä: ${res.messages_added}`; + // Päivitä tikettilista jos ollaan tukivälilehdellä + if (typeof loadTickets === 'function') loadTickets(); + } catch (e) { + result.style.background = '#f8d7da'; + result.innerHTML = '❌ ' + e.message; + } +}); + // ==================== MODALS ==================== customerModal.addEventListener('click', (e) => { if (e.target === customerModal) customerModal.style.display = 'none'; }); diff --git a/style.css b/style.css index 1bcfbb7..ec0a1f6 100644 --- a/style.css +++ b/style.css @@ -1999,3 +1999,37 @@ span.empty { .combo-badge.taken { background: #fee2e2; color: #991b1b; } .combo-badge.subnet { background: #e0e7ff; color: #3730a3; } .combo-grp { padding: 0.3rem 0.7rem; font-size: 0.75rem; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.03em; } + +/* Integraatiot */ +.integration-item { + background: #f8f9fb; + border-radius: 10px; + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + transition: background 0.15s; +} +.integration-item:hover { background: #eef1f6; } +.integration-toggle { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + user-select: none; +} +.integration-toggle input[type="checkbox"] { + width: 18px; height: 18px; accent-color: var(--primary-color); +} +.integration-icon { + font-size: 1.5rem; + flex-shrink: 0; +} +.integration-info { + display: flex; + flex-direction: column; + gap: 0.1rem; +} +.integration-info strong { font-size: 0.95rem; color: #333; } +.integration-info small { font-size: 0.8rem; color: #888; } +.integration-config-card { + border-left: 3px solid var(--primary-color); +}