feat: vastauspohjien hallinta asiakaspalvelu-tabissa + allekirjoitus-checkbox

- Vastauspohjat nyt hallittavissa Asiakaspalvelu-tabin kautta (kaikille käyttäjille)
- Uusi "Vastauspohjat" -nappi tikettilistan yläpalkissa
- CRUD: lisää, muokkaa, poista vastauspohjia tukitabin näkymässä
- "Älä käytä allekirjoitusta" -checkbox vastauslomakkeessa (oletuksena päällä)
- Backend: no_signature-parametri estää allekirjoituksen liittämisen
- Poistettu orpo profiili-vastauspohjien JS-koodi

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 21:51:56 +02:00
parent f6e11f8426
commit 2d2680483c
3 changed files with 154 additions and 9 deletions

11
api.php
View File

@@ -2108,12 +2108,15 @@ switch ($action) {
$replyMailbox = $companyConf['mailboxes'][0]; $replyMailbox = $companyConf['mailboxes'][0];
} }
// Hae käyttäjän allekirjoitus tälle postilaatikolle // Hae käyttäjän allekirjoitus tälle postilaatikolle (ellei estetty)
$noSignature = !empty($input['no_signature']);
$mailboxId = $replyMailbox['id'] ?? ''; $mailboxId = $replyMailbox['id'] ?? '';
$signature = ''; $signature = '';
$sigUser = dbGetUser($_SESSION['user_id']); if (!$noSignature) {
if ($sigUser) { $sigUser = dbGetUser($_SESSION['user_id']);
$signature = trim($sigUser['signatures'][$mailboxId] ?? ''); if ($sigUser) {
$signature = trim($sigUser['signatures'][$mailboxId] ?? '');
}
} }
$emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body; $emailBody = $signature ? $body . "\n\n-- \n" . $signature : $body;

View File

@@ -383,6 +383,7 @@
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.85rem;color:#777;cursor:pointer;white-space:nowrap;"> <label style="display:flex;align-items:center;gap:0.4rem;font-size:0.85rem;color:#777;cursor:pointer;white-space:nowrap;">
<input type="checkbox" id="ticket-show-closed"> Suljetut <input type="checkbox" id="ticket-show-closed"> Suljetut
</label> </label>
<button class="btn-secondary" id="btn-ticket-templates" style="padding:7px 14px;font-size:0.82rem;">&#128221; Vastauspohjat</button>
<button class="btn-secondary" id="btn-ticket-rules" style="padding:7px 14px;font-size:0.82rem;">&#9881; Säännöt</button> <button class="btn-secondary" id="btn-ticket-rules" style="padding:7px 14px;font-size:0.82rem;">&#9881; Säännöt</button>
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;color:#777;cursor:pointer;white-space:nowrap;margin-left:auto;"> <label style="display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;color:#777;cursor:pointer;white-space:nowrap;margin-left:auto;">
<input type="checkbox" id="ticket-auto-refresh" checked> Autopäivitys <input type="checkbox" id="ticket-auto-refresh" checked> Autopäivitys
@@ -458,7 +459,10 @@
</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 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:space-between;align-items:center;gap:0.5rem;margin-top:0.5rem;">
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;color:#888;cursor:pointer;white-space:nowrap;">
<input type="checkbox" id="reply-no-signature" checked> Älä käytä allekirjoitusta
</label>
<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>
</div> </div>
@@ -534,6 +538,39 @@
</div> </div>
</div> </div>
<!-- Vastauspohjien hallinta -->
<div id="ticket-templates-view" style="display:none;">
<button class="btn-secondary" id="btn-templates-back" style="color:#555;border-color:#ddd;margin-bottom:1rem;">&#8592; Takaisin tiketteihin</button>
<div class="table-card" style="padding:1.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:#0f3460;margin:0;">Vastauspohjat</h3>
<button class="btn-primary" id="btn-add-tpl" style="font-size:0.85rem;">+ Lisää pohja</button>
</div>
<p style="color:#888;font-size:0.85rem;margin-bottom:1rem;">Yrityksen yhteiset vastauspohjat tiketteihin. Valittavissa vastauslomakkeen valikosta kaikille käyttäjille.</p>
<div id="tpl-list"></div>
</div>
<!-- Pohjanlomake -->
<div id="tpl-form-container" class="table-card" style="padding:1.5rem;margin-top:1rem;display:none;">
<h4 style="color:#0f3460;margin-bottom:1rem;" id="tpl-form-title">Uusi vastauspohja</h4>
<input type="hidden" id="tpl-form-id">
<div class="form-grid" style="max-width:600px;">
<div class="form-group full-width">
<label>Nimi *</label>
<input type="text" id="tpl-form-name" placeholder="esim. Kuittaus vastaanotettu">
</div>
<div class="form-group full-width">
<label>Sisältö *</label>
<textarea id="tpl-form-body" rows="6" placeholder="Kiitos viestistäsi! Olemme vastaanottaneet asiasi ja palaamme siihen mahdollisimman pian."></textarea>
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn-primary" id="btn-save-tpl">Tallenna</button>
<button class="btn-secondary" id="btn-cancel-tpl">Peruuta</button>
</div>
</div>
</div>
</div> </div>
</div> </div>

