diff --git a/api.php b/api.php index cf7e036..6b1158d 100644 --- a/api.php +++ b/api.php @@ -105,6 +105,22 @@ function requireCompany(): string { return $companyId; } +// Kuten requireCompany(), mutta sallii company_id:n overriden GET-parametrista +// Käytetään tiketti-endpointeissa jotta toisen yrityksen tikettejä voi avata +function requireCompanyOrParam(): string { + $paramCompany = $_GET['company_id'] ?? ''; + if (!empty($paramCompany)) { + $userCompanies = $_SESSION['companies'] ?? []; + if (!in_array($paramCompany, $userCompanies)) { + http_response_code(403); + echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']); + exit; + } + $_SESSION['company_id'] = $paramCompany; + } + return requireCompany(); +} + function companyFile(string $filename): string { return getCompanyDir() . '/' . $filename; } @@ -1663,62 +1679,86 @@ switch ($action) { // ---------- TICKETS ---------- case 'tickets': requireAuth(); - requireCompany(); - $tickets = loadTickets(); - // Palauta ilman viestisisältöjä (lista-näkymä) - // Auto-close tarkistus: sulje tiketit joiden auto_close_at on ohitettu - $now = date('Y-m-d H:i:s'); - $autoCloseCount = 0; - foreach ($tickets as &$tc) { - if (!empty($tc['auto_close_at']) && $tc['auto_close_at'] <= $now && !in_array($tc['status'], ['suljettu'])) { - $tc['status'] = 'suljettu'; - $tc['updated'] = $now; - $autoCloseCount++; + $allCompanies = !empty($_GET['all']); + $userCompanyIds = $_SESSION['companies'] ?? []; + + // Kerää yritykset joista haetaan + $companiesToQuery = []; + if ($allCompanies && count($userCompanyIds) > 1) { + $allComps = loadCompanies(); + foreach ($allComps as $c) { + if (in_array($c['id'], $userCompanyIds)) { + $companiesToQuery[] = $c; + } + } + } else { + requireCompany(); + $companiesToQuery[] = ['id' => $_SESSION['company_id'], 'nimi' => '']; + } + + $list = []; + foreach ($companiesToQuery as $comp) { + $cDir = DATA_DIR . '/companies/' . $comp['id']; + $ticketsFile = $cDir . '/tickets.json'; + if (!file_exists($ticketsFile)) continue; + $tickets = json_decode(file_get_contents($ticketsFile), true) ?: []; + + // Auto-close tarkistus + $now = date('Y-m-d H:i:s'); + $autoCloseCount = 0; + foreach ($tickets as &$tc) { + if (!empty($tc['auto_close_at']) && $tc['auto_close_at'] <= $now && !in_array($tc['status'], ['suljettu'])) { + $tc['status'] = 'suljettu'; + $tc['updated'] = $now; + $autoCloseCount++; + } + } + unset($tc); + if ($autoCloseCount > 0) { + file_put_contents($ticketsFile, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + + // Resolve mailbox names for this company + $confFile = $cDir . '/config.json'; + $companyConf = file_exists($confFile) ? (json_decode(file_get_contents($confFile), true) ?: []) : []; + $mailboxNames = []; + foreach ($companyConf['mailboxes'] ?? [] as $mb) { + $mailboxNames[$mb['id']] = $mb['nimi']; + } + + foreach ($tickets as $t) { + $msgCount = count($t['messages'] ?? []); + $lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null; + $list[] = [ + 'id' => $t['id'], + 'subject' => $t['subject'], + 'from_email' => $t['from_email'], + 'from_name' => $t['from_name'], + 'status' => $t['status'], + 'type' => $t['type'] ?? 'muu', + 'assigned_to' => $t['assigned_to'] ?? '', + 'customer_id' => $t['customer_id'] ?? '', + 'customer_name' => $t['customer_name'] ?? '', + 'tags' => $t['tags'] ?? [], + 'auto_close_at' => $t['auto_close_at'] ?? '', + 'mailbox_id' => $t['mailbox_id'] ?? '', + 'mailbox_name' => $mailboxNames[$t['mailbox_id'] ?? ''] ?? '', + 'company_id' => $comp['id'], + 'company_name' => $comp['nimi'] ?? '', + 'created' => $t['created'], + 'updated' => $t['updated'], + 'message_count' => $msgCount, + 'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '', + 'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '', + ]; } } - unset($tc); - if ($autoCloseCount > 0) { - saveTickets($tickets); - addLog('ticket_auto_close', '', '', "Automaattisulku: $autoCloseCount tikettiä"); - } - - // Resolve mailbox names - $companyConf = loadCompanyConfig(); - $mailboxNames = []; - foreach ($companyConf['mailboxes'] ?? [] as $mb) { - $mailboxNames[$mb['id']] = $mb['nimi']; - } - - $list = array_map(function($t) use ($mailboxNames) { - $msgCount = count($t['messages'] ?? []); - $lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null; - return [ - 'id' => $t['id'], - 'subject' => $t['subject'], - 'from_email' => $t['from_email'], - 'from_name' => $t['from_name'], - 'status' => $t['status'], - 'type' => $t['type'] ?? 'muu', - 'assigned_to' => $t['assigned_to'] ?? '', - 'customer_id' => $t['customer_id'] ?? '', - 'customer_name' => $t['customer_name'] ?? '', - 'tags' => $t['tags'] ?? [], - 'auto_close_at' => $t['auto_close_at'] ?? '', - 'mailbox_id' => $t['mailbox_id'] ?? '', - 'mailbox_name' => $mailboxNames[$t['mailbox_id'] ?? ''] ?? '', - 'created' => $t['created'], - 'updated' => $t['updated'], - 'message_count' => $msgCount, - 'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '', - 'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '', - ]; - }, $tickets); echo json_encode($list); break; case 'ticket_detail': requireAuth(); - requireCompany(); + requireCompanyOrParam(); $id = $_GET['id'] ?? ''; $tickets = loadTickets(); $ticket = null; @@ -1880,7 +1920,7 @@ switch ($action) { case 'ticket_reply': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -1955,7 +1995,7 @@ switch ($action) { case 'ticket_status': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -1990,7 +2030,7 @@ switch ($action) { case 'ticket_type': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -2025,7 +2065,7 @@ switch ($action) { case 'ticket_customer': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -2055,7 +2095,7 @@ switch ($action) { case 'ticket_assign': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -2083,7 +2123,7 @@ switch ($action) { case 'ticket_note': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -2125,7 +2165,7 @@ switch ($action) { case 'ticket_delete': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; @@ -2142,7 +2182,7 @@ switch ($action) { case 'ticket_tags': requireAuth(); - requireCompany(); + requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; diff --git a/index.html b/index.html index 2f0d6af..61381ed 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,7 @@
+
@@ -78,7 +79,6 @@ - diff --git a/script.js b/script.js index fc7cdec..e7d0f2d 100644 --- a/script.js +++ b/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 ` + const companyBadge = multiCompany && t.company_name ? `${esc(t.company_name)} ` : ''; + return ` ${ticketStatusLabels[t.status] || t.status} ${typeLabel} - ${esc(t.subject)} + ${companyBadge}${esc(t.subject)} ${esc(t.mailbox_name || t.from_name || t.from_email)} ${t.customer_name ? esc(t.customer_name) : '-'} ${(t.tags || []).length > 0 ? (t.tags || []).map(tag => '#' + esc(tag) + '').join(' ') : '-'} @@ -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 {