Add Asiakaspalvelu email ticketing system
IMAP client for fetching emails from asiakaspalvelu@cuitunet.fi, Freshdesk-style ticket management with status tracking, message threading, reply/note functionality, and IMAP settings in API tab. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
550
api.php
550
api.php
@@ -18,6 +18,7 @@ define('LEADS_FILE', DATA_DIR . '/leads.json');
|
||||
define('TOKENS_FILE', DATA_DIR . '/reset_tokens.json');
|
||||
define('RATE_FILE', DATA_DIR . '/login_attempts.json');
|
||||
define('CONFIG_FILE', DATA_DIR . '/config.json');
|
||||
define('TICKETS_FILE', DATA_DIR . '/tickets.json');
|
||||
define('SITE_URL', 'https://intra.cuitunet.fi');
|
||||
|
||||
// Sähköpostiasetukset
|
||||
@@ -26,7 +27,7 @@ define('MAIL_FROM_NAME', 'CuituNet Intra');
|
||||
|
||||
// Varmista data-kansio ja tiedostot
|
||||
if (!file_exists(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
|
||||
foreach ([DATA_FILE, USERS_FILE, CHANGELOG_FILE, ARCHIVE_FILE, LEADS_FILE, TOKENS_FILE, RATE_FILE] as $f) {
|
||||
foreach ([DATA_FILE, USERS_FILE, CHANGELOG_FILE, ARCHIVE_FILE, LEADS_FILE, TOKENS_FILE, RATE_FILE, TICKETS_FILE] as $f) {
|
||||
if (!file_exists($f)) file_put_contents($f, '[]');
|
||||
}
|
||||
|
||||
@@ -116,6 +117,232 @@ function sendMail(string $to, string $subject, string $htmlBody): bool {
|
||||
return mail($to, $subject, $htmlBody, $headers, '-f ' . MAIL_FROM);
|
||||
}
|
||||
|
||||
// ==================== IMAP CLIENT ====================
|
||||
|
||||
class ImapClient {
|
||||
private $connection = null;
|
||||
|
||||
public function connect(array $config): bool {
|
||||
$host = $config['imap_host'] ?? '';
|
||||
$port = intval($config['imap_port'] ?? 993);
|
||||
$user = $config['imap_user'] ?? '';
|
||||
$pass = $config['imap_password'] ?? '';
|
||||
$encryption = $config['imap_encryption'] ?? 'ssl';
|
||||
|
||||
if (empty($host) || empty($user) || empty($pass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mailbox = '{' . $host . ':' . $port . '/imap/' . $encryption . '}INBOX';
|
||||
|
||||
// Suppress warnings, handle errors manually
|
||||
$this->connection = @imap_open($mailbox, $user, $pass, 0, 1);
|
||||
|
||||
if (!$this->connection) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function fetchMessages(int $limit = 50): array {
|
||||
if (!$this->connection) return [];
|
||||
|
||||
$messages = [];
|
||||
$check = imap_check($this->connection);
|
||||
if (!$check || $check->Nmsgs === 0) return [];
|
||||
|
||||
$start = max(1, $check->Nmsgs - $limit + 1);
|
||||
$end = $check->Nmsgs;
|
||||
|
||||
for ($i = $end; $i >= $start; $i--) {
|
||||
$header = @imap_headerinfo($this->connection, $i);
|
||||
if (!$header) continue;
|
||||
|
||||
$overview = @imap_fetch_overview($this->connection, strval($i), 0);
|
||||
|
||||
// Decode subject
|
||||
$subject = '';
|
||||
if (isset($header->subject)) {
|
||||
$decoded = imap_mime_header_decode($header->subject);
|
||||
foreach ($decoded as $part) {
|
||||
$subject .= $part->text;
|
||||
}
|
||||
}
|
||||
|
||||
// From
|
||||
$fromEmail = '';
|
||||
$fromName = '';
|
||||
if (isset($header->from[0])) {
|
||||
$fromEmail = $header->from[0]->mailbox . '@' . ($header->from[0]->host ?? '');
|
||||
if (isset($header->from[0]->personal)) {
|
||||
$decoded = imap_mime_header_decode($header->from[0]->personal);
|
||||
foreach ($decoded as $part) {
|
||||
$fromName .= $part->text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message-ID
|
||||
$messageId = isset($header->message_id) ? trim($header->message_id) : '';
|
||||
|
||||
// In-Reply-To
|
||||
$inReplyTo = '';
|
||||
if (isset($header->in_reply_to)) {
|
||||
$inReplyTo = trim($header->in_reply_to);
|
||||
}
|
||||
|
||||
// References
|
||||
$references = '';
|
||||
if (isset($header->references)) {
|
||||
$references = trim($header->references);
|
||||
}
|
||||
|
||||
// Date
|
||||
$date = isset($header->date) ? date('Y-m-d H:i:s', strtotime($header->date)) : date('Y-m-d H:i:s');
|
||||
|
||||
// Body — prefer plain text
|
||||
$body = $this->getBody($i);
|
||||
|
||||
$messages[] = [
|
||||
'subject' => $subject,
|
||||
'from_email' => $fromEmail,
|
||||
'from_name' => $fromName,
|
||||
'message_id' => $messageId,
|
||||
'in_reply_to' => $inReplyTo,
|
||||
'references' => $references,
|
||||
'date' => $date,
|
||||
'body' => $body,
|
||||
];
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private function getBody(int $msgNum): string {
|
||||
if (!$this->connection) return '';
|
||||
|
||||
$structure = @imap_fetchstructure($this->connection, $msgNum);
|
||||
if (!$structure) return '';
|
||||
|
||||
// Simple message (no parts)
|
||||
if (empty($structure->parts)) {
|
||||
$body = imap_fetchbody($this->connection, $msgNum, '1');
|
||||
return $this->decodeBody($body, $structure->encoding ?? 0);
|
||||
}
|
||||
|
||||
// Multipart — look for text/plain first, then text/html
|
||||
$plainBody = '';
|
||||
$htmlBody = '';
|
||||
|
||||
foreach ($structure->parts as $partNum => $part) {
|
||||
$partIndex = strval($partNum + 1);
|
||||
|
||||
if ($part->type === 0) { // TEXT
|
||||
$subtype = strtolower($part->subtype ?? '');
|
||||
$body = imap_fetchbody($this->connection, $msgNum, $partIndex);
|
||||
$decoded = $this->decodeBody($body, $part->encoding ?? 0);
|
||||
|
||||
// Handle charset
|
||||
$charset = $this->getCharset($part);
|
||||
if ($charset && strtolower($charset) !== 'utf-8') {
|
||||
$converted = @iconv($charset, 'UTF-8//IGNORE', $decoded);
|
||||
if ($converted !== false) $decoded = $converted;
|
||||
}
|
||||
|
||||
if ($subtype === 'plain') {
|
||||
$plainBody = $decoded;
|
||||
} elseif ($subtype === 'html') {
|
||||
$htmlBody = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($plainBody) return trim($plainBody);
|
||||
if ($htmlBody) return trim(strip_tags($htmlBody));
|
||||
return '';
|
||||
}
|
||||
|
||||
private function decodeBody(string $body, int $encoding): string {
|
||||
switch ($encoding) {
|
||||
case 3: return base64_decode($body); // BASE64
|
||||
case 4: return quoted_printable_decode($body); // QUOTED-PRINTABLE
|
||||
default: return $body;
|
||||
}
|
||||
}
|
||||
|
||||
private function getCharset($part): string {
|
||||
if (!isset($part->parameters)) return '';
|
||||
foreach ($part->parameters as $param) {
|
||||
if (strtolower($param->attribute) === 'charset') {
|
||||
return $param->value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function disconnect(): void {
|
||||
if ($this->connection) {
|
||||
@imap_close($this->connection);
|
||||
$this->connection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TICKETS ====================
|
||||
|
||||
function loadTickets(): array {
|
||||
return json_decode(file_get_contents(TICKETS_FILE), true) ?: [];
|
||||
}
|
||||
|
||||
function saveTickets(array $tickets): void {
|
||||
file_put_contents(TICKETS_FILE, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
function findTicketByMessageId(array $tickets, string $messageId): ?int {
|
||||
foreach ($tickets as $i => $t) {
|
||||
if ($t['message_id'] === $messageId) return $i;
|
||||
foreach ($t['messages'] ?? [] as $m) {
|
||||
if (($m['message_id'] ?? '') === $messageId) return $i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTicketByReferences(array $tickets, string $inReplyTo, string $references): ?int {
|
||||
// Check In-Reply-To header
|
||||
if ($inReplyTo) {
|
||||
$idx = findTicketByMessageId($tickets, $inReplyTo);
|
||||
if ($idx !== null) return $idx;
|
||||
}
|
||||
// Check References header
|
||||
if ($references) {
|
||||
$refs = preg_split('/\s+/', $references);
|
||||
foreach ($refs as $ref) {
|
||||
$ref = trim($ref);
|
||||
if (!$ref) continue;
|
||||
$idx = findTicketByMessageId($tickets, $ref);
|
||||
if ($idx !== null) return $idx;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = ''): bool {
|
||||
$config = loadConfig();
|
||||
$fromEmail = $config['imap_user'] ?? MAIL_FROM;
|
||||
$fromName = 'CuituNet Asiakaspalvelu';
|
||||
|
||||
$headers = "MIME-Version: 1.0\r\n";
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$headers .= "From: {$fromName} <{$fromEmail}>\r\n";
|
||||
$headers .= "Reply-To: {$fromEmail}\r\n";
|
||||
if ($inReplyTo) {
|
||||
$headers .= "In-Reply-To: {$inReplyTo}\r\n";
|
||||
$headers .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
|
||||
}
|
||||
return mail($to, $subject, $body, $headers, '-f ' . $fromEmail);
|
||||
}
|
||||
|
||||
// ==================== USERS ====================
|
||||
|
||||
function initUsers(): void {
|
||||
@@ -339,6 +566,12 @@ switch ($action) {
|
||||
$origins = array_filter(array_map('trim', explode("\n", $input['cors_origins'])));
|
||||
$config['cors_origins'] = array_values($origins);
|
||||
}
|
||||
// IMAP-asetukset
|
||||
if (isset($input['imap_host'])) $config['imap_host'] = trim($input['imap_host']);
|
||||
if (isset($input['imap_port'])) $config['imap_port'] = intval($input['imap_port']);
|
||||
if (isset($input['imap_user'])) $config['imap_user'] = trim($input['imap_user']);
|
||||
if (isset($input['imap_password'])) $config['imap_password'] = $input['imap_password'];
|
||||
if (isset($input['imap_encryption'])) $config['imap_encryption'] = trim($input['imap_encryption']);
|
||||
saveConfig($config);
|
||||
addLog('config_update', '', '', 'Päivitti asetukset');
|
||||
echo json_encode($config);
|
||||
@@ -996,6 +1229,321 @@ switch ($action) {
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
// ---------- TICKETS ----------
|
||||
case 'tickets':
|
||||
requireAuth();
|
||||
$tickets = loadTickets();
|
||||
// Palauta ilman viestisisältöjä (lista-näkymä)
|
||||
$list = array_map(function($t) {
|
||||
$msgCount = count($t['messages'] ?? []);
|
||||
$lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null;
|
||||
return [
|
||||
'id' => $t['id'],
|
||||
'subject' => $t['subject'],
|
||||
'from_email' => $t['from_email'],
|
||||
'from_name' => $t['from_name'],
|
||||
'status' => $t['status'],
|
||||
'assigned_to' => $t['assigned_to'] ?? '',
|
||||
'created' => $t['created'],
|
||||
'updated' => $t['updated'],
|
||||
'message_count' => $msgCount,
|
||||
'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '',
|
||||
'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '',
|
||||
];
|
||||
}, $tickets);
|
||||
echo json_encode($list);
|
||||
break;
|
||||
|
||||
case 'ticket_detail':
|
||||
requireAuth();
|
||||
$id = $_GET['id'] ?? '';
|
||||
$tickets = loadTickets();
|
||||
$ticket = null;
|
||||
foreach ($tickets as $t) {
|
||||
if ($t['id'] === $id) { $ticket = $t; break; }
|
||||
}
|
||||
if (!$ticket) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
||||
break;
|
||||
}
|
||||
echo json_encode($ticket);
|
||||
break;
|
||||
|
||||
case 'ticket_fetch':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$config = loadConfig();
|
||||
if (empty($config['imap_host']) || empty($config['imap_user']) || empty($config['imap_password'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'IMAP-asetukset puuttuvat. Aseta ne API-välilehdellä.']);
|
||||
break;
|
||||
}
|
||||
|
||||
$imap = new ImapClient();
|
||||
if (!$imap->connect($config)) {
|
||||
$errors = imap_errors();
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'IMAP-yhteys epäonnistui' . ($errors ? ': ' . implode(', ', $errors) : '')]);
|
||||
break;
|
||||
}
|
||||
|
||||
$emails = $imap->fetchMessages(100);
|
||||
$imap->disconnect();
|
||||
|
||||
$tickets = loadTickets();
|
||||
$newCount = 0;
|
||||
$threadedCount = 0;
|
||||
|
||||
// Collect all existing message IDs for duplicate detection
|
||||
$existingMsgIds = [];
|
||||
foreach ($tickets as $t) {
|
||||
if ($t['message_id']) $existingMsgIds[$t['message_id']] = true;
|
||||
foreach ($t['messages'] ?? [] as $m) {
|
||||
if (!empty($m['message_id'])) $existingMsgIds[$m['message_id']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($emails as $email) {
|
||||
// Skip duplicates
|
||||
if (!empty($email['message_id']) && isset($existingMsgIds[$email['message_id']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$msg = [
|
||||
'id' => generateId(),
|
||||
'type' => 'email_in',
|
||||
'from' => $email['from_email'],
|
||||
'from_name' => $email['from_name'],
|
||||
'body' => $email['body'],
|
||||
'timestamp' => $email['date'],
|
||||
'message_id' => $email['message_id'],
|
||||
];
|
||||
|
||||
// Try to thread into existing ticket
|
||||
$ticketIdx = findTicketByReferences($tickets, $email['in_reply_to'], $email['references']);
|
||||
|
||||
if ($ticketIdx !== null) {
|
||||
$tickets[$ticketIdx]['messages'][] = $msg;
|
||||
$tickets[$ticketIdx]['updated'] = $email['date'];
|
||||
// If ticket was resolved/closed, reopen it
|
||||
if (in_array($tickets[$ticketIdx]['status'], ['ratkaistu', 'suljettu'])) {
|
||||
$tickets[$ticketIdx]['status'] = 'kasittelyssa';
|
||||
}
|
||||
$threadedCount++;
|
||||
} else {
|
||||
// New ticket
|
||||
$ticket = [
|
||||
'id' => generateId(),
|
||||
'subject' => $email['subject'] ?: '(Ei aihetta)',
|
||||
'from_email' => $email['from_email'],
|
||||
'from_name' => $email['from_name'],
|
||||
'status' => 'uusi',
|
||||
'assigned_to' => '',
|
||||
'created' => $email['date'],
|
||||
'updated' => $email['date'],
|
||||
'message_id' => $email['message_id'],
|
||||
'messages' => [$msg],
|
||||
];
|
||||
$tickets[] = $ticket;
|
||||
$newCount++;
|
||||
}
|
||||
|
||||
if ($email['message_id']) $existingMsgIds[$email['message_id']] = true;
|
||||
}
|
||||
|
||||
// Sort tickets by updated date (newest first)
|
||||
usort($tickets, function($a, $b) {
|
||||
return strcmp($b['updated'], $a['updated']);
|
||||
});
|
||||
|
||||
saveTickets($tickets);
|
||||
addLog('ticket_fetch', '', '', "Haettu sähköpostit: {$newCount} uutta tikettiä, {$threadedCount} ketjutettu");
|
||||
echo json_encode(['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($tickets)]);
|
||||
break;
|
||||
|
||||
case 'ticket_reply':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$body = trim($input['body'] ?? '');
|
||||
if (empty($body)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Viesti ei voi olla tyhjä']);
|
||||
break;
|
||||
}
|
||||
$tickets = loadTickets();
|
||||
$found = false;
|
||||
foreach ($tickets as &$t) {
|
||||
if ($t['id'] === $id) {
|
||||
// Find last message_id for threading
|
||||
$lastMsgId = $t['message_id'] ?? '';
|
||||
$allRefs = $lastMsgId;
|
||||
foreach ($t['messages'] as $m) {
|
||||
if (!empty($m['message_id'])) {
|
||||
$lastMsgId = $m['message_id'];
|
||||
$allRefs .= ' ' . $m['message_id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Send email
|
||||
$subject = 'Re: ' . $t['subject'];
|
||||
$sent = sendTicketMail($t['from_email'], $subject, $body, $lastMsgId, trim($allRefs));
|
||||
|
||||
if (!$sent) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Sähköpostin lähetys epäonnistui']);
|
||||
break 2;
|
||||
}
|
||||
|
||||
// Add reply to ticket
|
||||
$reply = [
|
||||
'id' => generateId(),
|
||||
'type' => 'reply_out',
|
||||
'from' => currentUser(),
|
||||
'from_name' => $_SESSION['nimi'] ?? currentUser(),
|
||||
'body' => $body,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'message_id' => '',
|
||||
];
|
||||
$t['messages'][] = $reply;
|
||||
$t['updated'] = date('Y-m-d H:i:s');
|
||||
if ($t['status'] === 'uusi') $t['status'] = 'kasittelyssa';
|
||||
|
||||
$found = true;
|
||||
addLog('ticket_reply', $t['id'], $t['subject'], 'Vastasi tikettiin');
|
||||
echo json_encode($t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($t);
|
||||
if (!$found) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
||||
break;
|
||||
}
|
||||
saveTickets($tickets);
|
||||
break;
|
||||
|
||||
case 'ticket_status':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$status = $input['status'] ?? '';
|
||||
$validStatuses = ['uusi', 'kasittelyssa', 'odottaa', 'ratkaistu', 'suljettu'];
|
||||
if (!in_array($status, $validStatuses)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Virheellinen tila']);
|
||||
break;
|
||||
}
|
||||
$tickets = loadTickets();
|
||||
$found = false;
|
||||
foreach ($tickets as &$t) {
|
||||
if ($t['id'] === $id) {
|
||||
$oldStatus = $t['status'];
|
||||
$t['status'] = $status;
|
||||
$t['updated'] = date('Y-m-d H:i:s');
|
||||
$found = true;
|
||||
addLog('ticket_status', $t['id'], $t['subject'], "Tila: {$oldStatus} → {$status}");
|
||||
echo json_encode($t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($t);
|
||||
if (!$found) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
||||
break;
|
||||
}
|
||||
saveTickets($tickets);
|
||||
break;
|
||||
|
||||
case 'ticket_assign':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$assignTo = trim($input['assigned_to'] ?? '');
|
||||
$tickets = loadTickets();
|
||||
$found = false;
|
||||
foreach ($tickets as &$t) {
|
||||
if ($t['id'] === $id) {
|
||||
$t['assigned_to'] = $assignTo;
|
||||
$t['updated'] = date('Y-m-d H:i:s');
|
||||
$found = true;
|
||||
addLog('ticket_assign', $t['id'], $t['subject'], "Osoitettu: {$assignTo}");
|
||||
echo json_encode($t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($t);
|
||||
if (!$found) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
||||
break;
|
||||
}
|
||||
saveTickets($tickets);
|
||||
break;
|
||||
|
||||
case 'ticket_note':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$body = trim($input['body'] ?? '');
|
||||
if (empty($body)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Muistiinpano ei voi olla tyhjä']);
|
||||
break;
|
||||
}
|
||||
$tickets = loadTickets();
|
||||
$found = false;
|
||||
foreach ($tickets as &$t) {
|
||||
if ($t['id'] === $id) {
|
||||
$note = [
|
||||
'id' => generateId(),
|
||||
'type' => 'note',
|
||||
'from' => currentUser(),
|
||||
'from_name' => $_SESSION['nimi'] ?? currentUser(),
|
||||
'body' => $body,
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'message_id' => '',
|
||||
];
|
||||
$t['messages'][] = $note;
|
||||
$t['updated'] = date('Y-m-d H:i:s');
|
||||
$found = true;
|
||||
addLog('ticket_note', $t['id'], $t['subject'], 'Lisäsi muistiinpanon');
|
||||
echo json_encode($t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($t);
|
||||
if (!$found) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tikettiä ei löydy']);
|
||||
break;
|
||||
}
|
||||
saveTickets($tickets);
|
||||
break;
|
||||
|
||||
case 'ticket_delete':
|
||||
requireAuth();
|
||||
if ($method !== 'POST') break;
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? '';
|
||||
$tickets = loadTickets();
|
||||
$deleted = null;
|
||||
foreach ($tickets as $t) {
|
||||
if ($t['id'] === $id) { $deleted = $t; break; }
|
||||
}
|
||||
$tickets = array_values(array_filter($tickets, fn($t) => $t['id'] !== $id));
|
||||
saveTickets($tickets);
|
||||
if ($deleted) addLog('ticket_delete', $id, $deleted['subject'] ?? '', 'Poisti tiketin');
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Tuntematon toiminto']);
|
||||
|
||||
Reference in New Issue
Block a user