'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)); } // ==================== MULTI-COMPANY ==================== function loadCompanies(): array { if (!file_exists(COMPANIES_FILE)) return []; return json_decode(file_get_contents(COMPANIES_FILE), true) ?: []; } function saveCompanies(array $companies): void { file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } function getCompanyDir(?string $companyId = null): string { $id = $companyId ?? ($_SESSION['company_id'] ?? ''); if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) { http_response_code(400); echo json_encode(['error' => 'Yritystä ei ole valittu']); exit; } $dir = DATA_DIR . '/companies/' . $id; if (!file_exists($dir)) mkdir($dir, 0755, true); return $dir; } function requireCompany(): string { $companyId = $_SESSION['company_id'] ?? ''; if (empty($companyId)) { http_response_code(400); echo json_encode(['error' => 'Valitse ensin yritys']); exit; } $userCompanies = $_SESSION['companies'] ?? []; if (!in_array($companyId, $userCompanies)) { http_response_code(403); echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']); exit; } return $companyId; } // Kuten requireCompany(), mutta sallii company_id:n overriden GET-parametrista // Käytetään tiketti-endpointeissa jotta toisen yrityksen tikettejä voi avata function requireCompanyOrParam(): string { $paramCompany = $_GET['company_id'] ?? ''; if (!empty($paramCompany)) { $userCompanies = $_SESSION['companies'] ?? []; if (!in_array($paramCompany, $userCompanies)) { http_response_code(403); echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']); exit; } $_SESSION['company_id'] = $paramCompany; } return requireCompany(); } function companyFile(string $filename): string { return getCompanyDir() . '/' . $filename; } function loadCompanyConfig(): array { $file = companyFile('config.json'); if (!file_exists($file)) return ['mailboxes' => [], 'ticket_rules' => []]; return json_decode(file_get_contents($file), true) ?: ['mailboxes' => [], 'ticket_rules' => []]; } function saveCompanyConfig(array $config): void { file_put_contents(companyFile('config.json'), json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } function runMigration(): void { $companiesDir = DATA_DIR . '/companies'; // Varmista companies.json olemassaolo ja sisältö (voi kadota/tyhjentyä git deploy:ssa) $companiesData = file_exists(COMPANIES_FILE) ? (json_decode(file_get_contents(COMPANIES_FILE), true) ?: []) : []; if (empty($companiesData)) { // Skannaa olemassaolevat yritys-hakemistot ja luo companies.json $companies = []; if (is_dir($companiesDir)) { foreach (glob($companiesDir . '/*', GLOB_ONLYDIR) as $dir) { $id = basename($dir); $companies[] = [ 'id' => $id, 'nimi' => $id === 'cuitunet' ? 'CuituNet' : ucfirst($id), 'luotu' => date('Y-m-d H:i:s'), 'aktiivinen' => true, ]; } } if (!empty($companies)) { file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } } // Varmista jokaisen yrityksen config.json if (is_dir($companiesDir)) { foreach (glob($companiesDir . '/*', GLOB_ONLYDIR) as $dir) { $configFile = $dir . '/config.json'; if (!file_exists($configFile)) { file_put_contents($configFile, json_encode(['mailboxes' => [], 'ticket_rules' => []], JSON_PRETTY_PRINT)); } } } // Tarkista onko vanha data olemassa juuressa (pre-multitenant) $oldCustomers = DATA_DIR . '/customers.json'; if (!file_exists($oldCustomers)) return; // Ei vanhaa dataa → ei migraatiota // Vanha data löytyy juuresta → siirretään yrityksen alle if (!file_exists($companiesDir)) mkdir($companiesDir, 0755, true); $cuitunetDir = $companiesDir . '/cuitunet'; if (!file_exists($cuitunetDir)) mkdir($cuitunetDir, 0755, true); // Luo companies.json $companies = [[ 'id' => 'cuitunet', 'nimi' => 'CuituNet', 'luotu' => date('Y-m-d H:i:s'), 'aktiivinen' => true, ]]; file_put_contents(COMPANIES_FILE, json_encode($companies, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // Siirrä datatiedostot yrityksen alle $filesToMove = ['customers.json', 'leads.json', 'archive.json', 'tickets.json', 'changelog.json']; foreach ($filesToMove as $f) { $src = DATA_DIR . '/' . $f; if (file_exists($src)) { copy($src, $cuitunetDir . '/' . $f); unlink($src); } } // Siirrä tiedostokansio $oldFiles = DATA_DIR . '/files'; if (is_dir($oldFiles)) { rename($oldFiles, $cuitunetDir . '/files'); } // Siirrä backups-kansio $oldBackups = DATA_DIR . '/backups'; if (is_dir($oldBackups)) { rename($oldBackups, $cuitunetDir . '/backups'); } // Luo yrityksen config IMAP-asetuksista $globalConfig = json_decode(file_get_contents(CONFIG_FILE), true) ?: []; $companyConfig = ['mailboxes' => [], 'ticket_rules' => $globalConfig['ticket_rules'] ?? []]; if (!empty($globalConfig['imap_host'])) { $companyConfig['mailboxes'][] = [ 'id' => generateId(), 'nimi' => 'Cuitunet-asiakaspalvelu', 'imap_host' => $globalConfig['imap_host'], 'imap_port' => intval($globalConfig['imap_port'] ?? 993), 'imap_user' => $globalConfig['imap_user'] ?? '', 'imap_password' => $globalConfig['imap_password'] ?? '', 'imap_encryption' => $globalConfig['imap_encryption'] ?? 'ssl', 'smtp_from_email' => $globalConfig['imap_user'] ?? '', 'smtp_from_name' => 'CuituNet Asiakaspalvelu', 'aktiivinen' => true, ]; } file_put_contents($cuitunetDir . '/config.json', json_encode($companyConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // Päivitä tiketteihin mailbox_id $ticketsFile = $cuitunetDir . '/tickets.json'; if (file_exists($ticketsFile)) { $tickets = json_decode(file_get_contents($ticketsFile), true) ?: []; $mbId = !empty($companyConfig['mailboxes']) ? $companyConfig['mailboxes'][0]['id'] : ''; foreach ($tickets as &$t) { if (!isset($t['mailbox_id'])) $t['mailbox_id'] = $mbId; } unset($t); file_put_contents($ticketsFile, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // Lisää companies-array kaikkiin käyttäjiin $users = json_decode(file_get_contents(USERS_FILE), true) ?: []; foreach ($users as &$u) { if (!isset($u['companies'])) $u['companies'] = ['cuitunet']; } unset($u); file_put_contents(USERS_FILE, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // Siivoa globaali config unset($globalConfig['imap_host'], $globalConfig['imap_port'], $globalConfig['imap_user'], $globalConfig['imap_password'], $globalConfig['imap_encryption'], $globalConfig['ticket_rules']); file_put_contents(CONFIG_FILE, json_encode($globalConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // ==================== 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 (socket-pohjainen, ei vaadi php-imap) ==================== class ImapClient { private $socket = null; private int $tagCounter = 0; public string $lastError = ''; 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)) { $this->lastError = 'IMAP-asetukset puuttuvat'; return false; } $prefix = ($encryption === 'ssl') ? 'ssl://' : 'tcp://'; $context = stream_context_create([ 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] ]); $this->socket = @stream_socket_client( $prefix . $host . ':' . $port, $errno, $errstr, 15, STREAM_CLIENT_CONNECT, $context ); if (!$this->socket) { $this->lastError = "Yhteys epäonnistui: {$errstr} ({$errno})"; return false; } stream_set_timeout($this->socket, 30); // Read greeting $greeting = $this->readLine(); if (!$greeting || strpos($greeting, '* OK') === false) { $this->lastError = 'Palvelin ei vastannut oikein: ' . $greeting; $this->disconnect(); return false; } // STARTTLS if needed if ($encryption === 'tls') { $resp = $this->command('STARTTLS'); if (!$this->isOk($resp)) { $this->lastError = 'STARTTLS epäonnistui'; $this->disconnect(); return false; } if (!stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { $this->lastError = 'TLS-neuvottelu epäonnistui'; $this->disconnect(); return false; } } // Login $resp = $this->command('LOGIN "' . $this->escape($user) . '" "' . $this->escape($pass) . '"'); if (!$this->isOk($resp)) { $this->lastError = 'Kirjautuminen epäonnistui: väärä tunnus tai salasana'; $this->disconnect(); return false; } // Select INBOX $resp = $this->command('SELECT INBOX'); if (!$this->isOk($resp)) { $this->lastError = 'INBOX:n avaus epäonnistui'; $this->disconnect(); return false; } return true; } public function fetchMessages(int $limit = 50): array { if (!$this->socket) return []; // Get message count from STATUS $resp = $this->command('STATUS INBOX (MESSAGES)'); $totalMessages = 0; foreach ($resp as $line) { if (preg_match('/MESSAGES\s+(\d+)/i', $line, $m)) { $totalMessages = intval($m[1]); } } if ($totalMessages === 0) return []; $start = max(1, $totalMessages - $limit + 1); $range = $start . ':' . $totalMessages; // Fetch headers for range $resp = $this->command("FETCH {$range} (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE MESSAGE-ID IN-REPLY-TO REFERENCES)] BODY.PEEK[TEXT] FLAGS)"); $messages = []; $current = null; $headerBuf = ''; $bodyBuf = ''; $readingHeader = false; $readingBody = false; $headerBytesLeft = 0; $bodyBytesLeft = 0; // Simpler approach: fetch one-by-one for reliability $messages = []; for ($i = $totalMessages; $i >= $start; $i--) { $msg = $this->fetchSingleMessage($i); if ($msg) $messages[] = $msg; } return $messages; } private function fetchSingleMessage(int $num): ?array { // Fetch headers $resp = $this->command("FETCH {$num} BODY.PEEK[HEADER]"); $headerRaw = $this->extractLiteral($resp); if (!$headerRaw) return null; $headers = $this->parseHeaders($headerRaw); $subject = $this->decodeMimeHeader($headers['subject'] ?? ''); $fromRaw = $headers['from'] ?? ''; $fromParsed = $this->parseFrom($fromRaw); $messageId = trim($headers['message-id'] ?? ''); $inReplyTo = trim($headers['in-reply-to'] ?? ''); $references = trim($headers['references'] ?? ''); $dateStr = $headers['date'] ?? ''; $date = $dateStr ? @date('Y-m-d H:i:s', strtotime($dateStr)) : date('Y-m-d H:i:s'); if (!$date) $date = date('Y-m-d H:i:s'); // Fetch body (text part) $body = $this->fetchBody($num); return [ 'subject' => $subject, 'from_email' => $fromParsed['email'], 'from_name' => $this->decodeMimeHeader($fromParsed['name']), 'message_id' => $messageId, 'in_reply_to' => $inReplyTo, 'references' => $references, 'date' => $date, 'body' => $body, ]; } private function fetchBody(int $num): string { // Try text/plain first via BODYSTRUCTURE $resp = $this->command("FETCH {$num} BODYSTRUCTURE"); $structLine = implode(' ', $resp); // Simple approach: fetch BODY[1] (usually text/plain in multipart) // or BODY[TEXT] for simple messages $resp = $this->command("FETCH {$num} BODY.PEEK[1]"); $body = $this->extractLiteral($resp); if (!$body) { // Fallback: full text $resp = $this->command("FETCH {$num} BODY.PEEK[TEXT]"); $body = $this->extractLiteral($resp); } if (!$body) return ''; // Detect encoding from BODYSTRUCTURE $encoding = ''; // Parse BODYSTRUCTURE for encoding (7BIT, BASE64, QUOTED-PRINTABLE) if (preg_match('/"TEXT"\s+"PLAIN"\s+\([^)]*\)\s+NIL\s+NIL\s+"([^"]+)"/i', $structLine, $em)) { $encoding = strtoupper($em[1]); } elseif (preg_match('/BODY\[1\].*?"([^"]+)"/i', $structLine, $em)) { $encoding = strtoupper($em[1]); } // Try to detect encoding from body content if not found if (!$encoding) { // Check if it looks like base64 if (preg_match('/^[A-Za-z0-9+\/=\s]+$/', trim($body)) && strlen(trim($body)) > 50) { $decoded = @base64_decode($body, true); if ($decoded !== false && strlen($decoded) > 0) { // Verify it produces readable text if (preg_match('/[\x20-\x7E\xC0-\xFF]/', $decoded)) { $body = $decoded; } } } } else { if ($encoding === 'BASE64') { $body = base64_decode($body); } elseif ($encoding === 'QUOTED-PRINTABLE') { $body = quoted_printable_decode($body); } } // Strip HTML if it looks like HTML if (preg_match('/]+)>/', $from, $m)) { return ['name' => trim($m[1], ' "'), 'email' => trim($m[2])]; } if (preg_match('/^([^\s@]+@[^\s@]+)/', $from, $m)) { return ['name' => '', 'email' => $m[1]]; } return ['name' => '', 'email' => $from]; } private function decodeMimeHeader(string $str): string { if (strpos($str, '=?') === false) return trim($str); $decoded = ''; $parts = preg_split('/(=\?[^\?]+\?[BbQq]\?[^\?]*\?=)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE); foreach ($parts as $part) { if (preg_match('/^=\?([^\?]+)\?([BbQq])\?([^\?]*)\?=$/', $part, $m)) { $charset = $m[1]; $encoding = strtoupper($m[2]); $text = $m[3]; if ($encoding === 'B') { $text = base64_decode($text); } elseif ($encoding === 'Q') { $text = quoted_printable_decode(str_replace('_', ' ', $text)); } if (strtolower($charset) !== 'utf-8') { $converted = @iconv($charset, 'UTF-8//IGNORE', $text); if ($converted !== false) $text = $converted; } $decoded .= $text; } else { // Remove whitespace between encoded words if (trim($part) === '') continue; $decoded .= $part; } } return trim($decoded); } private function command(string $cmd): array { $tag = 'A' . (++$this->tagCounter); $this->writeLine("{$tag} {$cmd}"); $response = []; while (true) { $line = $this->readLine(); if ($line === false || $line === null) break; $response[] = $line; // Check for literal {N} — read N bytes if (preg_match('/\{(\d+)\}$/', $line, $m)) { $bytes = intval($m[1]); $data = $this->readBytes($bytes); $response[] = $data; // Read the closing line after literal $closingLine = $this->readLine(); if ($closingLine !== false && $closingLine !== null) { $response[] = $closingLine; } continue; } // Tagged response = done if (strpos($line, $tag . ' ') === 0) break; } return $response; } private function extractLiteral(array $resp): string { $result = ''; for ($i = 0; $i < count($resp); $i++) { if (preg_match('/\{(\d+)\}$/', $resp[$i], $m)) { // Next element should be the literal data if (isset($resp[$i + 1])) { $result .= $resp[$i + 1]; } } } return $result; } private function isOk(array $resp): bool { foreach ($resp as $line) { if (preg_match('/^A\d+\s+OK/i', $line)) return true; if (preg_match('/^A\d+\s+(NO|BAD)/i', $line)) return false; } return false; } private function escape(string $str): string { return str_replace(['\\', '"'], ['\\\\', '\\"'], $str); } private function writeLine(string $line): void { if (!$this->socket) return; fwrite($this->socket, $line . "\r\n"); } private function readLine(): ?string { if (!$this->socket) return null; $line = fgets($this->socket, 8192); if ($line === false) return null; return rtrim($line, "\r\n"); } private function readBytes(int $n): string { if (!$this->socket) return ''; $data = ''; $remaining = $n; while ($remaining > 0) { $chunk = fread($this->socket, min($remaining, 8192)); if ($chunk === false || $chunk === '') break; $data .= $chunk; $remaining -= strlen($chunk); } return $data; } public function disconnect(): void { if ($this->socket) { try { $this->command('LOGOUT'); } catch (\Throwable $e) {} @fclose($this->socket); $this->socket = null; } } } // ==================== TICKETS ==================== function loadTickets(): array { $file = companyFile('tickets.json'); if (!file_exists($file)) { file_put_contents($file, '[]'); return []; } return json_decode(file_get_contents($file), true) ?: []; } function saveTickets(array $tickets): void { file_put_contents(companyFile('tickets.json'), 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 = '', ?array $mailbox = null): bool { $fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM; $fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? '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', 'companies' => [], '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 { // Jos company-kontekstia ei ole (esim. globaalit asetukset), ohitetaan if (empty($_SESSION['company_id'])) return; $file = companyFile('changelog.json'); if (!file_exists($file)) file_put_contents($file, '[]'); $log = json_decode(file_get_contents($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($file, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // ==================== CUSTOMERS ==================== function loadCustomers(): array { $file = companyFile('customers.json'); if (!file_exists($file)) { file_put_contents($file, '[]'); return []; } $data = file_get_contents($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($file, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } return $customers; } function saveCustomers(array $customers): void { $file = companyFile('customers.json'); if (file_exists($file) && filesize($file) > 2) { $backupDir = getCompanyDir() . '/backups'; if (!file_exists($backupDir)) mkdir($backupDir, 0755, true); copy($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($file, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } function loadArchive(): array { $file = companyFile('archive.json'); if (!file_exists($file)) { file_put_contents($file, '[]'); return []; } return json_decode(file_get_contents($file), true) ?: []; } function saveArchive(array $archive): void { file_put_contents(companyFile('archive.json'), 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; } // Hae kaikista yrityksistä $allCompanies = loadCompanies(); $found = false; foreach ($allCompanies as $comp) { $compDir = DATA_DIR . '/companies/' . $comp['id']; $custFile = $compDir . '/customers.json'; if (!file_exists($custFile)) continue; $customers = json_decode(file_get_contents($custFile), true) ?: []; foreach ($customers as $c) { foreach ($c['liittymat'] ?? [] as $l) { $addr = normalizeAddress($l['asennusosoite'] ?? ''); $zip = trim($l['postinumero'] ?? ''); $city = strtolower(trim($l['kaupunki'] ?? '')); if ($zip === $queryPostinumero && $city === $queryKaupunki) { if (!empty($addr) && !empty($queryOsoite)) { if (strpos($addr, $queryOsoite) !== false || strpos($queryOsoite, $addr) !== false) { $found = true; break 3; } } } } } } // 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); } 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']; // Multi-company: aseta käyttäjän yritykset sessioon $userCompanies = $u['companies'] ?? []; $_SESSION['companies'] = $userCompanies; // Valitse ensimmäinen yritys oletukseksi $_SESSION['company_id'] = !empty($userCompanies) ? $userCompanies[0] : ''; // Hae yritysten nimet $allCompanies = loadCompanies(); $companyList = []; foreach ($allCompanies as $comp) { if (in_array($comp['id'], $userCompanies)) { $companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']]; } } echo json_encode([ 'success' => true, 'username' => $u['username'], 'nimi' => $u['nimi'], 'role' => $u['role'], 'companies' => $companyList, 'company_id' => $_SESSION['company_id'], ]); $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'])) { // Synkronoi aina tuoreet yritysoikeudet users.json:sta sessioon $users = loadUsers(); foreach ($users as $u) { if ($u['id'] === $_SESSION['user_id']) { $_SESSION['companies'] = $u['companies'] ?? []; // Varmista aktiivinen yritys on sallittu if (!in_array($_SESSION['company_id'] ?? '', $_SESSION['companies'])) { $_SESSION['company_id'] = !empty($_SESSION['companies']) ? $_SESSION['companies'][0] : ''; } break; } } // Hae yritysten nimet $userCompanyIds = $_SESSION['companies'] ?? []; $allCompanies = loadCompanies(); $companyList = []; foreach ($allCompanies as $comp) { if (in_array($comp['id'], $userCompanyIds)) { $companyList[] = ['id' => $comp['id'], 'nimi' => $comp['nimi']]; } } echo json_encode([ 'authenticated' => true, 'username' => $_SESSION['username'], 'nimi' => $_SESSION['nimi'], 'role' => $_SESSION['role'], 'companies' => $companyList, 'company_id' => $_SESSION['company_id'] ?? '', ]); } 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 = '
'; $html .= '

