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];
}
// 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'] ?? '';
$signature = '';
$sigUser = dbGetUser($_SESSION['user_id']);
if ($sigUser) {
$signature = trim($sigUser['signatures'][$mailboxId] ?? '');
if (!$noSignature) {
$sigUser = dbGetUser($_SESSION['user_id']);
if ($sigUser) {
$signature = trim($sigUser['signatures'][$mailboxId] ?? '');
}
}
$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;">
<input type="checkbox" id="ticket-show-closed"> Suljetut
</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>
<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
@@ -458,7 +459,10 @@
</div>
<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: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>
</div>
</div>
@@ -534,6 +538,39 @@
</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>

113
script.js
View File

@@ -1184,6 +1184,7 @@ document.getElementById('profile-form').addEventListener('submit', async (e) =>
} catch (e) { alert(e.message); }
});
// ==================== TICKETS (ASIAKASPALVELU) ====================
let tickets = [];
@@ -1564,8 +1565,10 @@ async function showTicketDetail(id, companyId = '') {
</div>`;
}).join('');
// Show detail, hide list
// Show detail, hide list + other views
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';
// Reset reply form
@@ -1598,8 +1601,9 @@ async function showTicketDetail(id, companyId = '') {
// Allekirjoituksen esikatselu
function updateSignaturePreview(mbId) {
const sigPreview = document.getElementById('signature-preview');
const noSigCheck = document.getElementById('reply-no-signature');
const sig = currentUserSignatures[mbId] || '';
if (sig) {
if (sig && !(noSigCheck && noSigCheck.checked)) {
sigPreview.textContent = '-- \n' + sig;
sigPreview.style.display = 'block';
} else {
@@ -1608,6 +1612,15 @@ async function showTicketDetail(id, companyId = '') {
}
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
try {
const templates = await apiCall('reply_templates');
@@ -1634,6 +1647,7 @@ async function showTicketDetail(id, companyId = '') {
function showTicketListView() {
document.getElementById('ticket-detail-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';
currentTicketId = null;
// Reset bulk selection
@@ -1659,19 +1673,23 @@ document.querySelectorAll('.btn-reply-tab').forEach(btn => {
const sigPrev = document.getElementById('signature-preview');
const metaFields = document.getElementById('reply-meta-fields');
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') {
textarea.placeholder = 'Kirjoita sisäinen muistiinpano...';
sendBtn.textContent = 'Tallenna muistiinpano';
sigPrev.style.display = 'none';
if (metaFields) metaFields.style.display = 'none';
if (tplWrap) tplWrap.style.display = 'none';
if (noSigLabel) noSigLabel.style.display = 'none';
} else {
textarea.placeholder = 'Kirjoita vastaus...';
sendBtn.textContent = 'Lähetä vastaus';
if (metaFields) metaFields.style.display = '';
if (tplWrap) tplWrap.style.display = '';
// Näytä allekirjoitus jos on asetettu
if (sigPrev.textContent.trim()) sigPrev.style.display = 'block';
if (noSigLabel) noSigLabel.style.display = '';
// 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') {
const mbSel = document.getElementById('reply-mailbox-select');
const ccFld = document.getElementById('reply-cc');
const noSig = document.getElementById('reply-no-signature');
if (mbSel) payload.mailbox_id = mbSel.value;
if (ccFld) payload.cc = ccFld.value.trim();
if (noSig && noSig.checked) payload.no_signature = true;
}
await apiCall(action + ticketCompanyParam(), 'POST', payload);
// Reload the detail view
@@ -1821,6 +1841,7 @@ function renderRules() {
function showRulesView() {
document.getElementById('ticket-list-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';
loadRules();
}
@@ -1898,6 +1919,90 @@ async function toggleRule(id, enabled) {
} 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 ====================
let bulkSelectedIds = new Set();