Lisää Zammad-integraatio ja modulaarinen integraatiot-hallinta

- Uusi integrations-taulu tietokantaan (moduulimalli: type, enabled, config)
- ZammadClient-luokka: tiketit, artikkelit, vastaukset, ryhmät
- API-endpointit: integration_save, integration_test, zammad_sync, zammad_reply, zammad_groups
- Synkronointi: Zammad-tiketit → intran tiketit, artikkelit → viestit
- Vastaukset: Zammad-tiketteihin vastaus kulkee Zammad API:n kautta (→ O365)
- UI: Integraatiot-osio API-välilehdellä, toggle-kytkimet, Zammad-konfiguraatio
- tickets.zammad_ticket_id ja ticket_messages.zammad_article_id linkitys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 19:25:51 +02:00
parent 1aea4bde20
commit fa8aaed11e
5 changed files with 645 additions and 12 deletions

204
script.js
View File

@@ -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 += `
<div class="integration-item" data-type="${type}">
<label class="integration-toggle">
<input type="checkbox" class="integration-enabled" data-type="${type}" ${enabled ? 'checked' : ''}>
<span class="integration-icon">${meta.icon}</span>
<span class="integration-info">
<strong>${esc(meta.name)}</strong>
<small>${esc(meta.desc)}</small>
</span>
</label>
</div>`;
}
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 = '<span style="color:#888;">Ei ryhmiä.</span>'; return; }
container.innerHTML = groups.map(g => `
<label style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.3rem;cursor:pointer;">
<input type="checkbox" class="zammad-group-cb" value="${g.id}" data-name="${esc(g.name)}" ${selectedIds.includes(g.id) || selectedIds.includes(String(g.id)) ? 'checked' : ''}>
${esc(g.name)}
</label>
`).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!<br>
Tikettejä löytyi: <strong>${res.tickets_found}</strong><br>
Uusia tikettejä: <strong>${res.created}</strong><br>
Päivitettyjä: <strong>${res.updated}</strong><br>
Uusia viestejä: <strong>${res.messages_added}</strong>`;
// 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'; });