Add cross-company ticket viewing and move Yritykset button to header
- tickets endpoint supports ?all=1 to fetch from all user's companies - ticket_detail/reply/status/etc support ?company_id= for cross-company ops - Support tab shows all companies' tickets with company badge on subject - Yritykset button moved from tab bar to header (next to Käyttäjät) - requireCompanyOrParam() helper for ticket endpoints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
55
script.js
55
script.js
@@ -6,6 +6,7 @@ let currentDetailId = null;
|
||||
let currentUser = { username: '', nimi: '', role: '' };
|
||||
let currentCompany = null; // {id, nimi}
|
||||
let availableCompanies = []; // [{id, nimi}, ...]
|
||||
let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki)
|
||||
|
||||
// Elements
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
@@ -176,7 +177,7 @@ async function showDashboard() {
|
||||
// Näytä admin-toiminnot vain adminille
|
||||
document.getElementById('btn-users').style.display = currentUser.role === 'admin' ? '' : 'none';
|
||||
document.getElementById('tab-settings').style.display = currentUser.role === 'admin' ? '' : 'none';
|
||||
document.getElementById('tab-companies').style.display = currentUser.role === 'admin' ? '' : 'none';
|
||||
document.getElementById('btn-companies').style.display = currentUser.role === 'admin' ? '' : 'none';
|
||||
// Yritysvalitsin
|
||||
populateCompanySelector();
|
||||
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
||||
@@ -249,6 +250,14 @@ document.getElementById('btn-users').addEventListener('click', () => {
|
||||
loadUsers();
|
||||
});
|
||||
|
||||
document.getElementById('btn-companies').addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
document.getElementById('tab-content-companies').classList.add('active');
|
||||
window.location.hash = 'companies';
|
||||
loadCompaniesTab();
|
||||
});
|
||||
|
||||
// ==================== CUSTOMERS ====================
|
||||
|
||||
async function loadCustomers() {
|
||||
@@ -1031,7 +1040,9 @@ const ticketTypeLabels = {
|
||||
|
||||
async function loadTickets() {
|
||||
try {
|
||||
tickets = await apiCall('tickets');
|
||||
// Hae kaikkien yritysten tiketit jos useampi yritys
|
||||
const allParam = availableCompanies.length > 1 ? '&all=1' : '';
|
||||
tickets = await apiCall('tickets' + allParam);
|
||||
renderTickets();
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
@@ -1081,16 +1092,18 @@ function renderTickets() {
|
||||
} else {
|
||||
noTickets.style.display = 'none';
|
||||
document.getElementById('tickets-table').style.display = 'table';
|
||||
const multiCompany = availableCompanies.length > 1;
|
||||
ttbody.innerHTML = filtered.map(t => {
|
||||
const lastType = t.last_message_type === 'reply_out' ? '→' : (t.last_message_type === 'note' ? '📝' : '←');
|
||||
const typeLabel = ticketTypeLabels[t.type] || 'Muu';
|
||||
const rowClass = t.status === 'kasittelyssa' ? 'ticket-row-active' : '';
|
||||
const checked = bulkSelectedIds.has(t.id) ? 'checked' : '';
|
||||
return `<tr data-ticket-id="${t.id}" class="${rowClass}">
|
||||
const companyBadge = multiCompany && t.company_name ? `<span class="company-badge">${esc(t.company_name)}</span> ` : '';
|
||||
return `<tr data-ticket-id="${t.id}" data-company-id="${t.company_id || ''}" class="${rowClass}">
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="ticket-checkbox" data-ticket-id="${t.id}" ${checked}></td>
|
||||
<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>${companyBadge}<strong>${esc(t.subject)}</strong></td>
|
||||
<td>${esc(t.mailbox_name || t.from_name || t.from_email)}</td>
|
||||
<td>${t.customer_name ? esc(t.customer_name) : '<span style="color:#ccc;">-</span>'}</td>
|
||||
<td>${(t.tags || []).length > 0 ? (t.tags || []).map(tag => '<span class="ticket-tag">#' + esc(tag) + '</span>').join(' ') : '<span style="color:#ccc;">-</span>'}</td>
|
||||
@@ -1138,12 +1151,18 @@ document.getElementById('bulk-select-all').addEventListener('change', function()
|
||||
|
||||
document.getElementById('tickets-tbody').addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr');
|
||||
if (row && row.dataset.ticketId) showTicketDetail(row.dataset.ticketId);
|
||||
if (row && row.dataset.ticketId) showTicketDetail(row.dataset.ticketId, row.dataset.companyId || '');
|
||||
});
|
||||
|
||||
async function showTicketDetail(id) {
|
||||
// Helper: lisää company_id query parametri tiketti-kutsuihin
|
||||
function ticketCompanyParam() {
|
||||
return currentTicketCompanyId ? '&company_id=' + encodeURIComponent(currentTicketCompanyId) : '';
|
||||
}
|
||||
|
||||
async function showTicketDetail(id, companyId = '') {
|
||||
try {
|
||||
const ticket = await apiCall('ticket_detail&id=' + encodeURIComponent(id));
|
||||
currentTicketCompanyId = companyId;
|
||||
const ticket = await apiCall('ticket_detail&id=' + encodeURIComponent(id) + ticketCompanyParam());
|
||||
currentTicketId = id;
|
||||
|
||||
// Header
|
||||
@@ -1205,21 +1224,21 @@ async function showTicketDetail(id) {
|
||||
// Type change handler
|
||||
document.getElementById('ticket-type-select').addEventListener('change', async function() {
|
||||
try {
|
||||
await apiCall('ticket_type', 'POST', { id: currentTicketId, type: this.value });
|
||||
await apiCall('ticket_type' + ticketCompanyParam(), 'POST', { id: currentTicketId, type: this.value });
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
// Status change handler
|
||||
document.getElementById('ticket-status-select').addEventListener('change', async function() {
|
||||
try {
|
||||
await apiCall('ticket_status', 'POST', { id: currentTicketId, status: this.value });
|
||||
await apiCall('ticket_status' + ticketCompanyParam(), 'POST', { id: currentTicketId, status: this.value });
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
// Assign handler
|
||||
document.getElementById('ticket-assign-select').addEventListener('change', async function() {
|
||||
try {
|
||||
await apiCall('ticket_assign', 'POST', { id: currentTicketId, assigned_to: this.value });
|
||||
await apiCall('ticket_assign' + ticketCompanyParam(), 'POST', { id: currentTicketId, assigned_to: this.value });
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
@@ -1239,7 +1258,7 @@ async function showTicketDetail(id) {
|
||||
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 });
|
||||
await apiCall('ticket_customer' + ticketCompanyParam(), 'POST', { id: currentTicketId, customer_id: this.value, customer_name: custName });
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
@@ -1247,7 +1266,7 @@ async function showTicketDetail(id) {
|
||||
document.getElementById('btn-ticket-delete').addEventListener('click', async () => {
|
||||
if (!confirm('Poistetaanko tiketti "' + ticket.subject + '"?')) return;
|
||||
try {
|
||||
await apiCall('ticket_delete', 'POST', { id: currentTicketId });
|
||||
await apiCall('ticket_delete' + ticketCompanyParam(), 'POST', { id: currentTicketId });
|
||||
showTicketListView();
|
||||
loadTickets();
|
||||
} catch (e) { alert(e.message); }
|
||||
@@ -1264,8 +1283,8 @@ async function showTicketDetail(id) {
|
||||
if (!currentTags.includes(newTag)) currentTags.push(newTag);
|
||||
input.value = '';
|
||||
try {
|
||||
await apiCall('ticket_tags', 'POST', { id: currentTicketId, tags: currentTags });
|
||||
await showTicketDetail(currentTicketId);
|
||||
await apiCall('ticket_tags' + ticketCompanyParam(), 'POST', { id: currentTicketId, tags: currentTags });
|
||||
await showTicketDetail(currentTicketId, currentTicketCompanyId);
|
||||
} catch (e2) { alert(e2.message); }
|
||||
});
|
||||
|
||||
@@ -1277,8 +1296,8 @@ async function showTicketDetail(id) {
|
||||
const tagToRemove = tagEl.dataset.tag;
|
||||
const currentTags = (ticket.tags || []).filter(t => t !== tagToRemove);
|
||||
try {
|
||||
await apiCall('ticket_tags', 'POST', { id: currentTicketId, tags: currentTags });
|
||||
await showTicketDetail(currentTicketId);
|
||||
await apiCall('ticket_tags' + ticketCompanyParam(), 'POST', { id: currentTicketId, tags: currentTags });
|
||||
await showTicketDetail(currentTicketId, currentTicketCompanyId);
|
||||
} catch (e2) { alert(e2.message); }
|
||||
});
|
||||
});
|
||||
@@ -1362,9 +1381,9 @@ document.getElementById('btn-send-reply').addEventListener('click', async () =>
|
||||
|
||||
try {
|
||||
const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply';
|
||||
await apiCall(action, 'POST', { id: currentTicketId, body });
|
||||
await apiCall(action + ticketCompanyParam(), 'POST', { id: currentTicketId, body });
|
||||
// Reload the detail view
|
||||
await showTicketDetail(currentTicketId);
|
||||
await showTicketDetail(currentTicketId, currentTicketCompanyId);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user