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:
29
api.php
29
api.php
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
40
script.js
40
script.js
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user