113
script.js
View File

@@ -1184,6 +1184,7 @@ document.getElementById('profile-form').addEventListener('submit', async (e) =>
} catch (e) { alert(e.message); } } catch (e) { alert(e.message); }
}); });
// ==================== TICKETS (ASIAKASPALVELU) ==================== // ==================== TICKETS (ASIAKASPALVELU) ====================
let tickets = []; let tickets = [];
@@ -1564,8 +1565,10 @@ async function showTicketDetail(id, companyId = '') {
</div>`; </div>`;
}).join(''); }).join('');
// Show detail, hide list // Show detail, hide list + other views
document.getElementById('ticket-list-view').style.display = 'none'; document.getElementById('ticket-list-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'none';
document.getElementById('ticket-templates-view').style.display = 'none';
document.getElementById('ticket-detail-view').style.display = 'block'; document.getElementById('ticket-detail-view').style.display = 'block';
// Reset reply form // Reset reply form
@@ -1598,8 +1601,9 @@ async function showTicketDetail(id, companyId = '') {
// Allekirjoituksen esikatselu // Allekirjoituksen esikatselu
function updateSignaturePreview(mbId) { function updateSignaturePreview(mbId) {
const sigPreview = document.getElementById('signature-preview'); const sigPreview = document.getElementById('signature-preview');
const noSigCheck = document.getElementById('reply-no-signature');
const sig = currentUserSignatures[mbId] || ''; const sig = currentUserSignatures[mbId] || '';
if (sig) { if (sig && !(noSigCheck && noSigCheck.checked)) {
sigPreview.textContent = '-- \n' + sig; sigPreview.textContent = '-- \n' + sig;
sigPreview.style.display = 'block'; sigPreview.style.display = 'block';
} else { } else {
@@ -1608,6 +1612,15 @@ async function showTicketDetail(id, companyId = '') {
} }
updateSignaturePreview(ticket.mailbox_id || ''); updateSignaturePreview(ticket.mailbox_id || '');
// Allekirjoitus-checkbox: päivitä esikatselu vaihdettaessa
const noSigCheckbox = document.getElementById('reply-no-signature');
if (noSigCheckbox) {
noSigCheckbox.addEventListener('change', () => {
const mbSelect = document.getElementById('reply-mailbox-select');
updateSignaturePreview(mbSelect ? mbSelect.value : '');
});
}
// Vastauspohjat — lataa dropdown // Vastauspohjat — lataa dropdown
try { try {
const templates = await apiCall('reply_templates'); const templates = await apiCall('reply_templates');
@@ -1634,6 +1647,7 @@ async function showTicketDetail(id, companyId = '') {
function showTicketListView() { function showTicketListView() {
document.getElementById('ticket-detail-view').style.display = 'none'; document.getElementById('ticket-detail-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'none'; document.getElementById('ticket-rules-view').style.display = 'none';
document.getElementById('ticket-templates-view').style.display = 'none';
document.getElementById('ticket-list-view').style.display = 'block'; document.getElementById('ticket-list-view').style.display = 'block';
currentTicketId = null; currentTicketId = null;
// Reset bulk selection // Reset bulk selection
@@ -1659,19 +1673,23 @@ document.querySelectorAll('.btn-reply-tab').forEach(btn => {
const sigPrev = document.getElementById('signature-preview'); const sigPrev = document.getElementById('signature-preview');
const metaFields = document.getElementById('reply-meta-fields'); const metaFields = document.getElementById('reply-meta-fields');
const tplWrap = document.getElementById('reply-template-select-wrap'); const tplWrap = document.getElementById('reply-template-select-wrap');
const noSigLabel = document.getElementById('reply-no-signature') ? document.getElementById('reply-no-signature').closest('label') : null;
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'; sigPrev.style.display = 'none';
if (metaFields) metaFields.style.display = 'none'; if (metaFields) metaFields.style.display = 'none';
if (tplWrap) tplWrap.style.display = 'none'; if (tplWrap) tplWrap.style.display = 'none';
if (noSigLabel) noSigLabel.style.display = 'none';
} else { } else {
textarea.placeholder = 'Kirjoita vastaus...'; textarea.placeholder = 'Kirjoita vastaus...';
sendBtn.textContent = 'Lähetä vastaus'; sendBtn.textContent = 'Lähetä vastaus';
if (metaFields) metaFields.style.display = ''; if (metaFields) metaFields.style.display = '';
if (tplWrap) tplWrap.style.display = ''; if (tplWrap) tplWrap.style.display = '';
// Näytä allekirjoitus jos on asetettu if (noSigLabel) noSigLabel.style.display = '';
if (sigPrev.textContent.trim()) sigPrev.style.display = 'block'; // Näytä allekirjoitus jos on asetettu ja checkbox sallii
const noSigCheck = document.getElementById('reply-no-signature');
if (sigPrev.textContent.trim() && !(noSigCheck && noSigCheck.checked)) sigPrev.style.display = 'block';
} }
}); });
}); });
@@ -1692,8 +1710,10 @@ document.getElementById('btn-send-reply').addEventListener('click', async () =>
if (ticketReplyType !== 'note') { if (ticketReplyType !== 'note') {
const mbSel = document.getElementById('reply-mailbox-select'); const mbSel = document.getElementById('reply-mailbox-select');
const ccFld = document.getElementById('reply-cc'); const ccFld = document.getElementById('reply-cc');
const noSig = document.getElementById('reply-no-signature');
if (mbSel) payload.mailbox_id = mbSel.value; if (mbSel) payload.mailbox_id = mbSel.value;
if (ccFld) payload.cc = ccFld.value.trim(); if (ccFld) payload.cc = ccFld.value.trim();
if (noSig && noSig.checked) payload.no_signature = true;
} }
await apiCall(action + ticketCompanyParam(), 'POST', payload); await apiCall(action + ticketCompanyParam(), 'POST', payload);
// Reload the detail view // Reload the detail view
@@ -1821,6 +1841,7 @@ function renderRules() {
function showRulesView() { function showRulesView() {
document.getElementById('ticket-list-view').style.display = 'none'; document.getElementById('ticket-list-view').style.display = 'none';
document.getElementById('ticket-detail-view').style.display = 'none'; document.getElementById('ticket-detail-view').style.display = 'none';
document.getElementById('ticket-templates-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'block'; document.getElementById('ticket-rules-view').style.display = 'block';
loadRules(); loadRules();
} }
@@ -1898,6 +1919,90 @@ async function toggleRule(id, enabled) {
} catch (e) { alert(e.message); } } catch (e) { alert(e.message); }
} }
// ==================== VASTAUSPOHJAT (TUKITABISSA) ====================
function showTemplatesView() {
document.getElementById('ticket-list-view').style.display = 'none';
document.getElementById('ticket-detail-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'none';
document.getElementById('ticket-templates-view').style.display = 'block';
hideTplForm();
renderTplList();
}
function hideTemplatesView() {
document.getElementById('ticket-templates-view').style.display = 'none';
document.getElementById('ticket-list-view').style.display = 'block';
}
function renderTplList() {
const list = document.getElementById('tpl-list');
if (!list) return;
if (replyTemplates.length === 0) {
list.innerHTML = '<p style="color:#aaa;font-size:0.85rem;">Ei vastauspohjia vielä. Lisää ensimmäinen klikkaamalla "+ Lisää pohja".</p>';
return;
}
list.innerHTML = replyTemplates.map(t =>
`<div style="display:flex;justify-content:space-between;align-items:center;padding:0.6rem 0;border-bottom:1px solid #f0f2f5;">
<div style="min-width:0;flex:1;">
<strong style="font-size:0.9rem;">${esc(t.nimi)}</strong>
<div style="font-size:0.8rem;color:#888;max-width:450px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(t.body.substring(0, 100))}</div>
</div>
<div style="display:flex;gap:0.3rem;flex-shrink:0;">
<button class="btn-secondary" onclick="editTpl('${t.id}')" style="padding:4px 10px;font-size:0.8rem;">Muokkaa</button>
<button class="btn-danger" onclick="deleteTpl('${t.id}')" style="padding:4px 10px;font-size:0.8rem;">Poista</button>
</div>
</div>`
).join('');
}
function showTplForm(tpl) {
document.getElementById('tpl-form-container').style.display = '';
document.getElementById('tpl-form-title').textContent = tpl ? 'Muokkaa vastauspohjaa' : 'Uusi vastauspohja';
document.getElementById('tpl-form-id').value = tpl ? tpl.id : '';
document.getElementById('tpl-form-name').value = tpl ? tpl.nimi : '';
document.getElementById('tpl-form-body').value = tpl ? tpl.body : '';
}
function hideTplForm() {
document.getElementById('tpl-form-container').style.display = 'none';
}
document.getElementById('btn-ticket-templates').addEventListener('click', async () => {
await loadTemplates();
showTemplatesView();
});
document.getElementById('btn-templates-back').addEventListener('click', () => hideTemplatesView());
document.getElementById('btn-add-tpl').addEventListener('click', () => showTplForm(null));
document.getElementById('btn-cancel-tpl').addEventListener('click', () => hideTplForm());
document.getElementById('btn-save-tpl').addEventListener('click', async () => {
const nimi = document.getElementById('tpl-form-name').value.trim();
const body = document.getElementById('tpl-form-body').value.trim();
if (!nimi || !body) { alert('Täytä nimi ja sisältö'); return; }
const id = document.getElementById('tpl-form-id').value || undefined;
try {
await apiCall('reply_template_save', 'POST', { id, nimi, body });
hideTplForm();
await loadTemplates();
renderTplList();
} catch (e) { alert(e.message); }
});
window.editTpl = function(id) {
const t = replyTemplates.find(x => x.id === id);
if (t) showTplForm(t);
};
window.deleteTpl = async function(id) {
if (!confirm('Poistetaanko vastauspohja?')) return;
try {
await apiCall('reply_template_delete', 'POST', { id });
await loadTemplates();
renderTplList();
} catch (e) { alert(e.message); }
};
// ==================== BULK ACTIONS ==================== // ==================== BULK ACTIONS ====================
let bulkSelectedIds = new Set(); let bulkSelectedIds = new Set();