Add deep linking to tickets for easy sharing

Tickets now get a unique URL hash (#support/ticket/ID) when opened.
Copy link button in ticket header copies the full URL to clipboard.
Opening a ticket link navigates directly to the ticket detail view.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 12:20:12 +02:00
parent f8073a2350
commit e2f08304b3

View File

@@ -209,11 +209,14 @@ async function showDashboard() {
populateCompanySelector();
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
const hash = window.location.hash.replace('#', '');
const [mainHash, subHash] = hash.split('/');
const hashParts = hash.split('/');
const mainHash = hashParts[0];
const subHash = hashParts[1];
const extraHash = hashParts[2]; // esim. ticket ID: #support/ticket/123
const validTabs = ['customers', 'leads', 'tekniikka', 'ohjeet', 'todo', 'documents', 'laitetilat', 'netadmin', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
// ohjeet, laitetilat, archive ovat nyt sub-tabeja — switchToTab hoitaa uudelleenohjauksen
const startTab = validTabs.includes(mainHash) ? mainHash : 'support';
switchToTab(startTab, subHash);
switchToTab(startTab, subHash, extraHash);
}
function populateCompanySelector() {
@@ -258,7 +261,7 @@ async function switchCompany(companyId) {
// ==================== TABS ====================
function switchToTab(target, subTab) {
function switchToTab(target, subTab, extra) {
// Yhteensopivuus: vanhat hash-linkit → uusi rakenne
if (target === 'ohjeet') { target = 'support'; subTab = 'ohjeet'; }
if (target === 'archive') { target = 'customers'; subTab = 'archive'; }
@@ -299,10 +302,17 @@ function switchToTab(target, subTab) {
if (target === 'changelog') loadChangelog();
if (target === 'todo') { loadTodos(); if (subTab) switchTodoSubTab(subTab); }
if (target === 'support') {
loadTickets(); showTicketListView();
loadTickets();
if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh();
const supportSubMap = { ohjeet: 'support-ohjeet', saannot: 'support-saannot', vastauspohjat: 'support-vastauspohjat', asetukset: 'support-asetukset' };
switchSupportSubTab(supportSubMap[subTab] || 'support-tickets');
if (subTab === 'ticket' && extra) {
// Suora linkki tikettiin: #support/ticket/ID
switchSupportSubTab('support-tickets');
showTicketDetail(decodeURIComponent(extra));
} else {
showTicketListView();
const supportSubMap = { ohjeet: 'support-ohjeet', saannot: 'support-saannot', vastauspohjat: 'support-vastauspohjat', asetukset: 'support-asetukset' };
switchSupportSubTab(supportSubMap[subTab] || 'support-tickets');
}
}
if (target === 'documents') {
if (subTab && subTab !== 'kokoukset') {
@@ -1618,12 +1628,13 @@ async function showTicketDetail(id, companyId = '') {
const ticket = await apiCall('ticket_detail&id=' + encodeURIComponent(id) + ticketCompanyParam());
currentTicketId = id;
currentTicketData = ticket;
window.location.hash = 'support/ticket/' + encodeURIComponent(id);
// Header
document.getElementById('ticket-detail-header').innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:1.25rem;">
<div>
<h2 style="color:#0f3460;margin-bottom:0.25rem;font-size:1.2rem;">${ticket.ticket_number ? `<span style="color:#888;font-weight:normal;font-size:0.9rem;">#${ticket.ticket_number}</span> ` : ''}${esc(ticket.subject)}</h2>
<h2 style="color:#0f3460;margin-bottom:0.25rem;font-size:1.2rem;">${ticket.ticket_number ? `<span style="color:#888;font-weight:normal;font-size:0.9rem;">#${ticket.ticket_number}</span> ` : ''}${esc(ticket.subject)} <button id="btn-copy-ticket-link" title="Kopioi linkki" style="background:none;border:none;cursor:pointer;font-size:0.9rem;vertical-align:middle;opacity:0.5;">🔗</button></h2>
<div style="font-size:0.85rem;color:#888;" id="ticket-sender-line">
${esc(ticket.from_name)} &lt;${esc(ticket.from_email)}&gt; · Luotu ${esc(ticket.created)}
</div>
@@ -1783,6 +1794,15 @@ async function showTicketDetail(id, companyId = '') {
createTodoFromTicket(ticket);
});
// Kopioi linkki tikettiin
document.getElementById('btn-copy-ticket-link')?.addEventListener('click', () => {
const url = window.location.origin + window.location.pathname + '#support/ticket/' + encodeURIComponent(id);
navigator.clipboard.writeText(url).then(() => {
const btn = document.getElementById('btn-copy-ticket-link');
if (btn) { btn.textContent = '✓'; setTimeout(() => { btn.textContent = '🔗'; }, 1500); }
}).catch(() => { prompt('Kopioi linkki:', url); });
});
// Tags: add new tag on Enter
document.getElementById('ticket-tag-input').addEventListener('keydown', async (e) => {
if (e.key !== 'Enter') return;
@@ -1979,6 +1999,7 @@ function showTicketListView() {
document.getElementById('ticket-detail-view').style.display = 'none';
document.getElementById('ticket-list-view').style.display = 'block';
currentTicketId = null;
window.location.hash = 'support';
// Reset bulk selection
bulkSelectedIds.clear();
const selectAll = document.getElementById('bulk-select-all');