Asiakaspalvelu: alinavi-uudelleenjärjestely + tikettityyppien hallinta

Vastauspohjat, Säännöt ja Asetukset siirretty omiksi alinaveikseen
tikettilistan overlay-napeista. Säännöt-välilehdelle lisätty
tikettityyppien hallinta (lisää/poista). Tyypit tallennetaan
tietokantaan yrityskohtaisesti ja populoidaan dynaamisesti
kaikkiin dropdown-valikoihin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 12:52:54 +02:00
parent 306dc6c5cc
commit 656b5042e4
5 changed files with 377 additions and 214 deletions

162
script.js
View File

@@ -301,11 +301,8 @@ function switchToTab(target, subTab) {
if (target === 'support') {
loadTickets(); showTicketListView();
if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh();
if (subTab === 'ohjeet') {
switchSupportSubTab('support-ohjeet');
} else {
switchSupportSubTab('support-tickets');
}
const supportSubMap = { ohjeet: 'support-ohjeet', saannot: 'support-saannot', vastauspohjat: 'support-vastauspohjat', asetukset: 'support-asetukset' };
switchSupportSubTab(supportSubMap[subTab] || 'support-tickets');
}
if (target === 'documents') {
if (subTab && subTab !== 'kokoukset') {
@@ -1344,7 +1341,7 @@ const ticketStatusLabels = {
suljettu: 'Suljettu',
};
const ticketTypeLabels = {
let ticketTypeLabels = {
laskutus: 'Laskutus',
tekniikka: 'Tekniikka',
vika: 'Vika',
@@ -1520,10 +1517,9 @@ async function showTicketDetail(id, companyId = '') {
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<select id="ticket-type-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="muu" ${(ticket.type || 'muu') === 'muu' ? 'selected' : ''}>Muu</option>
<option value="laskutus" ${ticket.type === 'laskutus' ? 'selected' : ''}>Laskutus</option>
<option value="tekniikka" ${ticket.type === 'tekniikka' ? 'selected' : ''}>Tekniikka</option>
<option value="vika" ${ticket.type === 'vika' ? 'selected' : ''}>Vika</option>
${Object.entries(ticketTypeLabels).map(([val, label]) =>
`<option value="${val}" ${(ticket.type || 'muu') === val ? 'selected' : ''}>${esc(label)}</option>`
).join('')}
</select>
<select id="ticket-status-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
<option value="uusi" ${ticket.status === 'uusi' ? 'selected' : ''}>Uusi</option>
@@ -2022,20 +2018,6 @@ function renderRules() {
}).join('');
}
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-settings-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'block';
loadRules();
}
function hideRulesView() {
document.getElementById('ticket-rules-view').style.display = 'none';
document.getElementById('ticket-list-view').style.display = 'block';
}
function showRuleForm(rule) {
document.getElementById('rule-form-container').style.display = '';
document.getElementById('rule-form-title').textContent = rule ? 'Muokkaa sääntöä' : 'Uusi sääntö';
@@ -2057,8 +2039,6 @@ function hideRuleForm() {
editingRuleId = null;
}
document.getElementById('btn-ticket-rules').addEventListener('click', () => showRulesView());
document.getElementById('btn-rules-back').addEventListener('click', () => hideRulesView());
document.getElementById('btn-add-rule').addEventListener('click', () => showRuleForm(null));
document.getElementById('btn-cancel-rule').addEventListener('click', () => hideRuleForm());
@@ -2108,23 +2088,86 @@ async function toggleRule(id, enabled) {
} catch (e) { alert(e.message); }
}
// ==================== TIKETTITYYPIT ====================
async function loadTicketTypes() {
try {
const types = await apiCall('ticket_types');
ticketTypeLabels = {};
types.forEach(t => { ticketTypeLabels[t.value] = t.label; });
renderTicketTypes(types);
populateTypeDropdowns();
} catch (e) { console.error('loadTicketTypes:', e); }
}
function renderTicketTypes(types) {
const container = document.getElementById('ticket-types-list');
if (!container) return;
if (!types || types.length === 0) {
container.innerHTML = '<p style="color:#888;font-size:0.85rem;">Ei tikettityyppejä.</p>';
return;
}
container.innerHTML = types.map(t =>
`<div class="ticket-type-item">
<span class="ticket-type-badge ticket-type-${t.value}">${esc(t.label)}</span>
<span style="color:#888;font-size:0.8rem;">(${esc(t.value)})</span>
<button class="btn-danger" onclick="deleteTicketType('${esc(t.value)}')" style="padding:2px 8px;font-size:0.75rem;margin-left:auto;">Poista</button>
</div>`
).join('');
}
function populateTypeDropdowns() {
const options = Object.entries(ticketTypeLabels).map(
([val, label]) => `<option value="${val}">${esc(label)}</option>`
).join('');
// Tikettilistan suodatin
const filter = document.getElementById('ticket-type-filter');
if (filter) {
const current = filter.value;
filter.innerHTML = '<option value="">Kaikki tyypit</option>' + options;
filter.value = current;
}
// Sääntölomakkeen tyyppi
const ruleType = document.getElementById('rule-form-type');
if (ruleType) {
const current = ruleType.value;
ruleType.innerHTML = '<option value="">— Ei muuteta —</option>' + options;
ruleType.value = current;
}
// Tiketin detail-näkymän tyyppi-select
const detailType = document.getElementById('ticket-detail-type');
if (detailType) {
const current = detailType.value;
detailType.innerHTML = options;
detailType.value = current;
}
}
document.getElementById('btn-add-ticket-type')?.addEventListener('click', async () => {
const value = document.getElementById('new-ticket-type-value').value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
const label = document.getElementById('new-ticket-type-label').value.trim();
if (!value || !label) { alert('Täytä tunnus ja nimi'); return; }
try {
await apiCall('ticket_type_save', 'POST', { value, label });
document.getElementById('new-ticket-type-value').value = '';
document.getElementById('new-ticket-type-label').value = '';
await loadTicketTypes();
} catch (e) { alert(e.message); }
});
window.deleteTicketType = async function(value) {
if (!confirm(`Poistetaanko tikettityyppi "${value}"?`)) return;
try {
await apiCall('ticket_type_delete', 'POST', { value });
await loadTicketTypes();
} 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-settings-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;
@@ -2158,11 +2201,6 @@ 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());
@@ -2195,14 +2233,7 @@ window.deleteTpl = async function(id) {
// ==================== OMAT ASETUKSET (TIKETTIEN ASETUKSET) ====================
async function openTicketSettings() {
// Piilota muut näkymät, näytä asetukset
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 = 'none';
document.getElementById('ticket-settings-view').style.display = 'block';
async function initTicketSettings() {
const sigContainer = document.getElementById('ticket-settings-signatures');
const visContainer = document.getElementById('ticket-settings-mailbox-visibility');
sigContainer.innerHTML = '<p style="color:#888;font-size:0.85rem;">Ladataan...</p>';
@@ -2239,13 +2270,6 @@ async function openTicketSettings() {
}
}
function closeTicketSettings() {
document.getElementById('ticket-settings-view').style.display = 'none';
document.getElementById('ticket-list-view').style.display = 'block';
}
document.getElementById('btn-ticket-settings').addEventListener('click', () => openTicketSettings());
document.getElementById('btn-save-ticket-settings').addEventListener('click', async () => {
// Kerää allekirjoitukset
const signatures = {};
@@ -2267,7 +2291,6 @@ document.getElementById('btn-save-ticket-settings').addEventListener('click', as
// Päivitä lokaalit muuttujat
currentUserSignatures = signatures;
currentHiddenMailboxes = hiddenMailboxes;
closeTicketSettings();
// Lataa tiketit uudelleen suodatuksen päivittämiseksi
loadTickets();
alert('Asetukset tallennettu!');
@@ -3052,8 +3075,19 @@ function switchSupportSubTab(target) {
if (btn) btn.classList.add('active');
const content = document.getElementById('subtab-' + target);
if (content) content.classList.add('active');
if (target === 'support-ohjeet') { loadGuides(); window.location.hash = 'support/ohjeet'; }
else { window.location.hash = 'support'; }
// Lataa data tarvittaessa
const hashMap = {
'support-tickets': 'support',
'support-ohjeet': 'support/ohjeet',
'support-saannot': 'support/saannot',
'support-vastauspohjat': 'support/vastauspohjat',
'support-asetukset': 'support/asetukset',
};
if (target === 'support-ohjeet') loadGuides();
if (target === 'support-saannot') { loadRules(); loadTicketTypes(); }
if (target === 'support-vastauspohjat') loadTemplates();
if (target === 'support-asetukset') initTicketSettings();
window.location.hash = hashMap[target] || 'support';
}
document.querySelectorAll('#support-sub-tab-bar .sub-tab').forEach(btn => {