Lisää sähköpostiallekirjoitus per käyttäjä per postilaatikko

- Allekirjoitukset tallennetaan users.json:iin (signatures-objekti, avaimena mailbox_id)
- Käyttäjälomakkeessa dynaamiset textareat jokaiselle postilaatikolle
- Allekirjoitus liitetään automaattisesti sähköpostivastauksiin (ticket_reply)
- Esikatselu näkyy tikettivastauslomakkeen alla
- Muistiinpanoihin (ticket_note) ei lisätä allekirjoitusta
- Uusi all_mailboxes endpoint palauttaa kaikki käyttäjän postilaatikot
- check_auth ja login palauttavat nyt myös user_id ja signatures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 13:09:30 +02:00
parent 11e1103eb4
commit 918a5ff120
3 changed files with 125 additions and 5 deletions

67
api.php
View File

@@ -1067,6 +1067,7 @@ switch ($action) {
'role' => $u['role'], 'role' => $u['role'],
'companies' => $companyList, 'companies' => $companyList,
'company_id' => $_SESSION['company_id'], 'company_id' => $_SESSION['company_id'],
'signatures' => $u['signatures'] ?? [],
]); ]);
$found = true; $found = true;
break; break;
@@ -1107,13 +1108,23 @@ switch ($action) {
$companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']]; $companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']];
} }
} }
// Hae allekirjoitukset
$userSignatures = [];
foreach ($users as $uu) {
if ($uu['id'] === $_SESSION['user_id']) {
$userSignatures = $uu['signatures'] ?? [];
break;
}
}
echo json_encode([ echo json_encode([
'authenticated' => true, 'authenticated' => true,
'user_id' => $_SESSION['user_id'],
'username' => $_SESSION['username'], 'username' => $_SESSION['username'],
'nimi' => $_SESSION['nimi'], 'nimi' => $_SESSION['nimi'],
'role' => $_SESSION['role'], 'role' => $_SESSION['role'],
'companies' => $companyList, 'companies' => $companyList,
'company_id' => $_SESSION['company_id'] ?? '', 'company_id' => $_SESSION['company_id'] ?? '',
'signatures' => $userSignatures,
]); ]);
} else { } else {
echo json_encode(['authenticated' => false]); echo json_encode(['authenticated' => false]);
@@ -1232,6 +1243,12 @@ switch ($action) {
$allCompanies = loadCompanies(); $allCompanies = loadCompanies();
$validIds = array_column($allCompanies, 'id'); $validIds = array_column($allCompanies, 'id');
$companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds))); $companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds)));
$signatures = [];
if (isset($input['signatures']) && is_array($input['signatures'])) {
foreach ($input['signatures'] as $mbId => $sig) {
$signatures[(string)$mbId] = (string)$sig;
}
}
$newUser = [ $newUser = [
'id' => generateId(), 'id' => generateId(),
'username' => $username, 'username' => $username,
@@ -1240,6 +1257,7 @@ switch ($action) {
'email' => $email, 'email' => $email,
'role' => $role, 'role' => $role,
'companies' => $companies, 'companies' => $companies,
'signatures' => $signatures,
'luotu' => date('Y-m-d H:i:s'), 'luotu' => date('Y-m-d H:i:s'),
]; ];
$users[] = $newUser; $users[] = $newUser;
@@ -1269,6 +1287,13 @@ switch ($action) {
if (!empty($input['password'])) { if (!empty($input['password'])) {
$u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT); $u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
} }
if (isset($input['signatures']) && is_array($input['signatures'])) {
$sigs = [];
foreach ($input['signatures'] as $mbId => $sig) {
$sigs[(string)$mbId] = (string)$sig;
}
$u['signatures'] = $sigs;
}
$found = true; $found = true;
addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}"); addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
// Päivitä sessio jos muokattiin kirjautunutta käyttäjää // Päivitä sessio jos muokattiin kirjautunutta käyttäjää
@@ -2022,8 +2047,20 @@ switch ($action) {
$replyMailbox = $companyConf['mailboxes'][0]; $replyMailbox = $companyConf['mailboxes'][0];
} }
// Hae käyttäjän allekirjoitus tälle postilaatikolle
$mailboxId = $t['mailbox_id'] ?? '';
$signature = '';
$usersForSig = loadUsers();
foreach ($usersForSig as $sigUser) {
if ($sigUser['id'] === $_SESSION['user_id']) {
$signature = trim($sigUser['signatures'][$mailboxId] ?? '');
break;
}
}
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;
$subject = 'Re: ' . $t['subject']; $subject = 'Re: ' . $t['subject'];
$sent = sendTicketMail($t['from_email'], $subject, $body, $lastMsgId, trim($allRefs), $replyMailbox); $sent = sendTicketMail($t['from_email'], $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox);
if (!$sent) { if (!$sent) {
http_response_code(500); http_response_code(500);
@@ -2031,13 +2068,13 @@ switch ($action) {
break 2; break 2;
} }
// Add reply to ticket // Add reply to ticket (tallennetaan allekirjoituksen kanssa)
$reply = [ $reply = [
'id' => generateId(), 'id' => generateId(),
'type' => 'reply_out', 'type' => 'reply_out',
'from' => currentUser(), 'from' => currentUser(),
'from_name' => $_SESSION['nimi'] ?? currentUser(), 'from_name' => $_SESSION['nimi'] ?? currentUser(),
'body' => $body, 'body' => $emailBody,
'timestamp' => date('Y-m-d H:i:s'), 'timestamp' => date('Y-m-d H:i:s'),
'message_id' => '', 'message_id' => '',
]; ];
@@ -2399,6 +2436,30 @@ switch ($action) {
echo json_encode(loadCompanies()); echo json_encode(loadCompanies());
break; break;
case 'all_mailboxes':
requireAuth();
// Palauttaa kaikki postilaatikot käyttäjän yrityksistä (allekirjoituksia varten)
$userCompanyIds = $_SESSION['companies'] ?? [];
$allCompanies = loadCompanies();
$result = [];
foreach ($allCompanies as $comp) {
if (!in_array($comp['id'], $userCompanyIds)) continue;
$oldCompanyId = $_SESSION['company_id'] ?? '';
$_SESSION['company_id'] = $comp['id'];
$conf = loadCompanyConfig();
$_SESSION['company_id'] = $oldCompanyId;
foreach ($conf['mailboxes'] ?? [] as $mb) {
$result[] = [
'id' => $mb['id'],
'nimi' => $mb['nimi'] ?? $mb['imap_user'] ?? '',
'company_id' => $comp['id'],
'company_nimi' => $comp['nimi'],
];
}
}
echo json_encode($result);
break;
case 'company_create': case 'company_create':
requireAdmin(); requireAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;

View File

@@ -331,6 +331,7 @@
<button class="btn-reply-tab" data-reply-type="note">&#128221; Muistiinpano</button> <button class="btn-reply-tab" data-reply-type="note">&#128221; Muistiinpano</button>
</div> </div>
<textarea id="ticket-reply-body" rows="5" placeholder="Kirjoita vastaus..."></textarea> <textarea id="ticket-reply-body" rows="5" placeholder="Kirjoita vastaus..."></textarea>
<div id="signature-preview" style="display:none;padding:0.5rem 0.75rem;margin-top:0.25rem;border-left:3px solid #d0d5dd;color:#888;font-size:0.82rem;white-space:pre-line;"></div>
<div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.5rem;"> <div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.5rem;">
<button class="btn-primary" id="btn-send-reply">Lähetä vastaus</button> <button class="btn-primary" id="btn-send-reply">Lähetä vastaus</button>
</div> </div>
@@ -735,6 +736,11 @@
<div id="user-company-checkboxes" style="display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.25rem;"></div> <div id="user-company-checkboxes" style="display:flex;flex-wrap:wrap;gap:0.75rem;margin-top:0.25rem;"></div>
</div> </div>
</div> </div>
<div id="user-signatures-section" style="display:none;margin-top:1rem;border-top:1px solid #e5e7eb;padding-top:1rem;">
<h3 style="color:#0f3460;font-size:1rem;margin-bottom:0.75rem;">Sähköpostiallekirjoitukset</h3>
<p style="color:#888;font-size:0.82rem;margin-bottom:0.75rem;">Allekirjoitus liitetään automaattisesti sähköpostivastausten loppuun.</p>
<div id="user-signatures-list"></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>
<button type="button" class="btn-secondary" id="user-form-cancel">Peruuta</button> <button type="button" class="btn-secondary" id="user-form-cancel">Peruuta</button>

View File

@@ -7,6 +7,7 @@ let currentUser = { username: '', nimi: '', role: '' };
let currentCompany = null; // {id, nimi} let currentCompany = null; // {id, nimi}
let availableCompanies = []; // [{id, nimi}, ...] let availableCompanies = []; // [{id, nimi}, ...]
let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki) let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki)
let currentUserSignatures = {}; // {mailbox_id: "allekirjoitus teksti"}
// Elements // Elements
const loginScreen = document.getElementById('login-screen'); const loginScreen = document.getElementById('login-screen');
@@ -131,9 +132,10 @@ async function checkAuth() {
try { try {
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, id: data.user_id };
availableCompanies = data.companies || []; availableCompanies = data.companies || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {};
showDashboard(); showDashboard();
} }
} catch (e) { /* not logged in */ } } catch (e) { /* not logged in */ }
@@ -147,9 +149,10 @@ loginForm.addEventListener('submit', async (e) => {
try { try {
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, id: data.user_id };
availableCompanies = data.companies || []; availableCompanies = data.companies || [];
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
currentUserSignatures = data.signatures || {};
showDashboard(); showDashboard();
} catch (err) { } catch (err) {
loginError.textContent = err.message; loginError.textContent = err.message;
@@ -976,6 +979,27 @@ function openUserForm(user = null) {
</label>` </label>`
).join(''); ).join('');
}); });
// Allekirjoitukset per postilaatikko
const sigSection = document.getElementById('user-signatures-section');
const sigList = document.getElementById('user-signatures-list');
const userSigs = user ? (user.signatures || {}) : {};
apiCall('all_mailboxes').then(mailboxes => {
if (mailboxes.length === 0) {
sigSection.style.display = 'none';
return;
}
sigSection.style.display = '';
sigList.innerHTML = mailboxes.map(mb =>
`<div style="margin-bottom:0.75rem;">
<label style="font-weight:600;font-size:0.85rem;color:#333;">${esc(mb.company_nimi)}${esc(mb.nimi)}</label>
<textarea class="sig-textarea" data-mailbox-id="${mb.id}" rows="3"
style="width:100%;margin-top:0.25rem;padding:8px;border:1px solid #ddd;border-radius:6px;font-size:0.85rem;font-family:inherit;resize:vertical;"
placeholder="esim.\nJukka\nYritys Oy\ninfo@yritys.fi">${esc(userSigs[mb.id] || '')}</textarea>
</div>`
).join('');
}).catch(() => {
sigSection.style.display = 'none';
});
userModal.style.display = 'flex'; userModal.style.display = 'flex';
} }
@@ -999,12 +1023,20 @@ 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 companies = [...document.querySelectorAll('.user-company-cb:checked')].map(cb => cb.value);
// Kerää allekirjoitukset
const signatures = {};
document.querySelectorAll('.sig-textarea').forEach(ta => {
const mbId = ta.dataset.mailboxId;
const val = ta.value.trim();
if (val) signatures[mbId] = val;
});
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, companies,
signatures,
}; };
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;
@@ -1014,6 +1046,12 @@ document.getElementById('user-form').addEventListener('submit', async (e) => {
else { await apiCall('user_create', 'POST', data); } else { await apiCall('user_create', 'POST', data); }
userModal.style.display = 'none'; userModal.style.display = 'none';
loadUsers(); loadUsers();
// Päivitä omat allekirjoitukset (check_auth palauttaa tuoreet)
const auth = await apiCall('check_auth');
if (auth.authenticated) {
currentUser = { username: auth.username, nimi: auth.nimi, role: auth.role, id: auth.user_id };
currentUserSignatures = auth.signatures || {};
}
} catch (e) { alert(e.message); } } catch (e) { alert(e.message); }
}); });
@@ -1348,6 +1386,17 @@ async function showTicketDetail(id, companyId = '') {
document.querySelector('.btn-reply-tab[data-reply-type="reply"]').classList.add('active'); document.querySelector('.btn-reply-tab[data-reply-type="reply"]').classList.add('active');
document.getElementById('btn-send-reply').textContent = 'Lähetä vastaus'; document.getElementById('btn-send-reply').textContent = 'Lähetä vastaus';
// Allekirjoituksen esikatselu
const sigPreview = document.getElementById('signature-preview');
const mailboxId = ticket.mailbox_id || '';
const sig = currentUserSignatures[mailboxId] || '';
if (sig) {
sigPreview.textContent = '-- \n' + sig;
sigPreview.style.display = 'block';
} else {
sigPreview.style.display = 'none';
}
} catch (e) { alert(e.message); } } catch (e) { alert(e.message); }
} }
@@ -1376,12 +1425,16 @@ document.querySelectorAll('.btn-reply-tab').forEach(btn => {
ticketReplyType = btn.dataset.replyType; ticketReplyType = btn.dataset.replyType;
const textarea = document.getElementById('ticket-reply-body'); const textarea = document.getElementById('ticket-reply-body');
const sendBtn = document.getElementById('btn-send-reply'); const sendBtn = document.getElementById('btn-send-reply');
const sigPrev = document.getElementById('signature-preview');
if (ticketReplyType === 'note') { if (ticketReplyType === 'note') {
textarea.placeholder = 'Kirjoita sisäinen muistiinpano...'; textarea.placeholder = 'Kirjoita sisäinen muistiinpano...';
sendBtn.textContent = 'Tallenna muistiinpano'; sendBtn.textContent = 'Tallenna muistiinpano';
sigPrev.style.display = 'none';
} else { } else {
textarea.placeholder = 'Kirjoita vastaus...'; textarea.placeholder = 'Kirjoita vastaus...';
sendBtn.textContent = 'Lähetä vastaus'; sendBtn.textContent = 'Lähetä vastaus';
// Näytä allekirjoitus jos on asetettu
if (sigPrev.textContent.trim()) sigPrev.style.display = 'block';
} }
}); });
}); });