Files
intra.cuitunet.fi/api.php
Jukka Lampikoski 42e3648e3d 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>
2026-03-10 08:52:00 +02:00

1552 lines
57 KiB
PHP

<?php
// Turvalliset session-asetukset
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_samesite', 'Strict');
session_start();
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
define('DATA_DIR', __DIR__ . '/data');
define('DATA_FILE', DATA_DIR . '/customers.json');
define('USERS_FILE', DATA_DIR . '/users.json');
define('CHANGELOG_FILE', DATA_DIR . '/changelog.json');
define('ARCHIVE_FILE', DATA_DIR . '/archive.json');
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
define('MAIL_FROM', 'sivusto@cuitunet.fi');
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, TICKETS_FILE] as $f) {
if (!file_exists($f)) file_put_contents($f, '[]');
}
initUsers();
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
// ==================== HELPERS ====================
function requireAuth() {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Kirjaudu sisään']);
exit;
}
}
function requireAdmin() {
requireAuth();
if (($_SESSION['role'] ?? '') !== 'admin') {
http_response_code(403);
echo json_encode(['error' => 'Vain ylläpitäjä voi tehdä tämän']);
exit;
}
}
function currentUser(): string {
return $_SESSION['username'] ?? 'tuntematon';
}
function generateId(): string {
return bin2hex(random_bytes(8));
}
function generateToken(): string {
return bin2hex(random_bytes(32));
}
// ==================== RATE LIMITING ====================
function checkRateLimit(string $ip): bool {
$attempts = json_decode(file_get_contents(RATE_FILE), true) ?: [];
$now = time();
// Siivoa vanhat (yli 15 min)
$attempts = array_filter($attempts, fn($a) => ($now - $a['time']) < 900);
file_put_contents(RATE_FILE, json_encode(array_values($attempts)));
// Laske tämän IP:n yritykset viimeisen 15 min aikana
$ipAttempts = array_filter($attempts, fn($a) => $a['ip'] === $ip);
return count($ipAttempts) < 10; // Max 10 yritystä / 15 min
}
function recordLoginAttempt(string $ip): void {
$attempts = json_decode(file_get_contents(RATE_FILE), true) ?: [];
$attempts[] = ['ip' => $ip, 'time' => time()];
file_put_contents(RATE_FILE, json_encode($attempts));
}
function getClientIp(): string {
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
// ==================== CONFIG ====================
function loadConfig(): array {
if (!file_exists(CONFIG_FILE)) return [];
return json_decode(file_get_contents(CONFIG_FILE), true) ?: [];
}
function saveConfig(array $config): void {
file_put_contents(CONFIG_FILE, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
function normalizeAddress(string $addr): string {
$addr = strtolower(trim($addr));
$addr = preg_replace('/\s+/', ' ', $addr);
return $addr;
}
// ==================== EMAIL ====================
function sendMail(string $to, string $subject, string $htmlBody): bool {
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
$headers .= "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n";
$headers .= "Reply-To: " . MAIL_FROM . "\r\n";
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 {
$users = json_decode(file_get_contents(USERS_FILE), true) ?: [];
if (empty($users)) {
$users[] = [
'id' => generateId(),
'username' => 'admin',
'password_hash' => password_hash('cuitunet2024', PASSWORD_DEFAULT),
'nimi' => 'Ylläpitäjä',
'email' => '',
'role' => 'admin',
'luotu' => date('Y-m-d H:i:s'),
];
file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
}
function loadUsers(): array {
return json_decode(file_get_contents(USERS_FILE), true) ?: [];
}
function saveUsers(array $users): void {
file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
// ==================== RESET TOKENS ====================
function saveToken(string $userId, string $token): void {
$tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
// Poista vanhat tokenit tälle käyttäjälle
$tokens = array_filter($tokens, fn($t) => $t['user_id'] !== $userId);
$tokens[] = [
'user_id' => $userId,
'token' => hash('sha256', $token),
'expires' => time() + 3600, // 1 tunti
];
file_put_contents(TOKENS_FILE, json_encode(array_values($tokens)));
}
function validateToken(string $token): ?string {
$tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
$hashed = hash('sha256', $token);
$now = time();
foreach ($tokens as $t) {
if ($t['token'] === $hashed && $t['expires'] > $now) {
return $t['user_id'];
}
}
return null;
}
function removeToken(string $token): void {
$tokens = json_decode(file_get_contents(TOKENS_FILE), true) ?: [];
$hashed = hash('sha256', $token);
$tokens = array_filter($tokens, fn($t) => $t['token'] !== $hashed);
file_put_contents(TOKENS_FILE, json_encode(array_values($tokens)));
}
// ==================== CHANGELOG ====================
function addLog(string $action, string $customerId = '', string $customerName = '', string $details = ''): void {
$log = json_decode(file_get_contents(CHANGELOG_FILE), true) ?: [];
array_unshift($log, [
'id' => generateId(),
'timestamp' => date('Y-m-d H:i:s'),
'user' => currentUser(),
'action' => $action,
'customer_id' => $customerId,
'customer_name' => $customerName,
'details' => $details,
]);
if (count($log) > 500) $log = array_slice($log, 0, 500);
file_put_contents(CHANGELOG_FILE, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
// ==================== CUSTOMERS ====================
function loadCustomers(): array {
$data = file_get_contents(DATA_FILE);
$customers = json_decode($data, true) ?: [];
$migrated = false;
foreach ($customers as &$c) {
if (!isset($c['liittymat'])) {
$c['liittymat'] = [[
'asennusosoite' => $c['asennusosoite'] ?? '',
'postinumero' => $c['postinumero'] ?? '',
'kaupunki' => $c['kaupunki'] ?? '',
'liittymanopeus' => $c['liittymanopeus'] ?? '',
'hinta' => floatval($c['hinta'] ?? 0),
'sopimuskausi' => '',
'alkupvm' => '',
]];
unset($c['asennusosoite'], $c['postinumero'], $c['kaupunki'], $c['liittymanopeus'], $c['hinta']);
$migrated = true;
}
}
unset($c);
if ($migrated) {
file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
return $customers;
}
function saveCustomers(array $customers): void {
if (file_exists(DATA_FILE) && filesize(DATA_FILE) > 2) {
$backupDir = DATA_DIR . '/backups';
if (!file_exists($backupDir)) mkdir($backupDir, 0755, true);
copy(DATA_FILE, $backupDir . '/customers_' . date('Y-m-d_His') . '.json');
$backups = glob($backupDir . '/customers_*.json');
if (count($backups) > 30) {
sort($backups);
array_map('unlink', array_slice($backups, 0, count($backups) - 30));
}
}
file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
function loadArchive(): array {
return json_decode(file_get_contents(ARCHIVE_FILE), true) ?: [];
}
function saveArchive(array $archive): void {
file_put_contents(ARCHIVE_FILE, json_encode($archive, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
function parseLiittymat(array $input): array {
$liittymat = [];
foreach (($input['liittymat'] ?? []) as $l) {
$liittymat[] = [
'asennusosoite' => trim($l['asennusosoite'] ?? ''),
'postinumero' => trim($l['postinumero'] ?? ''),
'kaupunki' => trim($l['kaupunki'] ?? ''),
'liittymanopeus' => trim($l['liittymanopeus'] ?? ''),
'hinta' => floatval($l['hinta'] ?? 0),
'sopimuskausi' => trim($l['sopimuskausi'] ?? ''),
'alkupvm' => trim($l['alkupvm'] ?? ''),
];
}
if (empty($liittymat)) {
$liittymat[] = ['asennusosoite' => '', 'postinumero' => '', 'kaupunki' => '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => ''];
}
return $liittymat;
}
// ==================== ROUTES ====================
switch ($action) {
// ---------- SAATAVUUS (julkinen API) ----------
case 'saatavuus':
// CORS - salli cuitunet.fi
$config = loadConfig();
$allowedOrigins = $config['cors_origins'] ?? ['https://cuitunet.fi', 'https://www.cuitunet.fi'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins)) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
}
if ($method === 'OPTIONS') { http_response_code(204); break; }
// API-avain tarkistus
$apiKey = $config['api_key'] ?? '';
$providedKey = $_GET['key'] ?? ($_SERVER['HTTP_X_API_KEY'] ?? '');
if (empty($apiKey) || $providedKey !== $apiKey) {
http_response_code(403);
echo json_encode(['error' => 'Virheellinen API-avain']);
break;
}
// Parametrit: osoite (kadunnimi + numero), postinumero, kaupunki
$queryOsoite = normalizeAddress($_GET['osoite'] ?? '');
$queryPostinumero = trim($_GET['postinumero'] ?? '');
$queryKaupunki = strtolower(trim($_GET['kaupunki'] ?? ''));
if (empty($queryOsoite) || empty($queryPostinumero) || empty($queryKaupunki)) {
http_response_code(400);
echo json_encode(['error' => 'Anna osoite, postinumero ja kaupunki']);
break;
}
$customers = loadCustomers();
$found = false;
foreach ($customers as $c) {
foreach ($c['liittymat'] ?? [] as $l) {
$addr = normalizeAddress($l['asennusosoite'] ?? '');
$zip = trim($l['postinumero'] ?? '');
$city = strtolower(trim($l['kaupunki'] ?? ''));
// Kaikki kolme pitää mätsätä: osoite, postinumero, kaupunki
if ($zip === $queryPostinumero && $city === $queryKaupunki) {
// Osoite-match: tarkka sisältö-match
if (!empty($addr) && !empty($queryOsoite)) {
if (strpos($addr, $queryOsoite) !== false || strpos($queryOsoite, $addr) !== false) {
$found = true;
break 2;
}
}
}
}
}
// Palauta VAIN true/false - ei osoitteita, nopeuksia tai muuta dataa
echo json_encode(['saatavilla' => $found]);
break;
// ---------- CONFIG (admin) ----------
case 'config':
requireAdmin();
echo json_encode(loadConfig());
break;
case 'config_update':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$config = loadConfig();
if (isset($input['api_key'])) $config['api_key'] = trim($input['api_key']);
if (isset($input['cors_origins'])) {
$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);
break;
case 'generate_api_key':
requireAdmin();
if ($method !== 'POST') break;
$config = loadConfig();
$config['api_key'] = bin2hex(random_bytes(16));
saveConfig($config);
addLog('config_update', '', '', 'Generoi uuden API-avaimen');
echo json_encode($config);
break;
// ---------- CAPTCHA ----------
case 'captcha':
$a = rand(1, 20);
$b = rand(1, 20);
$_SESSION['captcha_answer'] = $a + $b;
echo json_encode(['question' => "$a + $b = ?"]);
break;
// ---------- AUTH ----------
case 'login':
if ($method !== 'POST') break;
$ip = getClientIp();
if (!checkRateLimit($ip)) {
http_response_code(429);
echo json_encode(['error' => 'Liian monta kirjautumisyritystä. Yritä uudelleen 15 minuutin kuluttua.']);
break;
}
$input = json_decode(file_get_contents('php://input'), true);
// Captcha-tarkistus
$captchaAnswer = intval($input['captcha'] ?? 0);
if (!isset($_SESSION['captcha_answer']) || $captchaAnswer !== $_SESSION['captcha_answer']) {
recordLoginAttempt($ip);
http_response_code(400);
echo json_encode(['error' => 'Virheellinen captcha-vastaus']);
unset($_SESSION['captcha_answer']);
break;
}
unset($_SESSION['captcha_answer']);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$users = loadUsers();
$found = false;
foreach ($users as $u) {
if ($u['username'] === $username && password_verify($password, $u['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user_id'] = $u['id'];
$_SESSION['username'] = $u['username'];
$_SESSION['nimi'] = $u['nimi'];
$_SESSION['role'] = $u['role'];
echo json_encode(['success' => true, 'username' => $u['username'], 'nimi' => $u['nimi'], 'role' => $u['role']]);
$found = true;
break;
}
}
if (!$found) {
recordLoginAttempt($ip);
http_response_code(401);
echo json_encode(['error' => 'Väärä käyttäjätunnus tai salasana']);
}
break;
case 'logout':
session_destroy();
echo json_encode(['success' => true]);
break;
case 'check_auth':
if (isset($_SESSION['user_id'])) {
echo json_encode([
'authenticated' => true,
'username' => $_SESSION['username'],
'nimi' => $_SESSION['nimi'],
'role' => $_SESSION['role'],
]);
} else {
echo json_encode(['authenticated' => false]);
}
break;
// ---------- PASSWORD RESET ----------
case 'password_reset_request':
if ($method !== 'POST') break;
$ip = getClientIp();
if (!checkRateLimit($ip)) {
http_response_code(429);
echo json_encode(['error' => 'Liian monta yritystä. Yritä uudelleen myöhemmin.']);
break;
}
recordLoginAttempt($ip);
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$users = loadUsers();
$user = null;
foreach ($users as $u) {
if ($u['username'] === $username) { $user = $u; break; }
}
// Palauta aina sama viesti (ei paljasta onko tunnus olemassa)
if ($user && !empty($user['email'])) {
$token = generateToken();
saveToken($user['id'], $token);
$resetUrl = SITE_URL . '/?reset=' . $token;
$html = '<div style="font-family:sans-serif;max-width:500px;margin:0 auto;">';
$html .= '<h2 style="color:#0f3460;">CuituNet Intra</h2>';
$html .= '<p>Hei ' . htmlspecialchars($user['nimi'] ?: $user['username']) . ',</p>';
$html .= '<p>Sait tämän viestin koska salasanan palautusta pyydettiin tilillesi.</p>';
$html .= '<p><a href="' . $resetUrl . '" style="display:inline-block;background:#0f3460;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;">Vaihda salasana</a></p>';
$html .= '<p style="color:#888;font-size:0.9em;">Linkki on voimassa 1 tunnin. Jos et pyytänyt salasanan vaihtoa, voit jättää tämän viestin huomiotta.</p>';
$html .= '</div>';
sendMail($user['email'], 'Salasanan palautus - CuituNet Intra', $html);
}
echo json_encode(['success' => true, 'message' => 'Jos käyttäjätunnus löytyy ja sillä on sähköposti, palautuslinkki lähetetään.']);
break;
case 'password_reset':
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$token = $input['token'] ?? '';
$newPassword = $input['password'] ?? '';
if (strlen($newPassword) < 4) {
http_response_code(400);
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
break;
}
$userId = validateToken($token);
if (!$userId) {
http_response_code(400);
echo json_encode(['error' => 'Palautuslinkki on vanhentunut tai virheellinen']);
break;
}
$users = loadUsers();
foreach ($users as &$u) {
if ($u['id'] === $userId) {
$u['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
break;
}
}
unset($u);
saveUsers($users);
removeToken($token);
echo json_encode(['success' => true, 'message' => 'Salasana vaihdettu onnistuneesti']);
break;
case 'validate_reset_token':
$token = $_GET['token'] ?? '';
$userId = validateToken($token);
echo json_encode(['valid' => $userId !== null]);
break;
// ---------- USERS ----------
case 'users':
requireAdmin();
$users = loadUsers();
$safe = array_map(function($u) {
unset($u['password_hash']);
return $u;
}, $users);
echo json_encode(array_values($safe));
break;
case 'user_create':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$nimi = trim($input['nimi'] ?? '');
$email = trim($input['email'] ?? '');
$role = ($input['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
if (empty($username) || empty($password)) {
http_response_code(400);
echo json_encode(['error' => 'Käyttäjätunnus ja salasana vaaditaan']);
break;
}
if (strlen($password) < 4) {
http_response_code(400);
echo json_encode(['error' => 'Salasanan pitää olla vähintään 4 merkkiä']);
break;
}
$users = loadUsers();
foreach ($users as $u) {
if ($u['username'] === $username) {
http_response_code(400);
echo json_encode(['error' => 'Käyttäjätunnus on jo käytössä']);
break 2;
}
}
$newUser = [
'id' => generateId(),
'username' => $username,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'nimi' => $nimi ?: $username,
'email' => $email,
'role' => $role,
'luotu' => date('Y-m-d H:i:s'),
];
$users[] = $newUser;
saveUsers($users);
addLog('user_create', '', '', "Lisäsi käyttäjän: {$username} ({$role})");
unset($newUser['password_hash']);
echo json_encode($newUser);
break;
case 'user_update':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$users = loadUsers();
$found = false;
foreach ($users as &$u) {
if ($u['id'] === $id) {
if (isset($input['nimi'])) $u['nimi'] = trim($input['nimi']);
if (isset($input['email'])) $u['email'] = trim($input['email']);
if (isset($input['role'])) $u['role'] = $input['role'] === 'admin' ? 'admin' : 'user';
if (!empty($input['password'])) {
$u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT);
}
$found = true;
addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}");
$safe = $u;
unset($safe['password_hash']);
echo json_encode($safe);
break;
}
}
unset($u);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Käyttäjää ei löydy']);
break;
}
saveUsers($users);
break;
case 'user_delete':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
if ($id === $_SESSION['user_id']) {
http_response_code(400);
echo json_encode(['error' => 'Et voi poistaa itseäsi']);
break;
}
$users = loadUsers();
$deleted = null;
foreach ($users as $u) {
if ($u['id'] === $id) { $deleted = $u; break; }
}
$users = array_values(array_filter($users, fn($u) => $u['id'] !== $id));
saveUsers($users);
if ($deleted) addLog('user_delete', '', '', "Poisti käyttäjän: {$deleted['username']}");
echo json_encode(['success' => true]);
break;
// ---------- CHANGELOG ----------
case 'changelog':
requireAuth();
$log = json_decode(file_get_contents(CHANGELOG_FILE), true) ?: [];
$limit = intval($_GET['limit'] ?? 100);
echo json_encode(array_slice($log, 0, $limit));
break;
// ---------- CUSTOMERS ----------
case 'customers':
requireAuth();
if ($method === 'GET') {
echo json_encode(loadCustomers());
}
break;
case 'customer':
requireAuth();
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$customers = loadCustomers();
$customer = [
'id' => generateId(),
'yritys' => trim($input['yritys'] ?? ''),
'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''),
'puhelin' => trim($input['puhelin'] ?? ''),
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
'laskutusosoite' => trim($input['laskutusosoite'] ?? ''),
'laskutuspostinumero' => trim($input['laskutuspostinumero'] ?? ''),
'laskutuskaupunki' => trim($input['laskutuskaupunki'] ?? ''),
'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''),
'elaskuosoite' => trim($input['elaskuosoite'] ?? ''),
'elaskuvalittaja' => trim($input['elaskuvalittaja'] ?? ''),
'ytunnus' => trim($input['ytunnus'] ?? ''),
'lisatiedot' => trim($input['lisatiedot'] ?? ''),
'liittymat' => parseLiittymat($input),
'luotu' => date('Y-m-d H:i:s'),
];
if (empty($customer['yritys'])) {
http_response_code(400);
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
break;
}
$customers[] = $customer;
saveCustomers($customers);
addLog('customer_create', $customer['id'], $customer['yritys'], 'Lisäsi asiakkaan');
echo json_encode($customer);
}
break;
case 'customer_update':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customers = loadCustomers();
$found = false;
foreach ($customers as &$c) {
if ($c['id'] === $id) {
$changes = [];
$fields = ['yritys','yhteyshenkilö','puhelin','sahkoposti','laskutusosoite','laskutuspostinumero','laskutuskaupunki','laskutussahkoposti','elaskuosoite','elaskuvalittaja','ytunnus','lisatiedot'];
foreach ($fields as $f) {
if (isset($input[$f])) {
$old = $c[$f] ?? '';
$new = trim($input[$f]);
if ($old !== $new) $changes[] = $f;
$c[$f] = $new;
}
}
if (isset($input['liittymat'])) {
$c['liittymat'] = parseLiittymat($input);
$changes[] = 'liittymat';
}
$c['muokattu'] = date('Y-m-d H:i:s');
$found = true;
addLog('customer_update', $c['id'], $c['yritys'], 'Muokkasi: ' . implode(', ', $changes));
echo json_encode($c);
break;
}
}
unset($c);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Asiakasta ei löydy']);
break;
}
saveCustomers($customers);
break;
case 'customer_delete':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$customers = loadCustomers();
$archived = null;
$remaining = [];
foreach ($customers as $c) {
if ($c['id'] === $id) {
$c['arkistoitu'] = date('Y-m-d H:i:s');
$c['arkistoija'] = currentUser();
$archived = $c;
} else {
$remaining[] = $c;
}
}
if ($archived) {
$archive = loadArchive();
$archive[] = $archived;
saveArchive($archive);
saveCustomers($remaining);
addLog('customer_archive', $archived['id'], $archived['yritys'], 'Arkistoi asiakkaan');
}
echo json_encode(['success' => true]);
break;
// ---------- ARCHIVE ----------
case 'archived_customers':
requireAuth();
echo json_encode(loadArchive());
break;
case 'customer_restore':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$archive = loadArchive();
$restored = null;
$remaining = [];
foreach ($archive as $c) {
if ($c['id'] === $id) {
unset($c['arkistoitu'], $c['arkistoija']);
$restored = $c;
} else {
$remaining[] = $c;
}
}
if ($restored) {
$customers = loadCustomers();
$customers[] = $restored;
saveCustomers($customers);
saveArchive($remaining);
addLog('customer_restore', $restored['id'], $restored['yritys'], 'Palautti asiakkaan arkistosta');
}
echo json_encode(['success' => true]);
break;
case 'customer_permanent_delete':
requireAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$archive = loadArchive();
$deleted = null;
foreach ($archive as $c) {
if ($c['id'] === $id) { $deleted = $c; break; }
}
$archive = array_values(array_filter($archive, fn($c) => $c['id'] !== $id));
saveArchive($archive);
$filesDir = DATA_DIR . '/files/' . $id;
if (is_dir($filesDir)) {
array_map('unlink', glob($filesDir . '/*'));
rmdir($filesDir);
}
if ($deleted) addLog('customer_permanent_delete', $id, $deleted['yritys'] ?? '', 'Poisti pysyvästi');
echo json_encode(['success' => true]);
break;
// ---------- LEADS ----------
case 'leads':
requireAuth();
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
echo json_encode($leads);
break;
case 'lead_create':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$lead = [
'id' => generateId(),
'yritys' => trim($input['yritys'] ?? ''),
'yhteyshenkilo' => trim($input['yhteyshenkilo'] ?? ''),
'puhelin' => trim($input['puhelin'] ?? ''),
'sahkoposti' => trim($input['sahkoposti'] ?? ''),
'osoite' => trim($input['osoite'] ?? ''),
'kaupunki' => trim($input['kaupunki'] ?? ''),
'tila' => trim($input['tila'] ?? 'uusi'),
'muistiinpanot' => trim($input['muistiinpanot'] ?? ''),
'luotu' => date('Y-m-d H:i:s'),
'luoja' => currentUser(),
];
if (empty($lead['yritys'])) {
http_response_code(400);
echo json_encode(['error' => 'Yrityksen nimi vaaditaan']);
break;
}
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
$leads[] = $lead;
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
addLog('lead_create', $lead['id'], $lead['yritys'], 'Lisäsi liidin');
echo json_encode($lead);
break;
case 'lead_update':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
$found = false;
foreach ($leads as &$l) {
if ($l['id'] === $id) {
$fields = ['yritys','yhteyshenkilo','puhelin','sahkoposti','osoite','kaupunki','tila','muistiinpanot'];
foreach ($fields as $f) {
if (isset($input[$f])) $l[$f] = trim($input[$f]);
}
$l['muokattu'] = date('Y-m-d H:i:s');
$l['muokkaaja'] = currentUser();
$found = true;
addLog('lead_update', $l['id'], $l['yritys'], 'Muokkasi liidiä');
echo json_encode($l);
break;
}
}
unset($l);
if (!$found) {
http_response_code(404);
echo json_encode(['error' => 'Liidiä ei löydy']);
break;
}
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
break;
case 'lead_delete':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
$deleted = null;
foreach ($leads as $l) {
if ($l['id'] === $id) { $deleted = $l; break; }
}
$leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
if ($deleted) addLog('lead_delete', $id, $deleted['yritys'] ?? '', 'Poisti liidin');
echo json_encode(['success' => true]);
break;
case 'lead_to_customer':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$leads = json_decode(file_get_contents(LEADS_FILE), true) ?: [];
$lead = null;
foreach ($leads as $l) {
if ($l['id'] === $id) { $lead = $l; break; }
}
if (!$lead) {
http_response_code(404);
echo json_encode(['error' => 'Liidiä ei löydy']);
break;
}
// Luo asiakas liidistä
$customer = [
'id' => generateId(),
'yritys' => $lead['yritys'],
'yhteyshenkilö' => $lead['yhteyshenkilo'] ?? '',
'puhelin' => $lead['puhelin'] ?? '',
'sahkoposti' => $lead['sahkoposti'] ?? '',
'laskutusosoite' => '',
'laskutuspostinumero' => '',
'laskutuskaupunki' => '',
'laskutussahkoposti' => '',
'elaskuosoite' => '',
'elaskuvalittaja' => '',
'ytunnus' => '',
'lisatiedot' => $lead['muistiinpanot'] ?? '',
'liittymat' => [['asennusosoite' => $lead['osoite'] ?? '', 'postinumero' => '', 'kaupunki' => $lead['kaupunki'] ?? '', 'liittymanopeus' => '', 'hinta' => 0, 'sopimuskausi' => '', 'alkupvm' => '']],
'luotu' => date('Y-m-d H:i:s'),
];
$customers = loadCustomers();
$customers[] = $customer;
saveCustomers($customers);
// Poista liidi
$leads = array_values(array_filter($leads, fn($l) => $l['id'] !== $id));
file_put_contents(LEADS_FILE, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
addLog('lead_to_customer', $customer['id'], $customer['yritys'], 'Muutti liidin asiakkaaksi');
echo json_encode($customer);
break;
// ---------- FILES ----------
case 'file_upload':
requireAuth();
if ($method !== 'POST') break;
$customerId = $_POST['customer_id'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) {
http_response_code(400);
echo json_encode(['error' => 'Virheellinen asiakas-ID']);
break;
}
if (empty($_FILES['file'])) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto puuttuu']);
break;
}
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo json_encode(['error' => 'Tiedoston lähetys epäonnistui']);
break;
}
if ($file['size'] > 20 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Tiedosto on liian suuri (max 20 MB)']);
break;
}
$uploadDir = DATA_DIR . '/files/' . $customerId;
if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true);
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name']));
$dest = $uploadDir . '/' . $safeName;
if (file_exists($dest)) {
$ext = pathinfo($safeName, PATHINFO_EXTENSION);
$base = pathinfo($safeName, PATHINFO_FILENAME);
$safeName = $base . '_' . date('His') . ($ext ? '.' . $ext : '');
$dest = $uploadDir . '/' . $safeName;
}
if (move_uploaded_file($file['tmp_name'], $dest)) {
echo json_encode(['success' => true, 'filename' => $safeName, 'size' => $file['size']]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Tallennusvirhe']);
}
break;
case 'file_list':
requireAuth();
$customerId = $_GET['customer_id'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) {
echo json_encode([]);
break;
}
$dir = DATA_DIR . '/files/' . $customerId;
$files = [];
if (is_dir($dir)) {
foreach (scandir($dir) as $f) {
if ($f === '.' || $f === '..') continue;
$path = $dir . '/' . $f;
$files[] = ['filename' => $f, 'size' => filesize($path), 'modified' => date('Y-m-d H:i', filemtime($path))];
}
}
usort($files, fn($a, $b) => strcmp($b['modified'], $a['modified']));
echo json_encode($files);
break;
case 'file_download':
requireAuth();
$customerId = $_GET['customer_id'] ?? '';
$filename = $_GET['filename'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId) || !$filename) {
http_response_code(400);
echo json_encode(['error' => 'Virheelliset parametrit']);
break;
}
$safeName = basename($filename);
$path = DATA_DIR . '/files/' . $customerId . '/' . $safeName;
if (!file_exists($path)) {
http_response_code(404);
echo json_encode(['error' => 'Tiedostoa ei löydy']);
break;
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safeName . '"');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
case 'file_delete':
requireAuth();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$customerId = $input['customer_id'] ?? '';
$filename = $input['filename'] ?? '';
if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId) || !$filename) {
http_response_code(400);
echo json_encode(['error' => 'Virheelliset parametrit']);
break;
}
$safeName = basename($filename);
$path = DATA_DIR . '/files/' . $customerId . '/' . $safeName;
if (file_exists($path)) unlink($path);
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']);
break;
}