Paginointi asiakaspalveluun + Zammad-ryhmät näkyvyysasetuksiin

- Paginointi: 100 tikettiä/sivu, navigointipalkki sivujen välillä
- Filtterit resetoivat sivunumeron 1:ksi
- Select All valitsee vain nykyisen sivun tiketit
- Zammad-synkronointi tallentaa source=zammad ja zammad_group
- Postilaatikoiden näkyvyysasetuksissa Zammad-ryhmät (Zammad)-merkinnällä
- Zammad-ryhmien piilotus filtteröi tiketit samalla tavalla kuin postilaatikot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 23:20:25 +02:00
parent cc6e5c2653
commit 08bce71b5b
2 changed files with 105 additions and 17 deletions

12
api.php
View File

@@ -5132,20 +5132,20 @@ switch ($action) {
$ticketId = substr(uniqid(), -8) . bin2hex(random_bytes(2)); $ticketId = substr(uniqid(), -8) . bin2hex(random_bytes(2));
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
_dbExecute( _dbExecute(
"INSERT INTO tickets (id, company_id, subject, from_email, from_name, status, type, priority, zammad_ticket_id, ticket_number, created, updated) "INSERT INTO tickets (id, company_id, subject, from_email, from_name, status, type, priority, zammad_ticket_id, zammad_group, source, ticket_number, created, updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[$ticketId, $companyId, $zt['title'] ?? '', $zt['customer'] ?? '', $zt['customer'] ?? '', [$ticketId, $companyId, $zt['title'] ?? '', $zt['customer'] ?? '', $zt['customer'] ?? '',
$status, $type, $priority, $zammadId, (int)($zt['number'] ?? 0), $status, $type, $priority, $zammadId, $group, 'zammad', (int)($zt['number'] ?? 0),
$zt['created_at'] ? date('Y-m-d H:i:s', strtotime($zt['created_at'])) : $now, $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] $zt['updated_at'] ? date('Y-m-d H:i:s', strtotime($zt['updated_at'])) : $now]
); );
$created++; $created++;
} else { } else {
$ticketId = $existing['id']; $ticketId = $existing['id'];
// Päivitä status/priority // Päivitä status/priority/group
_dbExecute( _dbExecute(
"UPDATE tickets SET status = ?, type = ?, priority = ?, subject = ?, updated = ? WHERE id = ?", "UPDATE tickets SET status = ?, type = ?, priority = ?, subject = ?, zammad_group = ?, updated = ? WHERE id = ?",
[$status, $type, $priority, $zt['title'] ?? '', date('Y-m-d H:i:s', strtotime($zt['updated_at'] ?? 'now')), $ticketId] [$status, $type, $priority, $zt['title'] ?? '', $group, date('Y-m-d H:i:s', strtotime($zt['updated_at'] ?? 'now')), $ticketId]
); );
$updated++; $updated++;
} }

110
script.js
View File