CuituNet Intra

'; $html .= '

Hei ' . htmlspecialchars($user['nimi'] ?: $user['username']) . ',

'; $html .= '

Sait tämän viestin koska salasanan palautusta pyydettiin tilillesi.

'; $html .= '

Vaihda salasana

'; $html .= '

Linkki on voimassa 1 tunnin. Jos et pyytänyt salasanan vaihtoa, voit jättää tämän viestin huomiotta.

'; $html .= '
'; 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; } } $companies = $input['companies'] ?? []; // Validoi yritys-IDt $allCompanies = loadCompanies(); $validIds = array_column($allCompanies, 'id'); $companies = array_values(array_filter($companies, fn($c) => in_array($c, $validIds))); $newUser = [ 'id' => generateId(), 'username' => $username, 'password_hash' => password_hash($password, PASSWORD_DEFAULT), 'nimi' => $nimi ?: $username, 'email' => $email, 'role' => $role, 'companies' => $companies, '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 (isset($input['companies'])) { $allCompanies = loadCompanies(); $validIds = array_column($allCompanies, 'id'); $u['companies'] = array_values(array_filter($input['companies'], fn($c) => in_array($c, $validIds))); } if (!empty($input['password'])) { $u['password_hash'] = password_hash($input['password'], PASSWORD_DEFAULT); } $found = true; addLog('user_update', '', '', "Muokkasi käyttäjää: {$u['username']}"); // Päivitä sessio jos muokattiin kirjautunutta käyttäjää if ($u['id'] === $_SESSION['user_id']) { $_SESSION['companies'] = $u['companies'] ?? []; if (!empty($u['companies']) && !in_array($_SESSION['company_id'] ?? '', $u['companies'])) { $_SESSION['company_id'] = $u['companies'][0]; } if (empty($u['companies'])) { $_SESSION['company_id'] = ''; } } $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(); requireCompany(); $logFile = companyFile('changelog.json'); if (!file_exists($logFile)) file_put_contents($logFile, '[]'); $log = json_decode(file_get_contents($logFile), true) ?: []; $limit = intval($_GET['limit'] ?? 100); echo json_encode(array_slice($log, 0, $limit)); break; // ---------- CUSTOMERS ---------- case 'customers': requireAuth(); requireCompany(); if ($method === 'GET') { echo json_encode(loadCustomers()); } break; case 'customer': requireAuth(); requireCompany(); 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(); requireCompany(); 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(); requireCompany(); 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(); requireCompany(); echo json_encode(loadArchive()); break; case 'customer_restore': requireAuth(); requireCompany(); 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(); requireCompany(); 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 = getCompanyDir() . '/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(); requireCompany(); $leadsFile = companyFile('leads.json'); if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]'); $leads = json_decode(file_get_contents($leadsFile), true) ?: []; echo json_encode($leads); break; case 'lead_create': requireAuth(); requireCompany(); 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; } $leadsFile = companyFile('leads.json'); if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]'); $leads = json_decode(file_get_contents($leadsFile), true) ?: []; $leads[] = $lead; file_put_contents($leadsFile, 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(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $leadsFile = companyFile('leads.json'); if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]'); $leads = json_decode(file_get_contents($leadsFile), 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($leadsFile, json_encode($leads, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); break; case 'lead_delete': requireAuth(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $leadsFile = companyFile('leads.json'); if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]'); $leads = json_decode(file_get_contents($leadsFile), 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($leadsFile, 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(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $leadsFile = companyFile('leads.json'); if (!file_exists($leadsFile)) file_put_contents($leadsFile, '[]'); $leads = json_decode(file_get_contents($leadsFile), 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($leadsFile, 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(); requireCompany(); 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 = getCompanyDir() . '/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(); requireCompany(); $customerId = $_GET['customer_id'] ?? ''; if (!$customerId || !preg_match('/^[a-f0-9]+$/', $customerId)) { echo json_encode([]); break; } $dir = getCompanyDir() . '/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(); requireCompany(); $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 = getCompanyDir() . '/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(); requireCompany(); 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 = getCompanyDir() . '/files/' . $customerId . '/' . $safeName; if (file_exists($path)) unlink($path); echo json_encode(['success' => true]); break; // ---------- TICKETS ---------- case 'tickets': requireAuth(); $allCompanies = !empty($_GET['all']); $userCompanyIds = $_SESSION['companies'] ?? []; // Kerää yritykset joista haetaan $companiesToQuery = []; if ($allCompanies && count($userCompanyIds) > 1) { $allComps = loadCompanies(); foreach ($allComps as $c) { if (in_array($c['id'], $userCompanyIds)) { $companiesToQuery[] = $c; } } } else { requireCompany(); $companiesToQuery[] = ['id' => $_SESSION['company_id'], 'nimi' => '']; } $list = []; foreach ($companiesToQuery as $comp) { $cDir = DATA_DIR . '/companies/' . $comp['id']; $ticketsFile = $cDir . '/tickets.json'; if (!file_exists($ticketsFile)) continue; $tickets = json_decode(file_get_contents($ticketsFile), true) ?: []; // Auto-close tarkistus $now = date('Y-m-d H:i:s'); $autoCloseCount = 0; foreach ($tickets as &$tc) { if (!empty($tc['auto_close_at']) && $tc['auto_close_at'] <= $now && !in_array($tc['status'], ['suljettu'])) { $tc['status'] = 'suljettu'; $tc['updated'] = $now; $autoCloseCount++; } } unset($tc); if ($autoCloseCount > 0) { file_put_contents($ticketsFile, json_encode($tickets, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // Resolve mailbox names for this company $confFile = $cDir . '/config.json'; $companyConf = file_exists($confFile) ? (json_decode(file_get_contents($confFile), true) ?: []) : []; $mailboxNames = []; foreach ($companyConf['mailboxes'] ?? [] as $mb) { $mailboxNames[$mb['id']] = $mb['nimi']; } foreach ($tickets as $t) { $msgCount = count($t['messages'] ?? []); $lastMsg = $msgCount > 0 ? $t['messages'][$msgCount - 1] : null; $list[] = [ 'id' => $t['id'], 'subject' => $t['subject'], 'from_email' => $t['from_email'], 'from_name' => $t['from_name'], 'status' => $t['status'], 'type' => $t['type'] ?? 'muu', 'assigned_to' => $t['assigned_to'] ?? '', 'customer_id' => $t['customer_id'] ?? '', 'customer_name' => $t['customer_name'] ?? '', 'tags' => $t['tags'] ?? [], 'auto_close_at' => $t['auto_close_at'] ?? '', 'mailbox_id' => $t['mailbox_id'] ?? '', 'mailbox_name' => $mailboxNames[$t['mailbox_id'] ?? ''] ?? '', 'company_id' => $comp['id'], 'company_name' => $comp['nimi'] ?? '', 'created' => $t['created'], 'updated' => $t['updated'], 'message_count' => $msgCount, 'last_message_type' => $lastMsg ? ($lastMsg['type'] ?? '') : '', 'last_message_time' => $lastMsg ? ($lastMsg['timestamp'] ?? '') : '', ]; } } echo json_encode($list); break; case 'ticket_detail': requireAuth(); requireCompanyOrParam(); $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(); requireCompany(); if ($method !== 'POST') break; $companyConf = loadCompanyConfig(); $mailboxes = array_filter($companyConf['mailboxes'] ?? [], fn($mb) => !empty($mb['aktiivinen'])); if (empty($mailboxes)) { http_response_code(400); echo json_encode(['error' => 'Postilaatikoita ei ole määritetty. Lisää ne Yritykset-välilehdellä.']); break; } $tickets = loadTickets(); $newCount = 0; $threadedCount = 0; $errors = []; // 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; } } // Hae kaikista aktiivisista postilaatikoista foreach ($mailboxes as $mailbox) { $imapConfig = [ 'imap_host' => $mailbox['imap_host'] ?? '', 'imap_port' => $mailbox['imap_port'] ?? 993, 'imap_user' => $mailbox['imap_user'] ?? '', 'imap_password' => $mailbox['imap_password'] ?? '', 'imap_encryption' => $mailbox['imap_encryption'] ?? 'ssl', ]; $imap = new ImapClient(); if (!$imap->connect($imapConfig)) { $errors[] = ($mailbox['nimi'] ?? 'Tuntematon') . ': ' . $imap->lastError; continue; } $emails = $imap->fetchMessages(100); $imap->disconnect(); $rules = $companyConf['ticket_rules'] ?? []; foreach ($emails as $email) { 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'], ]; $ticketIdx = findTicketByReferences($tickets, $email['in_reply_to'], $email['references']); if ($ticketIdx !== null) { $tickets[$ticketIdx]['messages'][] = $msg; $tickets[$ticketIdx]['updated'] = $email['date']; if (in_array($tickets[$ticketIdx]['status'], ['ratkaistu', 'suljettu'])) { $tickets[$ticketIdx]['status'] = 'kasittelyssa'; } $threadedCount++; } else { $ticket = [ 'id' => generateId(), 'subject' => $email['subject'] ?: '(Ei aihetta)', 'from_email' => $email['from_email'], 'from_name' => $email['from_name'], 'status' => 'uusi', 'type' => 'muu', 'assigned_to' => '', 'customer_id' => '', 'customer_name' => '', 'tags' => [], 'auto_close_at' => '', 'mailbox_id' => $mailbox['id'], 'created' => $email['date'], 'updated' => $email['date'], 'message_id' => $email['message_id'], 'messages' => [$msg], ]; // Apply auto-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']; if (!empty($rule['set_tags'])) { $ruleTags = array_map('trim', explode(',', $rule['set_tags'])); $ticket['tags'] = array_values(array_unique(array_merge($ticket['tags'], $ruleTags))); } if (!empty($rule['auto_close_days'])) { $days = intval($rule['auto_close_days']); if ($days > 0) { $ticket['auto_close_at'] = date('Y-m-d H:i:s', strtotime("+{$days} days")); } } break; } } $tickets[] = $ticket; $newCount++; } if ($email['message_id']) $existingMsgIds[$email['message_id']] = true; } } 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"); $result = ['success' => true, 'new_tickets' => $newCount, 'threaded' => $threadedCount, 'total' => count($tickets)]; if (!empty($errors)) $result['errors'] = $errors; echo json_encode($result); break; case 'ticket_reply': requireAuth(); requireCompanyOrParam(); 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 — hae postilaatikon asetukset $companyConf = loadCompanyConfig(); $replyMailbox = null; foreach ($companyConf['mailboxes'] ?? [] as $mb) { if ($mb['id'] === ($t['mailbox_id'] ?? '')) { $replyMailbox = $mb; break; } } // Fallback: käytä ensimmäistä postilaatikkoa if (!$replyMailbox && !empty($companyConf['mailboxes'])) { $replyMailbox = $companyConf['mailboxes'][0]; } $subject = 'Re: ' . $t['subject']; $sent = sendTicketMail($t['from_email'], $subject, $body, $lastMsgId, trim($allRefs), $replyMailbox); 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(); requireCompanyOrParam(); 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_type': requireAuth(); requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $type = $input['type'] ?? ''; $validTypes = ['laskutus', 'tekniikka', 'vika', 'muu']; if (!in_array($type, $validTypes)) { http_response_code(400); echo json_encode(['error' => 'Virheellinen tyyppi']); break; } $tickets = loadTickets(); $found = false; foreach ($tickets as &$t) { if ($t['id'] === $id) { $oldType = $t['type'] ?? 'muu'; $t['type'] = $type; $t['updated'] = date('Y-m-d H:i:s'); $found = true; addLog('ticket_type', $t['id'], $t['subject'], "Tyyppi: {$oldType} → {$type}"); 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_customer': requireAuth(); requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $customerId = $input['customer_id'] ?? ''; $customerName = $input['customer_name'] ?? ''; $tickets = loadTickets(); $found = false; foreach ($tickets as &$t) { if ($t['id'] === $id) { $t['customer_id'] = $customerId; $t['customer_name'] = $customerName; $t['updated'] = date('Y-m-d H:i:s'); $found = true; addLog('ticket_customer', $t['id'], $t['subject'], "Asiakkuus: {$customerName}"); 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(); requireCompanyOrParam(); 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(); requireCompanyOrParam(); 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(); requireCompanyOrParam(); 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; case 'ticket_tags': requireAuth(); requireCompanyOrParam(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $tags = $input['tags'] ?? []; // Sanitize tags: trim, lowercase, remove empty $tags = array_values(array_filter(array_map(function($t) { return trim(strtolower($t)); }, $tags))); $tickets = loadTickets(); $found = false; foreach ($tickets as &$t) { if ($t['id'] === $id) { $t['tags'] = $tags; $t['updated'] = date('Y-m-d H:i:s'); $found = true; addLog('ticket_tags', $t['id'], $t['subject'], 'Tagit: ' . implode(', ', $tags)); 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_rules': requireAuth(); requireCompany(); $companyConf = loadCompanyConfig(); echo json_encode($companyConf['ticket_rules'] ?? []); break; case 'ticket_rule_save': requireAuth(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $companyConf = loadCompanyConfig(); $rules = $companyConf['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'] ?? '', 'set_tags' => trim($input['set_tags'] ?? ''), 'auto_close_days' => intval($input['auto_close_days'] ?? 0), 'enabled' => $input['enabled'] ?? true, ]; if (empty($rule['name'])) { http_response_code(400); echo json_encode(['error' => 'Säännön nimi puuttuu']); break; } $found = false; foreach ($rules as &$r) { if ($r['id'] === $rule['id']) { $r = $rule; $found = true; break; } } unset($r); if (!$found) $rules[] = $rule; $companyConf['ticket_rules'] = $rules; saveCompanyConfig($companyConf); addLog('config_update', '', '', 'Tikettisääntö: ' . $rule['name']); echo json_encode($rule); break; case 'ticket_bulk_status': requireAuth(); requireCompany(); 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 = loadTickets(); $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); saveTickets($tickets); addLog('ticket_status', '', '', "Massapäivitys: $changed tikettiä → $newStatus"); echo json_encode(['success' => true, 'changed' => $changed]); break; case 'ticket_bulk_delete': requireAuth(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $ids = $input['ids'] ?? []; $tickets = loadTickets(); $before = count($tickets); $tickets = array_values(array_filter($tickets, fn($t) => !in_array($t['id'], $ids))); $deleted = $before - count($tickets); saveTickets($tickets); addLog('ticket_delete', '', '', "Massapoisto: $deleted tikettiä"); echo json_encode(['success' => true, 'deleted' => $deleted]); break; case 'ticket_rule_delete': requireAuth(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $ruleId = $input['id'] ?? ''; $companyConf = loadCompanyConfig(); $rules = $companyConf['ticket_rules'] ?? []; $companyConf['ticket_rules'] = array_values(array_filter($rules, fn($r) => $r['id'] !== $ruleId)); saveCompanyConfig($companyConf); echo json_encode(['success' => true]); break; // ---------- COMPANY MANAGEMENT ---------- case 'companies': requireAuth(); $userCompanyIds = $_SESSION['companies'] ?? []; $allCompanies = loadCompanies(); $result = array_values(array_filter($allCompanies, fn($c) => in_array($c['id'], $userCompanyIds))); echo json_encode($result); break; case 'companies_all': requireAdmin(); echo json_encode(loadCompanies()); break; case 'company_create': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = preg_replace('/[^a-z0-9-]/', '', strtolower(trim($input['id'] ?? ''))); $nimi = trim($input['nimi'] ?? ''); if (empty($id) || empty($nimi)) { http_response_code(400); echo json_encode(['error' => 'ID ja nimi vaaditaan']); break; } $companies = loadCompanies(); foreach ($companies as $c) { if ($c['id'] === $id) { http_response_code(400); echo json_encode(['error' => 'Yritys-ID on jo käytössä']); break 2; } } $company = [ 'id' => $id, 'nimi' => $nimi, 'luotu' => date('Y-m-d H:i:s'), 'aktiivinen' => true, ]; $companies[] = $company; saveCompanies($companies); // Luo hakemisto ja tyhjät tiedostot $compDir = DATA_DIR . '/companies/' . $id; if (!file_exists($compDir)) mkdir($compDir, 0755, true); file_put_contents($compDir . '/config.json', json_encode(['mailboxes' => [], 'ticket_rules' => []], JSON_PRETTY_PRINT)); file_put_contents($compDir . '/customers.json', '[]'); file_put_contents($compDir . '/leads.json', '[]'); file_put_contents($compDir . '/archive.json', '[]'); file_put_contents($compDir . '/tickets.json', '[]'); file_put_contents($compDir . '/changelog.json', '[]'); // Lisää luoja yrityksen käyttäjäksi $users = loadUsers(); foreach ($users as &$u) { if ($u['id'] === $_SESSION['user_id']) { $u['companies'] = array_unique(array_merge($u['companies'] ?? [], [$id])); $_SESSION['companies'] = $u['companies']; break; } } unset($u); saveUsers($users); echo json_encode($company); break; case 'company_update': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; $companies = loadCompanies(); $found = false; foreach ($companies as &$c) { if ($c['id'] === $id) { if (isset($input['nimi'])) $c['nimi'] = trim($input['nimi']); if (isset($input['aktiivinen'])) $c['aktiivinen'] = (bool)$input['aktiivinen']; $found = true; echo json_encode($c); break; } } unset($c); if (!$found) { http_response_code(404); echo json_encode(['error' => 'Yritystä ei löydy']); break; } saveCompanies($companies); break; case 'company_delete': requireAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $id = $input['id'] ?? ''; if (empty($id) || !preg_match('/^[a-z0-9-]+$/', $id)) { http_response_code(400); echo json_encode(['error' => 'Virheellinen yritys-ID']); break; } $companies = loadCompanies(); $companies = array_values(array_filter($companies, fn($c) => $c['id'] !== $id)); saveCompanies($companies); // Poista yritys käyttäjiltä $users = loadUsers(); foreach ($users as &$u) { $u['companies'] = array_values(array_filter($u['companies'] ?? [], fn($c) => $c !== $id)); } unset($u); saveUsers($users); echo json_encode(['success' => true]); break; case 'company_switch': requireAuth(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $companyId = $input['company_id'] ?? ''; $userCompanies = $_SESSION['companies'] ?? []; if (!in_array($companyId, $userCompanies)) { http_response_code(403); echo json_encode(['error' => 'Ei oikeutta tähän yritykseen']); break; } $_SESSION['company_id'] = $companyId; echo json_encode(['success' => true, 'company_id' => $companyId]); break; case 'company_config': requireAdmin(); requireCompany(); echo json_encode(loadCompanyConfig()); break; case 'company_config_update': requireAdmin(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $companyConf = loadCompanyConfig(); if (isset($input['mailboxes'])) $companyConf['mailboxes'] = $input['mailboxes']; if (isset($input['ticket_rules'])) $companyConf['ticket_rules'] = $input['ticket_rules']; saveCompanyConfig($companyConf); echo json_encode($companyConf); break; // ---------- MAILBOXES ---------- case 'mailboxes': requireAuth(); requireCompany(); $companyConf = loadCompanyConfig(); // Palauta postilaatikot ilman salasanoja $mbs = array_map(function($mb) { $mb['imap_password'] = !empty($mb['imap_password']) ? '********' : ''; return $mb; }, $companyConf['mailboxes'] ?? []); echo json_encode($mbs); break; case 'mailbox_save': requireAdmin(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $companyConf = loadCompanyConfig(); $mailboxes = $companyConf['mailboxes'] ?? []; $mb = [ 'id' => $input['id'] ?? generateId(), 'nimi' => trim($input['nimi'] ?? ''), 'imap_host' => trim($input['imap_host'] ?? ''), 'imap_port' => intval($input['imap_port'] ?? 993), 'imap_user' => trim($input['imap_user'] ?? ''), 'imap_encryption' => trim($input['imap_encryption'] ?? 'ssl'), 'smtp_from_email' => trim($input['smtp_from_email'] ?? ''), 'smtp_from_name' => trim($input['smtp_from_name'] ?? ''), 'aktiivinen' => $input['aktiivinen'] ?? true, ]; // Salasana: jos ******** → pidä vanha, muuten päivitä if (isset($input['imap_password']) && $input['imap_password'] !== '********') { $mb['imap_password'] = $input['imap_password']; } else { // Hae vanha salasana foreach ($mailboxes as $existing) { if ($existing['id'] === $mb['id']) { $mb['imap_password'] = $existing['imap_password'] ?? ''; break; } } if (!isset($mb['imap_password'])) $mb['imap_password'] = ''; } if (empty($mb['nimi'])) { http_response_code(400); echo json_encode(['error' => 'Postilaatikon nimi puuttuu']); break; } $found = false; foreach ($mailboxes as &$existing) { if ($existing['id'] === $mb['id']) { $existing = $mb; $found = true; break; } } unset($existing); if (!$found) $mailboxes[] = $mb; $companyConf['mailboxes'] = $mailboxes; saveCompanyConfig($companyConf); addLog('mailbox_save', '', '', 'Postilaatikko: ' . $mb['nimi']); // Palauta ilman salasanaa $mb['imap_password'] = '********'; echo json_encode($mb); break; case 'mailbox_delete': requireAdmin(); requireCompany(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $mbId = $input['id'] ?? ''; $companyConf = loadCompanyConfig(); $companyConf['mailboxes'] = array_values(array_filter($companyConf['mailboxes'] ?? [], fn($m) => $m['id'] !== $mbId)); saveCompanyConfig($companyConf); echo json_encode(['success' => true]); break; default: http_response_code(404); echo json_encode(['error' => 'Tuntematon toiminto']); break; }