'csrf'], 419); } $convId = (int)($_POST['conversation_id'] ?? 0); $body = isset($_POST['body']) ? trim((string)$_POST['body']) : ''; if ($convId <= 0) { json_response(['error' => 'bad_params'], 400); } // Autorisation + statut $conv = assert_conversation_access($pdo, $convId, $role, $meId); if (($conv['status'] ?? 'open') !== 'open') { json_response(['error' => 'closed'], 403); } // Upload (optionnel) $attPath = null; $attName = null; if (!empty($_FILES['file']['name'] ?? '')) { $okExt = ['jpg','jpeg','png','pdf','doc','docx']; $name = (string)$_FILES['file']['name']; $size = (int)$_FILES['file']['size']; $tmp = (string)$_FILES['file']['tmp_name']; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (!in_array($ext, $okExt, true)) { json_response(['error' => 'invalid_file_type'], 400); } if ($size > 10 * 1024 * 1024) { // 10 Mo json_response(['error' => 'file_too_large'], 400); } $dir = realpath(__DIR__ . '/..') . '/uploads/'; if (!is_dir($dir)) { @mkdir($dir, 0775, true); } $safe = $meId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext; $dest = $dir . $safe; if (!move_uploaded_file($tmp, $dest)) { json_response(['error' => 'upload_failed'], 500); } // Chemin web relatif (sans leading slash pour constituer une URL /messages/uploads/..) $attPath = 'messages/uploads/' . $safe; $attName = $name; } // Flag : doit-on envoyer une notification mail ? $shouldNotify = false; try { $pdo->beginTransaction(); // Insérer le message $ins = $pdo->prepare(" INSERT INTO messages (conversation_id, sender_role, sender_id, body, attachment_path, attachment_name, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW()) "); $ins->execute([ $convId, $role, $meId, $body !== '' ? $body : null, $attPath, $attName ]); $msgId = (int)$pdo->lastInsertId(); // Mettre à jour les compteurs de non-lus (chez l'autre) if ($role === 'praticien') { // Le praticien écrit → c'est le client qui a des messages non lus (c_unread) $stmt = $pdo->prepare(" SELECT c_unread FROM conversation_counters WHERE conversation_id = ? FOR UPDATE "); $stmt->execute([$convId]); $prev = $stmt->fetchColumn(); $prevUnread = ($prev === false) ? 0 : (int)$prev; $pdo->prepare(" UPDATE conversation_counters SET c_unread = c_unread + 1 WHERE conversation_id = ? ")->execute([$convId]); if ($prevUnread === 0) { $shouldNotify = true; } } else { // Le client écrit → c'est le praticien qui a des messages non lus (p_unread) $stmt = $pdo->prepare(" SELECT p_unread FROM conversation_counters WHERE conversation_id = ? FOR UPDATE "); $stmt->execute([$convId]); $prev = $stmt->fetchColumn(); $prevUnread = ($prev === false) ? 0 : (int)$prev; $pdo->prepare(" UPDATE conversation_counters SET p_unread = p_unread + 1 WHERE conversation_id = ? ")->execute([$convId]); if ($prevUnread === 0) { $shouldNotify = true; } } // Bump updated_at $pdo->prepare(" UPDATE conversations SET updated_at = NOW() WHERE id = ? ")->execute([$convId]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } error_log('send_message error: '.$e->getMessage()); json_response(['error' => 'server_error'], 500); } // 🔔 Notification par e-mail (ne doit jamais casser la réponse JSON) if ($shouldNotify) { try { require_once __DIR__ . '/../../public/message_par_mail.php'; notifier_nouveau_message_par_email($pdo, $convId, $role); } catch (Throwable $e) { error_log('Notif mail error: '.$e->getMessage()); } try { // Déterminer le destinataire (l'autre) $targetRole = ($role === 'praticien') ? 'client' : 'praticien'; // Normalement assert_conversation_access() te renvoie ces champs, // sinon fallback query (au cas où) $clientId = (int)($conv['client_id'] ?? 0); $praticienId = (int)($conv['praticien_id'] ?? 0); if ($clientId <= 0 || $praticienId <= 0) { $st = $pdo->prepare("SELECT client_id, praticien_id FROM conversations WHERE id=? LIMIT 1"); $st->execute([$convId]); $row = $st->fetch(PDO::FETCH_ASSOC) ?: []; $clientId = (int)($row['client_id'] ?? 0); $praticienId = (int)($row['praticien_id'] ?? 0); } $targetRef = ($targetRole === 'client') ? $clientId : $praticienId; // ✅ Trouver le compte_id du destinataire (obligatoire pour worker + badge global) $stC = $pdo->prepare(" SELECT id FROM compte WHERE role = :role AND reference_id = :ref LIMIT 1 "); $stC->execute([ ':role' => $targetRole, ':ref' => $targetRef, ]); $targetCompteId = (int)($stC->fetchColumn() ?: 0); if ($targetRef > 0) { // Texte push (simple) $title = 'Новое сообщение c uslugi.fr'; $text = 'У вас новое сообщение с uslugi.fr.'; if ($body !== '') { $snippet = mb_substr($body, 0, 120); $text = $snippet; } $url = ($targetRole === 'client') ? '/messages/?conv=' . $convId : '/messages/?conv=' . $convId; $data = json_encode([ 'message_id' => $msgId, 'sender_role' => $role, 'conversation_id' => $convId, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($targetCompteId > 0) { $pdo->prepare(" INSERT INTO push_badge_counters (compte_id, msg_unread, rdv_unread) VALUES (:cid, 1, 0) ON DUPLICATE KEY UPDATE msg_unread = LEAST(msg_unread + 1, 99), updated_at = NOW() ")->execute([':cid' => $targetCompteId]); } $insPush = $pdo->prepare(" INSERT INTO push_notifications (target_role, target_reference_id, target_compte_id, type, title, body, url, data_json, status, attempts, created_at) VALUES (:role, :ref, :cid, 'message_new', :title, :body, :url, :data, 'pending', 0, NOW()) "); $insPush->execute([ ':role' => $targetRole, ':ref' => $targetRef, ':cid' => ($targetCompteId > 0 ? $targetCompteId : null), ':title' => $title, ':body' => $text, ':url' => $url, ':data' => $data, ]); } } catch (Throwable $e) { error_log('Notif push error: ' . $e->getMessage()); } } json_response(['ok' => true, 'message_id' => $msgId]);