@@ -1350,11 +1350,15 @@ let ticketTypeLabels = {
muu: 'Muu', muu: 'Muu',
}; };
let ticketPage = 1;
const TICKETS_PER_PAGE = 100;
async function loadTickets() { async function loadTickets() {
try { try {
// Hae kaikkien yritysten tiketit jos useampi yritys // Hae kaikkien yritysten tiketit jos useampi yritys
const allParam = availableCompanies.length > 1 ? '&all=1' : ''; const allParam = availableCompanies.length > 1 ? '&all=1' : '';
tickets = await apiCall('tickets' + allParam); tickets = await apiCall('tickets' + allParam);
ticketPage = 1;
renderTickets(); renderTickets();
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
} }
@@ -1367,9 +1371,15 @@ function renderTickets() {
const showMine = document.getElementById('ticket-show-mine').checked; const showMine = document.getElementById('ticket-show-mine').checked;
let filtered = tickets; let filtered = tickets;
// Piilota piilotettujen postilaatikoiden tiketit // Piilota piilotettujen postilaatikoiden ja Zammad-ryhmien tiketit
if (currentHiddenMailboxes.length > 0) { if (currentHiddenMailboxes.length > 0) {
filtered = filtered.filter(t => !currentHiddenMailboxes.includes(String(t.mailbox_id)) && !currentHiddenMailboxes.includes(t.mailbox_id)); filtered = filtered.filter(t => {
// Piilota mailbox-perusteisesti
if (t.mailbox_id && (currentHiddenMailboxes.includes(String(t.mailbox_id)) || currentHiddenMailboxes.includes(t.mailbox_id))) return false;
// Piilota Zammad-ryhmä-perusteisesti
if (t.source === 'zammad' && t.zammad_group && currentHiddenMailboxes.includes('zammad_group:' + t.zammad_group)) return false;
return true;
});
} }
// Suljetut näkyvät vain kun täppä on päällä // Suljetut näkyvät vain kun täppä on päällä
@@ -1431,6 +1441,14 @@ function renderTickets() {
const ttbody = document.getElementById('tickets-tbody'); const ttbody = document.getElementById('tickets-tbody');
const noTickets = document.getElementById('no-tickets'); const noTickets = document.getElementById('no-tickets');
// Paginointi
const totalFiltered = filtered.length;
const totalPages = Math.max(1, Math.ceil(totalFiltered / TICKETS_PER_PAGE));
if (ticketPage > totalPages) ticketPage = totalPages;
const startIdx = (ticketPage - 1) * TICKETS_PER_PAGE;
const pageTickets = filtered.slice(startIdx, startIdx + TICKETS_PER_PAGE);
if (filtered.length === 0) { if (filtered.length === 0) {
ttbody.innerHTML = ''; ttbody.innerHTML = '';
noTickets.style.display = 'block'; noTickets.style.display = 'block';
@@ -1439,7 +1457,7 @@ function renderTickets() {
noTickets.style.display = 'none'; noTickets.style.display = 'none';
document.getElementById('tickets-table').style.display = 'table'; document.getElementById('tickets-table').style.display = 'table';
const multiCompany = availableCompanies.length > 1; const multiCompany = availableCompanies.length > 1;
ttbody.innerHTML = filtered.map(t => { ttbody.innerHTML = pageTickets.map(t => {
const lastType = t.last_message_type === 'reply_out' ? '&#8594;' : (t.last_message_type === 'note' ? '&#128221;' : '&#8592;'); const lastType = t.last_message_type === 'reply_out' ? '&#8594;' : (t.last_message_type === 'note' ? '&#128221;' : '&#8592;');
const typeLabel = ticketTypeLabels[t.type] || 'Muu'; const typeLabel = ticketTypeLabels[t.type] || 'Muu';
const rowClass = t.priority === 'urgent' ? 'ticket-row-urgent' : (t.priority === 'tärkeä' ? 'ticket-row-important' : (t.status === 'kasittelyssa' ? 'ticket-row-active' : '')); const rowClass = t.priority === 'urgent' ? 'ticket-row-urgent' : (t.priority === 'tärkeä' ? 'ticket-row-important' : (t.status === 'kasittelyssa' ? 'ticket-row-active' : ''));
@@ -1479,15 +1497,63 @@ function renderTickets() {
if (counts.kasittelyssa) parts.push(`${counts.kasittelyssa} käsittelyssä`); if (counts.kasittelyssa) parts.push(`${counts.kasittelyssa} käsittelyssä`);
if (counts.odottaa) parts.push(`${counts.odottaa} odottaa`); if (counts.odottaa) parts.push(`${counts.odottaa} odottaa`);
document.getElementById('ticket-status-summary').textContent = parts.join(' · '); document.getElementById('ticket-status-summary').textContent = parts.join(' · ');
// Paginointipalkki
renderTicketPagination(totalFiltered, totalPages);
} }
document.getElementById('ticket-search-input').addEventListener('input', () => renderTickets()); function renderTicketPagination(totalFiltered, totalPages) {
document.getElementById('ticket-status-filter').addEventListener('change', () => renderTickets()); let paginationEl = document.getElementById('ticket-pagination');
document.getElementById('ticket-type-filter').addEventListener('change', () => renderTickets()); if (!paginationEl) {
document.getElementById('ticket-tag-filter').addEventListener('input', () => renderTickets()); paginationEl = document.createElement('div');
document.getElementById('ticket-sort').addEventListener('change', () => renderTickets()); paginationEl.id = 'ticket-pagination';
document.getElementById('ticket-show-closed').addEventListener('change', () => renderTickets()); paginationEl.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:0.5rem;padding:0.75rem 0;flex-wrap:wrap;';
document.getElementById('ticket-show-mine').addEventListener('change', () => renderTickets()); const table = document.getElementById('tickets-table');
table.parentNode.insertBefore(paginationEl, table.nextSibling);
}
if (totalPages <= 1) {
paginationEl.innerHTML = totalFiltered > 0 ? `<span style="color:#888;font-size:0.85rem;">${totalFiltered} tikettiä</span>` : '';
return;
}
let html = '';
// Edellinen-nappi
html += `<button class="btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.85rem;" ${ticketPage <= 1 ? 'disabled' : ''} onclick="ticketPage=1;renderTickets()">⟪</button>`;
html += `<button class="btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.85rem;" ${ticketPage <= 1 ? 'disabled' : ''} onclick="ticketPage--;renderTickets()">← Edellinen</button>`;
// Sivunumerot
const maxShow = 5;
let startPage = Math.max(1, ticketPage - Math.floor(maxShow / 2));
let endPage = Math.min(totalPages, startPage + maxShow - 1);
if (endPage - startPage < maxShow - 1) startPage = Math.max(1, endPage - maxShow + 1);
if (startPage > 1) html += `<span style="color:#888;">...</span>`;
for (let p = startPage; p <= endPage; p++) {
if (p === ticketPage) {
html += `<button style="padding:0.3rem 0.6rem;font-size:0.85rem;background:#0f3460;color:white;border:none;border-radius:6px;font-weight:600;">${p}</button>`;
} else {
html += `<button class="btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.85rem;" onclick="ticketPage=${p};renderTickets()">${p}</button>`;
}
}
if (endPage < totalPages) html += `<span style="color:#888;">...</span>`;
// Seuraava-nappi
html += `<button class="btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.85rem;" ${ticketPage >= totalPages ? 'disabled' : ''} onclick="ticketPage++;renderTickets()">Seuraava →</button>`;
html += `<button class="btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.85rem;" ${ticketPage >= totalPages ? 'disabled' : ''} onclick="ticketPage=${totalPages};renderTickets()">⟫</button>`;
// Näytetään sivuinfo
const startNum = (ticketPage - 1) * TICKETS_PER_PAGE + 1;
const endNum = Math.min(ticketPage * TICKETS_PER_PAGE, totalFiltered);
html += `<span style="color:#888;font-size:0.85rem;margin-left:0.5rem;">${startNum}${endNum} / ${totalFiltered}</span>`;
paginationEl.innerHTML = html;
document.getElementById('ticket-search-input').addEventListener('input', () => { ticketPage = 1; renderTickets(); });
document.getElementById('ticket-status-filter').addEventListener('change', () => { ticketPage = 1; renderTickets(); });
document.getElementById('ticket-type-filter').addEventListener('change', () => { ticketPage = 1; renderTickets(); });
document.getElementById('ticket-tag-filter').addEventListener('input', () => { ticketPage = 1; renderTickets(); });
document.getElementById('ticket-sort').addEventListener('change', () => { ticketPage = 1; renderTickets(); });
document.getElementById('ticket-show-closed').addEventListener('change', () => { ticketPage = 1; renderTickets(); });
document.getElementById('ticket-show-mine').addEventListener('change', () => { ticketPage = 1; renderTickets(); });
document.getElementById('bulk-select-all').addEventListener('change', function() { document.getElementById('bulk-select-all').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.ticket-checkbox'); const checkboxes = document.querySelectorAll('.ticket-checkbox');
checkboxes.forEach(cb => { checkboxes.forEach(cb => {
@@ -1496,6 +1562,11 @@ document.getElementById('bulk-select-all').addEventListener('change', function()
else bulkSelectedIds.delete(cb.dataset.ticketId); else bulkSelectedIds.delete(cb.dataset.ticketId);
}); });
updateBulkToolbar(); updateBulkToolbar();
// Näytä montako valittu kaikista sivuilta
const allCheckbox = document.getElementById('bulk-select-all');
if (bulkSelectedIds.size > checkboxes.length) {
allCheckbox.title = `${bulkSelectedIds.size} tikettiä valittu (myös muilta sivuilta)`;
}
}); });
document.getElementById('tickets-tbody').addEventListener('click', (e) => { document.getElementById('tickets-tbody').addEventListener('click', (e) => {
@@ -2268,13 +2339,30 @@ async function initTicketSettings() {
).join(''); ).join('');
// Postilaatikoiden näkyvyys — checkbox per postilaatikko // Postilaatikoiden näkyvyys — checkbox per postilaatikko
visContainer.innerHTML = mailboxes.map(mb => { let visHtml = mailboxes.map(mb => {
const isHidden = currentHiddenMailboxes.includes(String(mb.id)) || currentHiddenMailboxes.includes(mb.id); const isHidden = currentHiddenMailboxes.includes(String(mb.id)) || currentHiddenMailboxes.includes(mb.id);
return `<label style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;font-size:0.9rem;cursor:pointer;"> return `<label style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" class="mb-visibility-cb" data-mailbox-id="${mb.id}" ${!isHidden ? 'checked' : ''}> <input type="checkbox" class="mb-visibility-cb" data-mailbox-id="${mb.id}" ${!isHidden ? 'checked' : ''}>
<span>${esc(mb.company_nimi)}${esc(mb.nimi)} &lt;${esc(mb.smtp_from_email)}&gt;</span> <span>${esc(mb.company_nimi)}${esc(mb.nimi)} &lt;${esc(mb.smtp_from_email)}&gt;</span>
</label>`; </label>`;
}).join(''); }).join('');
// Zammad-ryhmät näkyvyyteen (haetaan tiketeistä)
try {
const zammadGroups = [...new Set(tickets.filter(t => t.source === 'zammad' && t.zammad_group).map(t => t.zammad_group))].sort();
if (zammadGroups.length > 0) {
visHtml += '<div style="margin-top:0.75rem;padding-top:0.75rem;border-top:1px solid #eee;"><strong style="font-size:0.85rem;color:#666;">Zammad-ryhmät</strong></div>';
zammadGroups.forEach(grp => {
const key = 'zammad_group:' + grp;
const isHidden = currentHiddenMailboxes.includes(key);
visHtml += `<label style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" class="mb-visibility-cb" data-mailbox-id="${esc(key)}" ${!isHidden ? 'checked' : ''}>
<span>${esc(grp)} <span style="color:#888;font-size:0.8rem;">(Zammad)</span></span>
</label>`;
});
}
} catch (e) {}
visContainer.innerHTML = visHtml;
} catch (e) { } catch (e) {
sigContainer.innerHTML = '<p style="color:red;font-size:0.85rem;">Virhe ladattaessa postilaatikoita.</p>'; sigContainer.innerHTML = '<p style="color:red;font-size:0.85rem;">Virhe ladattaessa postilaatikoita.</p>';
} }