Add ticket types, move Asiakaspalvelu tab first, hide closed tickets

- Asiakaspalvelu tab moved to first position in navigation
- Added ticket type field (Laskutus, Tekniikka, Vika, Muu) with
  type filter dropdown and type column in ticket list
- Type selector in ticket detail view with API endpoint
- Closed tickets hidden by default (selectable via "Kaikki tilat")
- Käsittelyssä rows highlighted with green background
- Type badges with color coding per category

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 10:01:29 +02:00
parent f0a7676451
commit 91930c9420
4 changed files with 126 additions and 8 deletions

View File

@@ -958,6 +958,13 @@ const ticketStatusLabels = {
suljettu: 'Suljettu',
};
const ticketTypeLabels = {
laskutus: 'Laskutus',
tekniikka: 'Tekniikka',
vika: 'Vika',
muu: 'Muu',
};
async function loadTickets() {
try {
tickets = await apiCall('tickets');
@@ -968,7 +975,20 @@ async function loadTickets() {
function renderTickets() {
const query = document.getElementById('ticket-search-input').value.toLowerCase().trim();
const statusFilter = document.getElementById('ticket-status-filter').value;
const typeFilter = document.getElementById('ticket-type-filter').value;
let filtered = tickets;
// Default: hide closed tickets unless explicitly selected or "kaikki"
if (!statusFilter || statusFilter === '') {
filtered = filtered.filter(t => t.status !== 'suljettu');
} else if (statusFilter !== 'kaikki') {
filtered = filtered.filter(t => t.status === statusFilter);
}
if (typeFilter) {
filtered = filtered.filter(t => (t.type || 'muu') === typeFilter);
}
if (query) {
filtered = filtered.filter(t =>
(t.subject || '').toLowerCase().includes(query) ||
@@ -976,9 +996,6 @@ function renderTickets() {
(t.from_email || '').toLowerCase().includes(query)
);
}
if (statusFilter) {
filtered = filtered.filter(t => t.status === statusFilter);
}
const ttbody = document.getElementById('tickets-tbody');
const noTickets = document.getElementById('no-tickets');
@@ -991,8 +1008,11 @@ function renderTickets() {
document.getElementById('tickets-table').style.display = 'table';
ttbody.innerHTML = filtered.map(t => {
const lastType = t.last_message_type === 'reply_out' ? '&#8594;' : (t.last_message_type === 'note' ? '&#128221;' : '&#8592;');
return `<tr data-ticket-id="${t.id}">
const typeLabel = ticketTypeLabels[t.type] || 'Muu';
const rowClass = t.status === 'kasittelyssa' ? 'ticket-row-active' : '';
return `<tr data-ticket-id="${t.id}" class="${rowClass}">
<td><span class="ticket-status ticket-status-${t.status}">${ticketStatusLabels[t.status] || t.status}</span></td>
<td><span class="ticket-type ticket-type-${t.type || 'muu'}">${typeLabel}</span></td>
<td><strong>${esc(t.subject)}</strong></td>
<td>${esc(t.from_name || t.from_email)}</td>
<td style="text-align:center;">${lastType} ${t.message_count}</td>
@@ -1001,7 +1021,9 @@ function renderTickets() {
</tr>`;
}).join('');
}
document.getElementById('ticket-count').textContent = `${tickets.length} tikettiä`;
const openCount = tickets.filter(t => t.status !== 'suljettu').length;
document.getElementById('ticket-count').textContent = `${openCount} avointa tikettiä (${tickets.length} yht.)`;
// Status summary
const counts = {};
@@ -1015,6 +1037,7 @@ function renderTickets() {
document.getElementById('ticket-search-input').addEventListener('input', () => renderTickets());
document.getElementById('ticket-status-filter').addEventListener('change', () => renderTickets());
document.getElementById('ticket-type-filter').addEventListener('change', () => renderTickets());
document.getElementById('tickets-tbody').addEventListener('click', (e) => {
const row = e.target.closest('tr');
@@ -1036,6 +1059,12 @@ async function showTicketDetail(id) {
</div>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<select id="ticket-type-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="muu" ${(ticket.type || 'muu') === 'muu' ? 'selected' : ''}>Muu</option>
<option value="laskutus" ${ticket.type === 'laskutus' ? 'selected' : ''}>Laskutus</option>
<option value="tekniikka" ${ticket.type === 'tekniikka' ? 'selected' : ''}>Tekniikka</option>
<option value="vika" ${ticket.type === 'vika' ? 'selected' : ''}>Vika</option>
</select>
<select id="ticket-status-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="uusi" ${ticket.status === 'uusi' ? 'selected' : ''}>Uusi</option>
<option value="kasittelyssa" ${ticket.status === 'kasittelyssa' ? 'selected' : ''}>Käsittelyssä</option>
@@ -1063,6 +1092,13 @@ async function showTicketDetail(id) {
});
} catch (e) { /* non-admin may not access users */ }
// Type change handler
document.getElementById('ticket-type-select').addEventListener('change', async function() {
try {
await apiCall('ticket_type', 'POST', { id: currentTicketId, type: this.value });
} catch (e) { alert(e.message); }
});
// Status change handler
document.getElementById('ticket-status-select').addEventListener('change', async function() {
try {