diff --git a/api.php b/api.php
index 85996e7..9b96962 100644
--- a/api.php
+++ b/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']);
diff --git a/index.html b/index.html
index 4160ab8..ba2a939 100644
--- a/index.html
+++ b/index.html
@@ -264,12 +264,20 @@
+
+
+ 0 valittu
+
+
+
+
+ |
Tila |
Tyyppi |
Aihe |
@@ -312,6 +320,67 @@
+
+
+
+
+
+
+
Automaattisäännöt
+
+
+
Säännöt soveltuvat automaattisesti uusiin tiketteihin haettaessa sähköposteja. Ensimmäinen täsmäävä sääntö voittaa.
+
+
+
+
+
diff --git a/script.js b/script.js
index 68bc6e8..6630667 100644
--- a/script.js
+++ b/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 `
+ |
${ticketStatusLabels[t.status] || t.status} |
${typeLabel} |
${esc(t.subject)} |
@@ -1026,6 +1028,14 @@ function renderTickets() {
${esc((t.updated || '').substring(0, 16))} |
`;
}).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 = 'Ei sääntöjä vielä. Lisää ensimmäinen sääntö.
';
+ return;
+ }
+ list.innerHTML = ticketRules.map(r => {
+ const conditions = [];
+ if (r.from_contains) conditions.push('Lähettäjä: ' + esc(r.from_contains) + '');
+ if (r.subject_contains) conditions.push('Otsikko: ' + esc(r.subject_contains) + '');
+ 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 `
+
+
${esc(r.name)}
+
+ ${conditions.length ? 'Ehdot: ' + conditions.join(', ') : 'Ei ehtoja'} → ${actions.length ? actions.join(', ') : 'Ei toimenpiteitä'}
+
+
+
+
+
+
+
+
`;
+ }).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() {