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 {