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);
|
saveTickets($tickets);
|
||||||
break;
|
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':
|
case 'ticket_assign':
|
||||||
requireAuth();
|
requireAuth();
|
||||||
if ($method !== 'POST') break;
|
if ($method !== 'POST') break;
|
||||||
|
|||||||
@@ -255,14 +255,15 @@
|
|||||||
<option value="muu">Muu</option>
|
<option value="muu">Muu</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="ticket-status-filter" style="padding:9px 12px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.88rem;">
|
<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="uusi">Uusi</option>
|
||||||
<option value="kasittelyssa">Käsittelyssä</option>
|
<option value="kasittelyssa">Käsittelyssä</option>
|
||||||
<option value="odottaa">Odottaa vastausta</option>
|
<option value="odottaa">Odottaa vastausta</option>
|
||||||
<option value="ratkaistu">Ratkaistu</option>
|
<option value="ratkaistu">Ratkaistu</option>
|
||||||
<option value="suljettu">Suljettu</option>
|
|
||||||
<option value="kaikki">Kaikki tilat</option>
|
|
||||||
</select>
|
</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>
|
||||||
<div id="ticket-fetch-status" style="display:none;padding:0.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:0.9rem;"></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">
|
<div class="table-card">
|
||||||
@@ -273,8 +274,8 @@
|
|||||||
<th>Tyyppi</th>
|
<th>Tyyppi</th>
|
||||||
<th>Aihe</th>
|
<th>Aihe</th>
|
||||||
<th>Lähettäjä</th>
|
<th>Lähettäjä</th>
|
||||||
|
<th>Asiakas</th>
|
||||||
<th>Viestejä</th>
|
<th>Viestejä</th>
|
||||||
<th>Osoitettu</th>
|
|
||||||
<th>Päivitetty</th>
|
<th>Päivitetty</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
38
script.js
38
script.js
@@ -845,6 +845,8 @@ const actionLabels = {
|
|||||||
ticket_assign: 'Osoitti tiketin',
|
ticket_assign: 'Osoitti tiketin',
|
||||||
ticket_note: 'Lisäsi muistiinpanon',
|
ticket_note: 'Lisäsi muistiinpanon',
|
||||||
ticket_delete: 'Poisti tiketin',
|
ticket_delete: 'Poisti tiketin',
|
||||||
|
ticket_customer: 'Linkitti tiketin asiakkaaseen',
|
||||||
|
ticket_type: 'Muutti tiketin tyyppiä',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadChangelog() {
|
async function loadChangelog() {
|
||||||
@@ -976,14 +978,18 @@ function renderTickets() {
|
|||||||
const query = document.getElementById('ticket-search-input').value.toLowerCase().trim();
|
const query = document.getElementById('ticket-search-input').value.toLowerCase().trim();
|
||||||
const statusFilter = document.getElementById('ticket-status-filter').value;
|
const statusFilter = document.getElementById('ticket-status-filter').value;
|
||||||
const typeFilter = document.getElementById('ticket-type-filter').value;
|
const typeFilter = document.getElementById('ticket-type-filter').value;
|
||||||
|
const showClosed = document.getElementById('ticket-show-closed').checked;
|
||||||
let filtered = tickets;
|
let filtered = tickets;
|
||||||
|
|
||||||
// Default: hide closed tickets unless explicitly selected or "kaikki"
|
// Suljetut näkyvät vain kun täppä on päällä
|
||||||
if (!statusFilter || statusFilter === '') {
|
if (showClosed) {
|
||||||
|
filtered = filtered.filter(t => t.status === 'suljettu');
|
||||||
|
} else {
|
||||||
filtered = filtered.filter(t => t.status !== 'suljettu');
|
filtered = filtered.filter(t => t.status !== 'suljettu');
|
||||||
} else if (statusFilter !== 'kaikki') {
|
if (statusFilter) {
|
||||||
filtered = filtered.filter(t => t.status === statusFilter);
|
filtered = filtered.filter(t => t.status === statusFilter);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeFilter) {
|
if (typeFilter) {
|
||||||
filtered = filtered.filter(t => (t.type || 'muu') === typeFilter);
|
filtered = filtered.filter(t => (t.type || 'muu') === typeFilter);
|
||||||
@@ -1015,8 +1021,8 @@ function renderTickets() {
|
|||||||
<td><span class="ticket-type ticket-type-${t.type || 'muu'}">${typeLabel}</span></td>
|
<td><span class="ticket-type ticket-type-${t.type || 'muu'}">${typeLabel}</span></td>
|
||||||
<td><strong>${esc(t.subject)}</strong></td>
|
<td><strong>${esc(t.subject)}</strong></td>
|
||||||
<td>${esc(t.from_name || t.from_email)}</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 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>
|
<td class="nowrap">${esc((t.updated || '').substring(0, 16))}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -1038,6 +1044,7 @@ function renderTickets() {
|
|||||||
document.getElementById('ticket-search-input').addEventListener('input', () => renderTickets());
|
document.getElementById('ticket-search-input').addEventListener('input', () => renderTickets());
|
||||||
document.getElementById('ticket-status-filter').addEventListener('change', () => renderTickets());
|
document.getElementById('ticket-status-filter').addEventListener('change', () => renderTickets());
|
||||||
document.getElementById('ticket-type-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) => {
|
document.getElementById('tickets-tbody').addEventListener('click', (e) => {
|
||||||
const row = e.target.closest('tr');
|
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;">
|
<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>
|
<option value="">Ei osoitettu</option>
|
||||||
</select>
|
</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>
|
<button class="btn-danger" id="btn-ticket-delete" style="padding:6px 12px;font-size:0.82rem;">Poista</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -1113,6 +1123,26 @@ async function showTicketDetail(id) {
|
|||||||
} catch (e) { alert(e.message); }
|
} 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
|
// Delete handler
|
||||||
document.getElementById('btn-ticket-delete').addEventListener('click', async () => {
|
document.getElementById('btn-ticket-delete').addEventListener('click', async () => {
|
||||||
if (!confirm('Poistetaanko tiketti "' + ticket.subject + '"?')) return;
|
if (!confirm('Poistetaanko tiketti "' + ticket.subject + '"?')) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user