Add multi-tenant support with per-company data isolation
Implement full multi-company architecture:
- Per-company directory structure (data/companies/{id}/)
- Automatic migration from single-tenant to multi-tenant
- Company management admin tab (create, edit, delete companies)
- Per-company IMAP mailbox configuration (multiple mailboxes per company)
- User access control per company (companies array on users)
- Company switcher in header (shown when user has access to >1 company)
- Session-based company context with check_auth fallback for old sessions
- Ticket list shows mailbox name instead of sender
- IMAP settings moved from global config to company-specific config
- All data endpoints protected with requireCompany() guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
8
data/companies.json
Normal file
8
data/companies.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "cuitunet",
|
||||||
|
"nimi": "CuituNet",
|
||||||
|
"luotu": "2026-03-10 08:58:43",
|
||||||
|
"aktiivinen": true
|
||||||
|
}
|
||||||
|
]
|
||||||
4
data/companies/cuitunet/config.json
Normal file
4
data/companies/cuitunet/config.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"mailboxes": [],
|
||||||
|
"ticket_rules": []
|
||||||
|
}
|
||||||
143
index.html
143
index.html
@@ -61,6 +61,7 @@
|
|||||||
<span class="subtitle">Kuituasiakkaiden hallinta</span>
|
<span class="subtitle">Kuituasiakkaiden hallinta</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<select id="company-selector" class="company-selector" style="display:none;"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<span id="user-info" class="user-info"></span>
|
<span id="user-info" class="user-info"></span>
|
||||||
@@ -77,6 +78,7 @@
|
|||||||
<button class="tab" data-tab="leads">Liidit</button>
|
<button class="tab" data-tab="leads">Liidit</button>
|
||||||
<button class="tab" data-tab="archive">Arkisto</button>
|
<button class="tab" data-tab="archive">Arkisto</button>
|
||||||
<button class="tab" data-tab="changelog">Muutosloki</button>
|
<button class="tab" data-tab="changelog">Muutosloki</button>
|
||||||
|
<button class="tab" data-tab="companies" id="tab-companies" style="display:none">Yritykset</button>
|
||||||
<button class="tab" data-tab="settings" id="tab-settings" style="display:none">API</button>
|
<button class="tab" data-tab="settings" id="tab-settings" style="display:none">API</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -291,7 +293,7 @@
|
|||||||
<th>Tila</th>
|
<th>Tila</th>
|
||||||
<th>Tyyppi</th>
|
<th>Tyyppi</th>
|
||||||
<th>Aihe</th>
|
<th>Aihe</th>
|
||||||
<th>Lähettäjä</th>
|
<th>Postilaatikko</th>
|
||||||
<th>Asiakas</th>
|
<th>Asiakas</th>
|
||||||
<th>Tagit</th>
|
<th>Tagit</th>
|
||||||
<th>Viestejä</th>
|
<th>Viestejä</th>
|
||||||
@@ -449,34 +451,7 @@
|
|||||||
<button class="btn-primary" id="btn-save-settings">Tallenna asetukset</button>
|
<button class="btn-primary" id="btn-save-settings">Tallenna asetukset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 style="color:#0f3460;margin:1.5rem 0 1rem;border-bottom:2px solid #f0f2f5;padding-bottom:0.5rem;">Sähköposti (IMAP)</h3>
|
<p style="color:#888;font-size:0.85rem;margin-top:1rem;">Sähköpostiasetukset (IMAP/postilaatikot) hallitaan Yritykset-välilehdellä.</p>
|
||||||
<p style="color:#666;font-size:0.85rem;margin-bottom:1rem;">Asiakaspalvelu-sähköpostin IMAP-asetukset. Käytetään tikettien hakuun.</p>
|
|
||||||
<div class="form-grid" style="max-width:600px;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>IMAP-palvelin</label>
|
|
||||||
<input type="text" id="settings-imap-host" placeholder="mail.example.com">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Portti</label>
|
|
||||||
<input type="number" id="settings-imap-port" value="993" placeholder="993">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Käyttäjätunnus</label>
|
|
||||||
<input type="text" id="settings-imap-user" placeholder="asiakaspalvelu@cuitunet.fi">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Salasana</label>
|
|
||||||
<input type="password" id="settings-imap-password" placeholder="••••••••">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Salaus</label>
|
|
||||||
<select id="settings-imap-encryption">
|
|
||||||
<option value="ssl">SSL</option>
|
|
||||||
<option value="tls">TLS</option>
|
|
||||||
<option value="notls">Ei salausta</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 style="color:#0f3460;margin:1.5rem 0 1rem;border-bottom:2px solid #f0f2f5;padding-bottom:0.5rem;">API-ohjeet</h3>
|
<h3 style="color:#0f3460;margin:1.5rem 0 1rem;border-bottom:2px solid #f0f2f5;padding-bottom:0.5rem;">API-ohjeet</h3>
|
||||||
<div style="background:#f8f9fb;padding:1rem;border-radius:8px;font-size:0.85rem;font-family:monospace;overflow-x:auto;">
|
<div style="background:#f8f9fb;padding:1rem;border-radius:8px;font-size:0.85rem;font-family:monospace;overflow-x:auto;">
|
||||||
<div style="margin-bottom:0.75rem;"><strong>Endpoint:</strong><br>GET https://intra.cuitunet.fi/api.php?action=saatavuus</div>
|
<div style="margin-bottom:0.75rem;"><strong>Endpoint:</strong><br>GET https://intra.cuitunet.fi/api.php?action=saatavuus</div>
|
||||||
@@ -505,6 +480,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab: Yritykset (vain admin) -->
|
||||||
|
<div class="tab-content" id="tab-content-companies">
|
||||||
|
<div class="main-container">
|
||||||
|
<!-- Yrityslistanäkymä -->
|
||||||
|
<div id="companies-list-view">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||||
|
<h3 style="color:#0f3460;margin:0;">Yritykset</h3>
|
||||||
|
<button class="btn-primary" id="btn-add-company">+ Lisää yritys</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-card">
|
||||||
|
<table id="companies-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nimi</th>
|
||||||
|
<th>Postilaatikot</th>
|
||||||
|
<th>Luotu</th>
|
||||||
|
<th>Tila</th>
|
||||||
|
<th>Toiminnot</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="companies-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Yrityksen asetukset -->
|
||||||
|
<div id="company-detail-view" style="display:none;">
|
||||||
|
<button class="btn-secondary" id="btn-company-back" style="color:#555;border-color:#ddd;margin-bottom:1rem;">← Takaisin yrityslistaan</button>
|
||||||
|
<div class="table-card" style="padding:1.5rem;">
|
||||||
|
<h3 style="color:#0f3460;margin-bottom:0.5rem;" id="company-detail-title">Yrityksen asetukset</h3>
|
||||||
|
<div class="form-grid" style="max-width:400px;margin-bottom:1.5rem;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Yrityksen nimi</label>
|
||||||
|
<input type="text" id="company-edit-nimi">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn-primary" id="btn-save-company-name" style="font-size:0.85rem;">Tallenna nimi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Postilaatikot -->
|
||||||
|
<div class="table-card" style="padding:1.5rem;margin-top:1rem;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||||
|
<h3 style="color:#0f3460;margin:0;">Postilaatikot (IMAP)</h3>
|
||||||
|
<button class="btn-primary" id="btn-add-mailbox" style="font-size:0.85rem;">+ Lisää postilaatikko</button>
|
||||||
|
</div>
|
||||||
|
<p style="color:#888;font-size:0.85rem;margin-bottom:1rem;">Sähköpostilaatikot joista asiakaspalvelutiketit haetaan.</p>
|
||||||
|
<div id="mailboxes-list"></div>
|
||||||
|
<!-- Postilaatikkolom -->
|
||||||
|
<div id="mailbox-form-container" style="display:none;margin-top:1rem;padding:1rem;background:#f8f9fb;border-radius:8px;">
|
||||||
|
<h4 style="color:#0f3460;margin-bottom:0.75rem;" id="mailbox-form-title">Uusi postilaatikko</h4>
|
||||||
|
<input type="hidden" id="mailbox-form-id">
|
||||||
|
<div class="form-grid" style="max-width:600px;">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>Nimi (näkyy tikettilistassa) *</label>
|
||||||
|
<input type="text" id="mailbox-form-nimi" placeholder="esim. Cuitunet-asiakaspalvelu">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>IMAP-palvelin</label>
|
||||||
|
<input type="text" id="mailbox-form-host" placeholder="mail.example.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Portti</label>
|
||||||
|
<input type="number" id="mailbox-form-port" value="993" placeholder="993">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Käyttäjätunnus</label>
|
||||||
|
<input type="text" id="mailbox-form-user" placeholder="asiakaspalvelu@cuitunet.fi">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Salasana</label>
|
||||||
|
<input type="password" id="mailbox-form-password" placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Salaus</label>
|
||||||
|
<select id="mailbox-form-encryption">
|
||||||
|
<option value="ssl">SSL</option>
|
||||||
|
<option value="tls">TLS</option>
|
||||||
|
<option value="notls">Ei salausta</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Lähettäjän sähköposti</label>
|
||||||
|
<input type="text" id="mailbox-form-smtp-email" placeholder="asiakaspalvelu@cuitunet.fi">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Lähettäjän nimi</label>
|
||||||
|
<input type="text" id="mailbox-form-smtp-name" placeholder="CuituNet Asiakaspalvelu">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
|
||||||
|
<button class="btn-primary" id="btn-save-mailbox">Tallenna</button>
|
||||||
|
<button class="btn-secondary" id="btn-cancel-mailbox">Peruuta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Käyttäjäoikeudet -->
|
||||||
|
<div class="table-card" style="padding:1.5rem;margin-top:1rem;">
|
||||||
|
<h3 style="color:#0f3460;margin-bottom:0.5rem;">Käyttäjäoikeudet</h3>
|
||||||
|
<p style="color:#888;font-size:0.85rem;margin-bottom:1rem;">Käyttäjät joilla on pääsy tähän yritykseen.</p>
|
||||||
|
<div id="company-users-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>CuituNet Intra — Asiakashallintajärjestelmä</p>
|
<p>CuituNet Intra — Asiakashallintajärjestelmä</p>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -645,6 +726,10 @@
|
|||||||
<option value="admin">Ylläpitäjä</option>
|
<option value="admin">Ylläpitäjä</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>Yritysoikeudet</label>
|
||||||
|
<div id="user-company-checkboxes" style="display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.25rem;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn-primary">Tallenna</button>
|
<button type="submit" class="btn-primary">Tallenna</button>
|
||||||
|
|||||||
297
script.js
297
script.js
@@ -4,6 +4,8 @@ let sortField = 'yritys';
|
|||||||
let sortAsc = true;
|
let sortAsc = true;
|
||||||
let currentDetailId = null;
|
let currentDetailId = null;
|
||||||
let currentUser = { username: '', nimi: '', role: '' };
|
let currentUser = { username: '', nimi: '', role: '' };
|
||||||
|
let currentCompany = null; // {id, nimi}
|
||||||
|
let availableCompanies = []; // [{id, nimi}, ...]
|
||||||
|
|
||||||
// Elements
|
// Elements
|
||||||
const loginScreen = document.getElementById('login-screen');
|
const loginScreen = document.getElementById('login-screen');
|
||||||
@@ -129,6 +131,8 @@ async function checkAuth() {
|
|||||||
const data = await apiCall('check_auth');
|
const data = await apiCall('check_auth');
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
||||||
|
availableCompanies = data.companies || [];
|
||||||
|
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
|
||||||
showDashboard();
|
showDashboard();
|
||||||
}
|
}
|
||||||
} catch (e) { /* not logged in */ }
|
} catch (e) { /* not logged in */ }
|
||||||
@@ -143,6 +147,8 @@ loginForm.addEventListener('submit', async (e) => {
|
|||||||
const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) });
|
const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) });
|
||||||
loginError.style.display = 'none';
|
loginError.style.display = 'none';
|
||||||
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
||||||
|
availableCompanies = data.companies || [];
|
||||||
|
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
|
||||||
showDashboard();
|
showDashboard();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loginError.textContent = err.message;
|
loginError.textContent = err.message;
|
||||||
@@ -170,13 +176,38 @@ async function showDashboard() {
|
|||||||
// Näytä admin-toiminnot vain adminille
|
// Näytä admin-toiminnot vain adminille
|
||||||
document.getElementById('btn-users').style.display = currentUser.role === 'admin' ? '' : 'none';
|
document.getElementById('btn-users').style.display = currentUser.role === 'admin' ? '' : 'none';
|
||||||
document.getElementById('tab-settings').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';
|
||||||
|
// Yritysvalitsin
|
||||||
|
populateCompanySelector();
|
||||||
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
||||||
const hash = window.location.hash.replace('#', '');
|
const hash = window.location.hash.replace('#', '');
|
||||||
const validTabs = ['customers', 'leads', 'archive', 'changelog', 'support', 'users', 'settings'];
|
const validTabs = ['customers', 'leads', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
|
||||||
const startTab = validTabs.includes(hash) ? hash : 'customers';
|
const startTab = validTabs.includes(hash) ? hash : 'customers';
|
||||||
switchToTab(startTab);
|
switchToTab(startTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateCompanySelector() {
|
||||||
|
const sel = document.getElementById('company-selector');
|
||||||
|
if (availableCompanies.length <= 1) {
|
||||||
|
sel.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sel.style.display = '';
|
||||||
|
sel.innerHTML = availableCompanies.map(c =>
|
||||||
|
`<option value="${c.id}" ${currentCompany && c.id === currentCompany.id ? 'selected' : ''}>${esc(c.nimi)}</option>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchCompany(companyId) {
|
||||||
|
try {
|
||||||
|
await apiCall('company_switch', 'POST', { company_id: companyId });
|
||||||
|
currentCompany = availableCompanies.find(c => c.id === companyId) || null;
|
||||||
|
// Lataa uudelleen aktiivinen tab
|
||||||
|
const hash = window.location.hash.replace('#', '') || 'customers';
|
||||||
|
switchToTab(hash);
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== TABS ====================
|
// ==================== TABS ====================
|
||||||
|
|
||||||
function switchToTab(target) {
|
function switchToTab(target) {
|
||||||
@@ -196,6 +227,7 @@ function switchToTab(target) {
|
|||||||
if (target === 'support') { loadTickets(); showTicketListView(); }
|
if (target === 'support') { loadTickets(); showTicketListView(); }
|
||||||
if (target === 'users') loadUsers();
|
if (target === 'users') loadUsers();
|
||||||
if (target === 'settings') loadSettings();
|
if (target === 'settings') loadSettings();
|
||||||
|
if (target === 'companies') loadCompaniesTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.tab').forEach(tab => {
|
document.querySelectorAll('.tab').forEach(tab => {
|
||||||
@@ -915,6 +947,26 @@ function openUserForm(user = null) {
|
|||||||
document.getElementById('user-form-password').value = '';
|
document.getElementById('user-form-password').value = '';
|
||||||
document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*';
|
document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*';
|
||||||
document.getElementById('user-form-role').value = user ? user.role : 'user';
|
document.getElementById('user-form-role').value = user ? user.role : 'user';
|
||||||
|
// Yrityscheckboxit
|
||||||
|
const allComps = availableCompanies.length > 0 ? availableCompanies : [];
|
||||||
|
const userComps = user ? (user.companies || []) : [];
|
||||||
|
const container = document.getElementById('user-company-checkboxes');
|
||||||
|
// Hae kaikki yritykset admin-näkymää varten
|
||||||
|
apiCall('companies_all').then(companies => {
|
||||||
|
container.innerHTML = companies.map(c =>
|
||||||
|
`<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" class="user-company-cb" value="${c.id}" ${userComps.includes(c.id) ? 'checked' : ''}>
|
||||||
|
${esc(c.nimi)}
|
||||||
|
</label>`
|
||||||
|
).join('');
|
||||||
|
}).catch(() => {
|
||||||
|
container.innerHTML = allComps.map(c =>
|
||||||
|
`<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;">
|
||||||
|
<input type="checkbox" class="user-company-cb" value="${c.id}" ${userComps.includes(c.id) ? 'checked' : ''}>
|
||||||
|
${esc(c.nimi)}
|
||||||
|
</label>`
|
||||||
|
).join('');
|
||||||
|
});
|
||||||
userModal.style.display = 'flex';
|
userModal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,11 +989,13 @@ async function deleteUser(id, username) {
|
|||||||
document.getElementById('user-form').addEventListener('submit', async (e) => {
|
document.getElementById('user-form').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = document.getElementById('user-form-id').value;
|
const id = document.getElementById('user-form-id').value;
|
||||||
|
const companies = [...document.querySelectorAll('.user-company-cb:checked')].map(cb => cb.value);
|
||||||
const data = {
|
const data = {
|
||||||
username: document.getElementById('user-form-username').value,
|
username: document.getElementById('user-form-username').value,
|
||||||
nimi: document.getElementById('user-form-nimi').value,
|
nimi: document.getElementById('user-form-nimi').value,
|
||||||
email: document.getElementById('user-form-email').value,
|
email: document.getElementById('user-form-email').value,
|
||||||
role: document.getElementById('user-form-role').value,
|
role: document.getElementById('user-form-role').value,
|
||||||
|
companies,
|
||||||
};
|
};
|
||||||
const pw = document.getElementById('user-form-password').value;
|
const pw = document.getElementById('user-form-password').value;
|
||||||
if (pw) data.password = pw;
|
if (pw) data.password = pw;
|
||||||
@@ -1037,7 +1091,7 @@ function renderTickets() {
|
|||||||
<td><span class="ticket-status ticket-status-${t.status}">${ticketStatusLabels[t.status] || t.status}</span></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><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.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.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>
|
<td>${(t.tags || []).length > 0 ? (t.tags || []).map(tag => '<span class="ticket-tag">#' + esc(tag) + '</span>').join(' ') : '<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>
|
||||||
@@ -1556,12 +1610,6 @@ async function loadSettings() {
|
|||||||
document.getElementById('settings-cors').value = (config.cors_origins || ['https://cuitunet.fi', 'https://www.cuitunet.fi']).join('\n');
|
document.getElementById('settings-cors').value = (config.cors_origins || ['https://cuitunet.fi', 'https://www.cuitunet.fi']).join('\n');
|
||||||
const key = config.api_key || 'AVAIN';
|
const key = config.api_key || 'AVAIN';
|
||||||
document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`;
|
document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`;
|
||||||
// IMAP settings
|
|
||||||
document.getElementById('settings-imap-host').value = config.imap_host || '';
|
|
||||||
document.getElementById('settings-imap-port').value = config.imap_port || 993;
|
|
||||||
document.getElementById('settings-imap-user').value = config.imap_user || '';
|
|
||||||
document.getElementById('settings-imap-password').value = config.imap_password || '';
|
|
||||||
document.getElementById('settings-imap-encryption').value = config.imap_encryption || 'ssl';
|
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1578,11 +1626,6 @@ document.getElementById('btn-save-settings').addEventListener('click', async ()
|
|||||||
const config = await apiCall('config_update', 'POST', {
|
const config = await apiCall('config_update', 'POST', {
|
||||||
api_key: document.getElementById('settings-api-key').value,
|
api_key: document.getElementById('settings-api-key').value,
|
||||||
cors_origins: document.getElementById('settings-cors').value,
|
cors_origins: document.getElementById('settings-cors').value,
|
||||||
imap_host: document.getElementById('settings-imap-host').value,
|
|
||||||
imap_port: document.getElementById('settings-imap-port').value,
|
|
||||||
imap_user: document.getElementById('settings-imap-user').value,
|
|
||||||
imap_password: document.getElementById('settings-imap-password').value,
|
|
||||||
imap_encryption: document.getElementById('settings-imap-encryption').value,
|
|
||||||
});
|
});
|
||||||
alert('Asetukset tallennettu!');
|
alert('Asetukset tallennettu!');
|
||||||
} catch (e) { alert(e.message); }
|
} catch (e) { alert(e.message); }
|
||||||
@@ -1622,6 +1665,234 @@ document.addEventListener('keydown', (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== COMPANY SELECTOR ====================
|
||||||
|
|
||||||
|
document.getElementById('company-selector').addEventListener('change', function () {
|
||||||
|
switchCompany(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== YRITYKSET-TAB (admin) ====================
|
||||||
|
|
||||||
|
let companiesTabData = [];
|
||||||
|
let currentCompanyDetail = null;
|
||||||
|
|
||||||
|
async function loadCompaniesTab() {
|
||||||
|
try {
|
||||||
|
companiesTabData = await apiCall('companies_all');
|
||||||
|
renderCompaniesTable();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Fallback: käytä availableCompanies
|
||||||
|
companiesTabData = availableCompanies;
|
||||||
|
renderCompaniesTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCompaniesTable() {
|
||||||
|
const tbody = document.getElementById('companies-tbody');
|
||||||
|
tbody.innerHTML = companiesTabData.map(c => `<tr>
|
||||||
|
<td><code>${esc(c.id)}</code></td>
|
||||||
|
<td><strong>${esc(c.nimi)}</strong></td>
|
||||||
|
<td>-</td>
|
||||||
|
<td class="nowrap">${esc((c.luotu || '').substring(0, 10))}</td>
|
||||||
|
<td>${c.aktiivinen !== false ? '<span style="color:#22c55e;">Aktiivinen</span>' : '<span style="color:#888;">Ei aktiivinen</span>'}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-link" onclick="showCompanyDetail('${c.id}')">Asetukset</button>
|
||||||
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteCompany('${c.id}','${esc(c.nimi)}')">Poista</button>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
document.getElementById('companies-list-view').style.display = '';
|
||||||
|
document.getElementById('company-detail-view').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-add-company').addEventListener('click', () => {
|
||||||
|
const nimi = prompt('Yrityksen nimi:');
|
||||||
|
if (!nimi) return;
|
||||||
|
const id = prompt('Yrityksen ID (pienillä kirjaimilla, a-z, 0-9, viiva sallittu):');
|
||||||
|
if (!id) return;
|
||||||
|
apiCall('company_create', 'POST', { id, nimi }).then(() => {
|
||||||
|
loadCompaniesTab();
|
||||||
|
// Päivitä myös company-selector
|
||||||
|
apiCall('check_auth').then(data => {
|
||||||
|
if (data.authenticated) {
|
||||||
|
availableCompanies = data.companies || [];
|
||||||
|
currentCompany = availableCompanies.find(c => c.id === data.company_id) || currentCompany;
|
||||||
|
populateCompanySelector();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(e => alert(e.message));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteCompany(id, nimi) {
|
||||||
|
if (!confirm(`Poistetaanko yritys "${nimi}"? Tämä poistaa pääsyn yrityksen dataan.`)) return;
|
||||||
|
try {
|
||||||
|
await apiCall('company_delete', 'POST', { id });
|
||||||
|
loadCompaniesTab();
|
||||||
|
// Päivitä selector
|
||||||
|
availableCompanies = availableCompanies.filter(c => c.id !== id);
|
||||||
|
if (currentCompany && currentCompany.id === id) {
|
||||||
|
currentCompany = availableCompanies[0] || null;
|
||||||
|
if (currentCompany) switchCompany(currentCompany.id);
|
||||||
|
}
|
||||||
|
populateCompanySelector();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showCompanyDetail(id) {
|
||||||
|
currentCompanyDetail = id;
|
||||||
|
document.getElementById('companies-list-view').style.display = 'none';
|
||||||
|
document.getElementById('company-detail-view').style.display = '';
|
||||||
|
const comp = companiesTabData.find(c => c.id === id);
|
||||||
|
document.getElementById('company-detail-title').textContent = (comp ? comp.nimi : id) + ' — Asetukset';
|
||||||
|
document.getElementById('company-edit-nimi').value = comp ? comp.nimi : '';
|
||||||
|
|
||||||
|
// Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein
|
||||||
|
await apiCall('company_switch', 'POST', { company_id: id });
|
||||||
|
|
||||||
|
// Lataa postilaatikot
|
||||||
|
loadMailboxes();
|
||||||
|
// Lataa käyttäjäoikeudet
|
||||||
|
loadCompanyUsers(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-company-back').addEventListener('click', () => {
|
||||||
|
// Vaihda takaisin alkuperäiseen yritykseen
|
||||||
|
if (currentCompany) apiCall('company_switch', 'POST', { company_id: currentCompany.id });
|
||||||
|
renderCompaniesTable();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-save-company-name').addEventListener('click', async () => {
|
||||||
|
const nimi = document.getElementById('company-edit-nimi').value.trim();
|
||||||
|
if (!nimi) return;
|
||||||
|
try {
|
||||||
|
await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi });
|
||||||
|
alert('Nimi tallennettu!');
|
||||||
|
// Päivitä paikalliset tiedot
|
||||||
|
const comp = companiesTabData.find(c => c.id === currentCompanyDetail);
|
||||||
|
if (comp) comp.nimi = nimi;
|
||||||
|
const avail = availableCompanies.find(c => c.id === currentCompanyDetail);
|
||||||
|
if (avail) avail.nimi = nimi;
|
||||||
|
populateCompanySelector();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== POSTILAATIKOT ====================
|
||||||
|
|
||||||
|
let mailboxesData = [];
|
||||||
|
|
||||||
|
async function loadMailboxes() {
|
||||||
|
try {
|
||||||
|
mailboxesData = await apiCall('mailboxes');
|
||||||
|
renderMailboxes();
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMailboxes() {
|
||||||
|
const container = document.getElementById('mailboxes-list');
|
||||||
|
if (mailboxesData.length === 0) {
|
||||||
|
container.innerHTML = '<p style="color:#888;font-size:0.9rem;">Ei postilaatikoita. Lisää ensimmäinen postilaatikko.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = mailboxesData.map(mb => `<div class="mailbox-item" style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem;background:#fff;border:1px solid #e0e0e0;border-radius:8px;margin-bottom:0.5rem;">
|
||||||
|
<div>
|
||||||
|
<strong>${esc(mb.nimi)}</strong>
|
||||||
|
<span style="color:#888;font-size:0.85rem;margin-left:0.75rem;">${esc(mb.imap_user)}</span>
|
||||||
|
<span style="color:${mb.aktiivinen !== false ? '#22c55e' : '#888'};font-size:0.8rem;margin-left:0.5rem;">${mb.aktiivinen !== false ? 'Aktiivinen' : 'Ei aktiivinen'}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:0.5rem;">
|
||||||
|
<button class="btn-link" onclick="editMailbox('${mb.id}')">Muokkaa</button>
|
||||||
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteMailbox('${mb.id}','${esc(mb.nimi)}')">Poista</button>
|
||||||
|
</div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-add-mailbox').addEventListener('click', () => {
|
||||||
|
showMailboxForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMailboxForm(mb = null) {
|
||||||
|
document.getElementById('mailbox-form-title').textContent = mb ? 'Muokkaa postilaatikkoa' : 'Uusi postilaatikko';
|
||||||
|
document.getElementById('mailbox-form-id').value = mb ? mb.id : '';
|
||||||
|
document.getElementById('mailbox-form-nimi').value = mb ? mb.nimi : '';
|
||||||
|
document.getElementById('mailbox-form-host').value = mb ? mb.imap_host : '';
|
||||||
|
document.getElementById('mailbox-form-port').value = mb ? mb.imap_port : 993;
|
||||||
|
document.getElementById('mailbox-form-user').value = mb ? mb.imap_user : '';
|
||||||
|
document.getElementById('mailbox-form-password').value = mb ? mb.imap_password : '';
|
||||||
|
document.getElementById('mailbox-form-encryption').value = mb ? (mb.imap_encryption || 'ssl') : 'ssl';
|
||||||
|
document.getElementById('mailbox-form-smtp-email').value = mb ? (mb.smtp_from_email || '') : '';
|
||||||
|
document.getElementById('mailbox-form-smtp-name').value = mb ? (mb.smtp_from_name || '') : '';
|
||||||
|
document.getElementById('mailbox-form-container').style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editMailbox(id) {
|
||||||
|
const mb = mailboxesData.find(m => m.id === id);
|
||||||
|
if (mb) showMailboxForm(mb);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMailbox(id, nimi) {
|
||||||
|
if (!confirm(`Poistetaanko postilaatikko "${nimi}"?`)) return;
|
||||||
|
try {
|
||||||
|
await apiCall('mailbox_delete', 'POST', { id });
|
||||||
|
loadMailboxes();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-save-mailbox').addEventListener('click', async () => {
|
||||||
|
const data = {
|
||||||
|
id: document.getElementById('mailbox-form-id').value || undefined,
|
||||||
|
nimi: document.getElementById('mailbox-form-nimi').value,
|
||||||
|
imap_host: document.getElementById('mailbox-form-host').value,
|
||||||
|
imap_port: parseInt(document.getElementById('mailbox-form-port').value) || 993,
|
||||||
|
imap_user: document.getElementById('mailbox-form-user').value,
|
||||||
|
imap_password: document.getElementById('mailbox-form-password').value,
|
||||||
|
imap_encryption: document.getElementById('mailbox-form-encryption').value,
|
||||||
|
smtp_from_email: document.getElementById('mailbox-form-smtp-email').value,
|
||||||
|
smtp_from_name: document.getElementById('mailbox-form-smtp-name').value,
|
||||||
|
aktiivinen: true,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await apiCall('mailbox_save', 'POST', data);
|
||||||
|
document.getElementById('mailbox-form-container').style.display = 'none';
|
||||||
|
loadMailboxes();
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-cancel-mailbox').addEventListener('click', () => {
|
||||||
|
document.getElementById('mailbox-form-container').style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== YRITYKSEN KÄYTTÄJÄOIKEUDET ====================
|
||||||
|
|
||||||
|
async function loadCompanyUsers(companyId) {
|
||||||
|
try {
|
||||||
|
const users = await apiCall('users');
|
||||||
|
const container = document.getElementById('company-users-list');
|
||||||
|
container.innerHTML = users.map(u => {
|
||||||
|
const hasAccess = (u.companies || []).includes(companyId);
|
||||||
|
return `<label style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0;cursor:pointer;">
|
||||||
|
<input type="checkbox" class="company-user-cb" data-user-id="${u.id}" ${hasAccess ? 'checked' : ''} onchange="toggleCompanyUser('${u.id}','${companyId}',this.checked)">
|
||||||
|
<strong>${esc(u.nimi || u.username)}</strong>
|
||||||
|
<span style="color:#888;font-size:0.85rem;">(${u.username}) — ${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'}</span>
|
||||||
|
</label>`;
|
||||||
|
}).join('');
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleCompanyUser(userId, companyId, add) {
|
||||||
|
try {
|
||||||
|
const users = await apiCall('users');
|
||||||
|
const user = users.find(u => u.id === userId);
|
||||||
|
if (!user) return;
|
||||||
|
let companies = user.companies || [];
|
||||||
|
if (add && !companies.includes(companyId)) {
|
||||||
|
companies.push(companyId);
|
||||||
|
} else if (!add) {
|
||||||
|
companies = companies.filter(c => c !== companyId);
|
||||||
|
}
|
||||||
|
await apiCall('user_update', 'POST', { id: userId, companies });
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
loadCaptcha();
|
loadCaptcha();
|
||||||
checkAuth();
|
checkAuth();
|
||||||
|
|||||||
40
style.css
40
style.css
@@ -1321,3 +1321,43 @@ span.empty {
|
|||||||
color: #999;
|
color: #999;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Company selector */
|
||||||
|
.company-selector {
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f3460;
|
||||||
|
background: #f8f9fb;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-selector:hover,
|
||||||
|
.company-selector:focus {
|
||||||
|
border-color: #0f3460;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mailbox items */
|
||||||
|
.mailbox-item {
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mailbox-item:hover {
|
||||||
|
border-color: #0f3460 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Company badge */
|
||||||
|
.company-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #e8f0fe;
|
||||||
|
color: #1a56db;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user