Add closed tickets checkbox, customer linking for tickets

- Closed tickets completely hidden from default view, separate
  "Suljetut" checkbox to toggle them with search capability
- Removed "Osoitettu" column, added "Asiakas" column to ticket list
- Customer dropdown in ticket detail view to link ticket to a customer
- ticket_customer API endpoint for linking tickets to customers
- ticket_type changelog label added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 10:06:38 +02:00
parent 91930c9420
commit ea7c3e0cf7
3 changed files with 69 additions and 9 deletions

29
api.php
View File

@@ -1689,6 +1689,35 @@ switch ($action) {
saveTickets($tickets);
break;
case 'ticket_customer':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customerId = $input['customer_id'] ?? '';
$customerName = $input['customer_name'] ?? '';
$tickets = loadTickets();
$found = false;
foreach ($tickets as &$t) {
if ($t['id'] === $id) {
$t['customer_id'] = $customerId;
$t['customer_name'] = $customerName;
$t['updated'] = date('Y-m-d H:i:s');
$found = true;
addLog('ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}");
echo json_encode($t);
break;
}
}
unset($t);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Tikettiä ei löydy']);
break;
}
saveTickets($tickets);
break;
case 'ticket_assign':
requireAuth();
if ($method !== 'POST') break;

View File

@@ -255,14 +255,15 @@
<option value="muu">Muu</option>
</select>
<select id="ticket-status-filter" style="padding:9px 12px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.88rem;">
<option value="">Avoimet</option>
<option value="">Kaikki avoimet</option>
<option value="uusi">Uusi</option>
<option value="kasittelyssa">Käsittelyssä</option>
<option value="odottaa">Odottaa vastausta</option>
<option value="ratkaistu">Ratkaistu</option>
<option value="suljettu">Suljettu</option>
<option value="kaikki">Kaikki tilat</option>
</select>
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.85rem;color:#777;cursor:pointer;white-space:nowrap;">
<input type="checkbox" id="ticket-show-closed"> Suljetut
</label>
</div>
<div id="ticket-fetch-status" style="display:none;padding:0.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:0.9rem;"></div>
<div class="table-card">
@@ -273,8 +274,8 @@
<th>Tyyppi</th>
<th>Aihe</th>
<th>Lähettäjä</th>
<th>Asiakas</th>
<th>Viestejä</th>
<th>Osoitettu</th>
<th>Päivitetty</th>
</tr>
</thead>

View File

@@ -845,6 +845,8 @@ const actionLabels = {
ticket_assign: 'Osoitti tiketin',
ticket_note: 'Lisäsi muistiinpanon',
ticket_delete: 'Poisti tiketin',
ticket_customer: 'Linkitti tiketin asiakkaaseen',
ticket_type: 'Muutti tiketin tyyppiä',
};
async function loadChangelog() {
@@ -976,13 +978,17 @@ 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;
const showClosed = document.getElementById('ticket-show-closed').checked;
let filtered = tickets;
// Default: hide closed tickets unless explicitly selected or "kaikki"
if (!statusFilter || statusFilter === '') {
// Suljetut näkyvät vain kun täppä on päällä
if (showClosed) {
filtered = filtered.filter(t => t.status === 'suljettu');
} else {
filtered = filtered.filter(t => t.status !== 'suljettu');
} else if (statusFilter !== 'kaikki') {
filtered = filtered.filter(t => t.status === statusFilter);
if (statusFilter) {
filtered = filtered.filter(t => t.status === statusFilter);
}
}
if (typeFilter) {
@@ -1015,8 +1021,8 @@ function renderTickets() {
<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>${t.customer_name ? esc(t.customer_name) : '<span style="color:#ccc;">-</span>'}</td>
<td style="text-align:center;">${lastType} ${t.message_count}</td>
<td>${esc(t.assigned_to || '-')}</td>
<td class="nowrap">${esc((t.updated || '').substring(0, 16))}</td>
</tr>`;
}).join('');
@@ -1038,6 +1044,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('ticket-show-closed').addEventListener('change', () => renderTickets());
document.getElementById('tickets-tbody').addEventListener('click', (e) => {
const row = e.target.closest('tr');
@@ -1075,6 +1082,9 @@ async function showTicketDetail(id) {
<select id="ticket-assign-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="">Ei osoitettu</option>
</select>
<select id="ticket-customer-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="">Ei asiakkuutta</option>
</select>
<button class="btn-danger" id="btn-ticket-delete" style="padding:6px 12px;font-size:0.82rem;">Poista</button>
</div>
</div>`;
@@ -1113,6 +1123,26 @@ async function showTicketDetail(id) {
} catch (e) { alert(e.message); }
});
// Customer link — load customers dropdown
try {
const custSelect = document.getElementById('ticket-customer-select');
customers.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.yritys;
if (c.id === ticket.customer_id) opt.selected = true;
custSelect.appendChild(opt);
});
} catch (e) {}
document.getElementById('ticket-customer-select').addEventListener('change', async function() {
const selOpt = this.options[this.selectedIndex];
const custName = this.value ? selOpt.textContent : '';
try {
await apiCall('ticket_customer', 'POST', { id: currentTicketId, customer_id: this.value, customer_name: custName });
} catch (e) { alert(e.message); }
});
// Delete handler
document.getElementById('btn-ticket-delete').addEventListener('click', async () => {
if (!confirm('Poistetaanko tiketti "' + ticket.subject + '"?')) return;