Add auto-rules management, bulk actions for tickets
- Auto-rules JS: load, render, save, edit, delete, toggle rules - Rules UI: Säännöt view with rule list and form - Bulk actions: checkbox selection in ticket list - Bulk close/delete endpoints (ticket_bulk_status, ticket_bulk_delete) - Bulk toolbar with select all, close selected, delete selected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
128
api.php
128
api.php
@@ -1534,12 +1534,40 @@ switch ($action) {
|
||||
'from_email' => $email['from_email'],
|
||||
'from_name' => $email['from_name'],
|
||||
'status' => 'uusi',
|
||||
'type' => 'muu',
|
||||
'assigned_to' => '',
|
||||
'customer_id' => '',
|
||||
'customer_name' => '',
|
||||
'created' => $email['date'],
|
||||
'updated' => $email['date'],
|
||||
'message_id' => $email['message_id'],
|
||||
'messages' => [$msg],
|
||||
];
|
||||
|
||||
// Apply auto-rules
|
||||
$rules = $config['ticket_rules'] ?? [];
|
||||
foreach ($rules as $rule) {
|
||||
if (empty($rule['enabled'])) continue;
|
||||
$match = true;
|
||||
if (!empty($rule['from_contains'])) {
|
||||
$needle = strtolower($rule['from_contains']);
|
||||
if (strpos(strtolower($email['from_email'] . ' ' . $email['from_name']), $needle) === false) {
|
||||
$match = false;
|
||||
}
|
||||
}
|
||||
if (!empty($rule['subject_contains'])) {
|
||||
$needle = strtolower($rule['subject_contains']);
|
||||
if (strpos(strtolower($email['subject'] ?? ''), $needle) === false) {
|
||||
$match = false;
|
||||
}
|
||||
}
|
||||
if ($match) {
|
||||
if (!empty($rule['set_status'])) $ticket['status'] = $rule['set_status'];
|
||||
if (!empty($rule['set_type'])) $ticket['type'] = $rule['set_type'];
|
||||
break; // First matching rule wins
|
||||
}
|
||||
}
|
||||
|
||||
$tickets[] = $ticket;
|
||||
$newCount++;
|
||||
}
|
||||
@@ -1802,6 +1830,106 @@ switch ($action) {
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'ticket_rules':
|
||||
requireAuth();
|
||||
$config = loadConfig();
|
||||
echo json_encode($config['ticket_rules'] ?? []);
|
||||
break;
|
||||
|
||||
case 'ticket_rule_save':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$config = loadConfig();
|
||||
$rules = $config['ticket_rules'] ?? [];
|
||||
|
||||
$rule = [
|
||||
'id' => $input['id'] ?? generateId(),
|
||||
'name' => trim($input['name'] ?? ''),
|
||||
'from_contains' => trim($input['from_contains'] ?? ''),
|
||||
'subject_contains' => trim($input['subject_contains'] ?? ''),
|
||||
'set_status' => $input['set_status'] ?? '',
|
||||
'set_type' => $input['set_type'] ?? '',
|
||||
'enabled' => $input['enabled'] ?? true,
|
||||
];
|
||||
|
||||
if (empty($rule['name'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Säännön nimi puuttuu']);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update existing or add new
|
||||
$found = false;
|
||||
foreach ($rules as &$r) {
|
||||
if ($r['id'] === $rule['id']) {
|
||||
$r = $rule;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($r);
|
||||
if (!$found) $rules[] = $rule;
|
||||
|
||||
$config['ticket_rules'] = $rules;
|
||||
file_put_contents(DATA_DIR . '/config.json', json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
addLog('config_update', '', '', 'Tikettisääntö: ' . $rule['name']);
|
||||
echo json_encode($rule);
|
||||
break;
|
||||
|
||||
case 'ticket_bulk_status':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$ids = $input['ids'] ?? [];
|
||||
$newStatus = $input['status'] ?? '';
|
||||
$validStatuses = ['uusi','kasittelyssa','odottaa','ratkaistu','suljettu'];
|
||||
if (!in_array($newStatus, $validStatuses)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Virheellinen tila']);
|
||||
break;
|
||||
}
|
||||
$tickets = json_decode(file_get_contents(TICKETS_FILE), true) ?: [];
|
||||
$changed = 0;
|
||||
foreach ($tickets as &$t) {
|
||||
if (in_array($t['id'], $ids)) {
|
||||
$t['status'] = $newStatus;
|
||||
$t['updated'] = date('Y-m-d H:i:s');
|
||||
$changed++;
|
||||
}
|
||||
}
|
||||
unset($t);
|
||||
file_put_contents(TICKETS_FILE, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
addLog('ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus");
|
||||
echo json_encode(['success' => true, 'changed' => $changed]);
|
||||
break;
|
||||
|
||||
case 'ticket_bulk_delete':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$ids = $input['ids'] ?? [];
|
||||
$tickets = json_decode(file_get_contents(TICKETS_FILE), true) ?: [];
|
||||
$before = count($tickets);
|
||||
$tickets = array_values(array_filter($tickets, fn($t) => !in_array($t['id'], $ids)));
|
||||
$deleted = $before - count($tickets);
|
||||
file_put_contents(TICKETS_FILE, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
addLog('ticket_delete', '', '', "Massapoisto: $deleted tikettiä");
|
||||
echo json_encode(['success' => true, 'deleted' => $deleted]);
|
||||
break;
|
||||
|
||||
case 'ticket_rule_delete':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$ruleId = $input['id'] ?? '';
|
||||
$config = loadConfig();
|
||||
$rules = $config['ticket_rules'] ?? [];
|
||||
$config['ticket_rules'] = array_values(array_filter($rules, fn($r) => $r['id'] !== $ruleId));
|
||||
file_put_contents(DATA_DIR . '/config.json', json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tuntematon toiminto']);
|
||||
|
||||
69
index.html
69
index.html
@@ -264,12 +264,20 @@
|
||||
<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-rules" style="padding:7px 14px;font-size:0.82rem;">⚙ Säännöt</button>
|
||||
</div>
|
||||
<div id="ticket-fetch-status" style="display:none;padding:0.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:0.9rem;"></div>
|
||||
<div id="bulk-actions-toolbar" style="display:none;align-items:center;gap:0.75rem;padding:0.6rem 1rem;background:#fff3cd;border:1px solid #ffc107;border-radius:8px;margin-bottom:0.75rem;font-size:0.85rem;">
|
||||
<span id="bulk-count" style="font-weight:600;">0 valittu</span>
|
||||
<button class="btn-secondary" onclick="bulkCloseSelected()" style="padding:5px 14px;font-size:0.82rem;color:#555;border-color:#ddd;">Sulje valitut</button>
|
||||
<button class="btn-danger" onclick="bulkDeleteSelected()" style="padding:5px 14px;font-size:0.82rem;">Poista valitut</button>
|
||||
<button style="background:none;border:none;cursor:pointer;font-size:0.82rem;color:#888;margin-left:auto;" onclick="bulkSelectedIds.clear();document.querySelectorAll('.ticket-checkbox,#bulk-select-all').forEach(c=>c.checked=false);updateBulkToolbar();">Tyhjennä valinta</button>
|
||||
</div>
|
||||
<div class="table-card">
|
||||
<table id="tickets-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px;"><input type="checkbox" id="bulk-select-all" title="Valitse kaikki"></th>
|
||||
<th>Tila</th>
|
||||
<th>Tyyppi</th>
|
||||
<th>Aihe</th>
|
||||
@@ -312,6 +320,67 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sääntönäkymä -->
|
||||
<div id="ticket-rules-view" style="display:none;">
|
||||
<button class="btn-secondary" id="btn-rules-back" style="color:#555;border-color:#ddd;margin-bottom:1rem;">← 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;">Automaattisäännöt</h3>
|
||||
<button class="btn-primary" id="btn-add-rule" style="font-size:0.85rem;">+ Lisää sääntö</button>
|
||||
</div>
|
||||
<p style="color:#888;font-size:0.85rem;margin-bottom:1rem;">Säännöt soveltuvat automaattisesti uusiin tiketteihin haettaessa sähköposteja. Ensimmäinen täsmäävä sääntö voittaa.</p>
|
||||
<div id="rules-list"></div>
|
||||
</div>
|
||||
<!-- Sääntölomake -->
|
||||
<div id="rule-form-container" class="table-card" style="padding:1.5rem;margin-top:1rem;display:none;">
|
||||
<h4 style="color:#0f3460;margin-bottom:1rem;" id="rule-form-title">Uusi sääntö</h4>
|
||||
<input type="hidden" id="rule-form-id">
|
||||
<div class="form-grid" style="max-width:600px;">
|
||||
<div class="form-group full-width">
|
||||
<label>Säännön nimi *</label>
|
||||
<input type="text" id="rule-form-name" placeholder="esim. Sulje notifikaatiot">
|
||||
</div>
|
||||
<div class="form-group full-width" style="margin-top:0.5rem;">
|
||||
<label style="font-weight:600;color:#0f3460;">Ehdot (molemmat pitää täsmätä jos täytetty)</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Lähettäjä sisältää</label>
|
||||
<input type="text" id="rule-form-from" placeholder="esim. noreply@">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Otsikko sisältää</label>
|
||||
<input type="text" id="rule-form-subject" placeholder="esim. saatavuuskysely">
|
||||
</div>
|
||||
<div class="form-group full-width" style="margin-top:0.5rem;">
|
||||
<label style="font-weight:600;color:#0f3460;">Toimenpiteet</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aseta tila</label>
|
||||
<select id="rule-form-status">
|
||||
<option value="">Ei muuteta</option>
|
||||
<option value="suljettu">Suljettu</option>
|
||||
<option value="kasittelyssa">Käsittelyssä</option>
|
||||
<option value="ratkaistu">Ratkaistu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aseta tyyppi</label>
|
||||
<select id="rule-form-type">
|
||||
<option value="">Ei muuteta</option>
|
||||
<option value="laskutus">Laskutus</option>
|
||||
<option value="tekniikka">Tekniikka</option>
|
||||
<option value="vika">Vika</option>
|
||||
<option value="muu">Muu</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
|
||||
<button class="btn-primary" id="btn-save-rule">Tallenna</button>
|
||||
<button class="btn-secondary" id="btn-cancel-rule">Peruuta</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
180
script.js
180
script.js
@@ -1016,7 +1016,9 @@ function renderTickets() {
|
||||
const lastType = t.last_message_type === 'reply_out' ? '→' : (t.last_message_type === 'note' ? '📝' : '←');
|
||||
const typeLabel = ticketTypeLabels[t.type] || 'Muu';
|
||||
const rowClass = t.status === 'kasittelyssa' ? 'ticket-row-active' : '';
|
||||
const checked = bulkSelectedIds.has(t.id) ? 'checked' : '';
|
||||
return `<tr data-ticket-id="${t.id}" class="${rowClass}">
|
||||
<td onclick="event.stopPropagation()"><input type="checkbox" class="ticket-checkbox" data-ticket-id="${t.id}" ${checked}></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><strong>${esc(t.subject)}</strong></td>
|
||||
@@ -1026,6 +1028,14 @@ function renderTickets() {
|
||||
<td class="nowrap">${esc((t.updated || '').substring(0, 16))}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
// Re-attach checkbox listeners
|
||||
document.querySelectorAll('.ticket-checkbox').forEach(cb => {
|
||||
cb.addEventListener('change', function() {
|
||||
if (this.checked) bulkSelectedIds.add(this.dataset.ticketId);
|
||||
else bulkSelectedIds.delete(this.dataset.ticketId);
|
||||
updateBulkToolbar();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const openCount = tickets.filter(t => t.status !== 'suljettu').length;
|
||||
@@ -1045,6 +1055,15 @@ document.getElementById('ticket-search-input').addEventListener('input', () => r
|
||||
document.getElementById('ticket-status-filter').addEventListener('change', () => renderTickets());
|
||||
document.getElementById('ticket-type-filter').addEventListener('change', () => renderTickets());
|
||||
document.getElementById('ticket-show-closed').addEventListener('change', () => renderTickets());
|
||||
document.getElementById('bulk-select-all').addEventListener('change', function() {
|
||||
const checkboxes = document.querySelectorAll('.ticket-checkbox');
|
||||
checkboxes.forEach(cb => {
|
||||
cb.checked = this.checked;
|
||||
if (this.checked) bulkSelectedIds.add(cb.dataset.ticketId);
|
||||
else bulkSelectedIds.delete(cb.dataset.ticketId);
|
||||
});
|
||||
updateBulkToolbar();
|
||||
});
|
||||
|
||||
document.getElementById('tickets-tbody').addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr');
|
||||
@@ -1187,8 +1206,14 @@ async function showTicketDetail(id) {
|
||||
|
||||
function showTicketListView() {
|
||||
document.getElementById('ticket-detail-view').style.display = 'none';
|
||||
document.getElementById('ticket-rules-view').style.display = 'none';
|
||||
document.getElementById('ticket-list-view').style.display = 'block';
|
||||
currentTicketId = null;
|
||||
// Reset bulk selection
|
||||
bulkSelectedIds.clear();
|
||||
const selectAll = document.getElementById('bulk-select-all');
|
||||
if (selectAll) selectAll.checked = false;
|
||||
updateBulkToolbar();
|
||||
}
|
||||
|
||||
document.getElementById('btn-ticket-back').addEventListener('click', () => {
|
||||
@@ -1266,6 +1291,161 @@ document.getElementById('btn-fetch-emails').addEventListener('click', async () =
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== TICKET RULES (AUTOMAATTISÄÄNNÖT) ====================
|
||||
|
||||
let ticketRules = [];
|
||||
let editingRuleId = null;
|
||||
|
||||
async function loadRules() {
|
||||
try {
|
||||
ticketRules = await apiCall('ticket_rules');
|
||||
renderRules();
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderRules() {
|
||||
const list = document.getElementById('rules-list');
|
||||
if (ticketRules.length === 0) {
|
||||
list.innerHTML = '<div style="text-align:center;padding:2rem;color:#aaa;">Ei sääntöjä vielä. Lisää ensimmäinen sääntö.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = ticketRules.map(r => {
|
||||
const conditions = [];
|
||||
if (r.from_contains) conditions.push('Lähettäjä: <strong>' + esc(r.from_contains) + '</strong>');
|
||||
if (r.subject_contains) conditions.push('Otsikko: <strong>' + esc(r.subject_contains) + '</strong>');
|
||||
const actions = [];
|
||||
if (r.set_status) actions.push('Tila → ' + (ticketStatusLabels[r.set_status] || r.set_status));
|
||||
if (r.set_type) actions.push('Tyyppi → ' + (ticketTypeLabels[r.set_type] || r.set_type));
|
||||
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem 1rem;background:${r.enabled ? '#f8f9fb' : '#fafafa'};border:1px solid #e8ebf0;border-radius:8px;margin-bottom:0.5rem;opacity:${r.enabled ? '1' : '0.5'};">
|
||||
<div>
|
||||
<div style="font-weight:600;color:#0f3460;font-size:0.9rem;">${esc(r.name)}</div>
|
||||
<div style="font-size:0.8rem;color:#888;margin-top:2px;">
|
||||
${conditions.length ? 'Ehdot: ' + conditions.join(', ') : '<em>Ei ehtoja</em>'} → ${actions.length ? actions.join(', ') : '<em>Ei toimenpiteitä</em>'}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.4rem;align-items:center;flex-shrink:0;">
|
||||
<label style="cursor:pointer;font-size:0.8rem;color:#888;display:flex;align-items:center;gap:3px;">
|
||||
<input type="checkbox" ${r.enabled ? 'checked' : ''} onchange="toggleRule('${r.id}', this.checked)"> Päällä
|
||||
</label>
|
||||
<button onclick="editRule('${r.id}')" style="background:none;border:none;cursor:pointer;font-size:1rem;padding:4px;">✎</button>
|
||||
<button onclick="deleteRule('${r.id}')" style="background:none;border:none;cursor:pointer;font-size:1rem;padding:4px;color:#e74c3c;">🗑</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showRulesView() {
|
||||
document.getElementById('ticket-list-view').style.display = 'none';
|
||||
document.getElementById('ticket-detail-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ö';
|
||||
document.getElementById('rule-form-id').value = rule ? rule.id : '';
|
||||
document.getElementById('rule-form-name').value = rule ? rule.name : '';
|
||||
document.getElementById('rule-form-from').value = rule ? rule.from_contains : '';
|
||||
document.getElementById('rule-form-subject').value = rule ? rule.subject_contains : '';
|
||||
document.getElementById('rule-form-status').value = rule ? (rule.set_status || '') : '';
|
||||
document.getElementById('rule-form-type').value = rule ? (rule.set_type || '') : '';
|
||||
editingRuleId = rule ? rule.id : null;
|
||||
}
|
||||
|
||||
function hideRuleForm() {
|
||||
document.getElementById('rule-form-container').style.display = 'none';
|
||||
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());
|
||||
|
||||
document.getElementById('btn-save-rule').addEventListener('click', async () => {
|
||||
const name = document.getElementById('rule-form-name').value.trim();
|
||||
if (!name) { alert('Nimi puuttuu'); return; }
|
||||
const data = {
|
||||
name,
|
||||
from_contains: document.getElementById('rule-form-from').value.trim(),
|
||||
subject_contains: document.getElementById('rule-form-subject').value.trim(),
|
||||
set_status: document.getElementById('rule-form-status').value,
|
||||
set_type: document.getElementById('rule-form-type').value,
|
||||
enabled: true,
|
||||
};
|
||||
const existingId = document.getElementById('rule-form-id').value;
|
||||
if (existingId) data.id = existingId;
|
||||
try {
|
||||
await apiCall('ticket_rule_save', 'POST', data);
|
||||
hideRuleForm();
|
||||
await loadRules();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
async function editRule(id) {
|
||||
const rule = ticketRules.find(r => r.id === id);
|
||||
if (rule) showRuleForm(rule);
|
||||
}
|
||||
|
||||
async function deleteRule(id) {
|
||||
if (!confirm('Poistetaanko sääntö?')) return;
|
||||
try {
|
||||
await apiCall('ticket_rule_delete', 'POST', { id });
|
||||
await loadRules();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function toggleRule(id, enabled) {
|
||||
const rule = ticketRules.find(r => r.id === id);
|
||||
if (!rule) return;
|
||||
try {
|
||||
await apiCall('ticket_rule_save', 'POST', { ...rule, enabled });
|
||||
await loadRules();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
// ==================== BULK ACTIONS ====================
|
||||
|
||||
let bulkSelectedIds = new Set();
|
||||
|
||||
function updateBulkToolbar() {
|
||||
const toolbar = document.getElementById('bulk-actions-toolbar');
|
||||
if (bulkSelectedIds.size > 0) {
|
||||
toolbar.style.display = 'flex';
|
||||
document.getElementById('bulk-count').textContent = bulkSelectedIds.size + ' valittu';
|
||||
} else {
|
||||
toolbar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkCloseSelected() {
|
||||
if (bulkSelectedIds.size === 0) return;
|
||||
if (!confirm(`Suljetaanko ${bulkSelectedIds.size} tikettiä?`)) return;
|
||||
try {
|
||||
await apiCall('ticket_bulk_status', 'POST', { ids: [...bulkSelectedIds], status: 'suljettu' });
|
||||
bulkSelectedIds.clear();
|
||||
updateBulkToolbar();
|
||||
await loadTickets();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function bulkDeleteSelected() {
|
||||
if (bulkSelectedIds.size === 0) return;
|
||||
if (!confirm(`Poistetaanko ${bulkSelectedIds.size} tikettiä pysyvästi?`)) return;
|
||||
try {
|
||||
await apiCall('ticket_bulk_delete', 'POST', { ids: [...bulkSelectedIds] });
|
||||
bulkSelectedIds.clear();
|
||||
updateBulkToolbar();
|
||||
await loadTickets();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
// ==================== SETTINGS ====================
|
||||
|
||||
async function loadSettings() {
|
||||
|
||||
Reference in New Issue
Block a user