fix: chat page — readable content, correct unread count, immediate send

- Add extractContent() helper: parses JSON WhatsApp payloads to show
  human-readable text (button titles, body text, captions) instead of
  raw JSON in both the conversation list preview and message bubbles
- Fix unread_count subquery: was comparing outbound_queue.id with
  conversations.id (different sequences); now uses created_at comparison
- Remove broken outbound_queue secondary query from chatMessages():
  all messages (bot + admin) are already in conversations table —
  the extra join caused duplicates and wrong ordering
- chatSend() now calls OutboundWorker::processQueue() immediately after
  queuing so admin messages are sent to WhatsApp without manual trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-28 17:54:43 -05:00
co-authored by Claude Sonnet 4.6
parent 2c8b597b28
commit 9ef9eeffcb
+91 -40
View File
@@ -2026,24 +2026,29 @@ HTML;
}
$convRows = db()->query("
SELECT c.phone_number,
MAX(c.contact_name) as contact_name,
MAX(c.company_id) as company_id,
MAX(c.created_at) as last_time,
(SELECT content FROM conversations c2 WHERE c2.phone_number = c.phone_number ORDER BY c2.id DESC LIMIT 1) as last_message,
(SELECT COUNT(*) FROM conversations c3 WHERE c3.phone_number = c.phone_number AND c3.direction = 'inbound'
AND c3.id > COALESCE((SELECT MAX(c4.id) FROM conversations c4 WHERE c4.phone_number = c.phone_number AND c4.direction = 'outbound'),0)) as unread_count
MAX(c.contact_name) as contact_name,
MAX(c.company_id) as company_id,
MAX(c.created_at) as last_time,
(SELECT content FROM conversations c2 WHERE c2.phone_number = c.phone_number ORDER BY c2.id DESC LIMIT 1) as last_message,
(SELECT message_type FROM conversations c2 WHERE c2.phone_number = c.phone_number ORDER BY c2.id DESC LIMIT 1) as last_type,
(SELECT COUNT(*) FROM conversations c3
WHERE c3.phone_number = c.phone_number AND c3.direction = 'inbound'
AND c3.created_at > COALESCE(
(SELECT MAX(c4.created_at) FROM conversations c4
WHERE c4.phone_number = c.phone_number AND c4.direction = 'outbound'), '2000-01-01')) as unread_count
FROM conversations c
GROUP BY c.phone_number
ORDER BY last_time DESC
")->fetchAll();
$initialConvs = [];
foreach ($convRows as $r) {
$cid = (int)$r['company_id'];
$cid = (int)$r['company_id'];
$preview = mb_substr(self::extractContent($r['last_message'] ?? '', $r['last_type'] ?? 'text'), 0, 80);
$initialConvs[] = [
'phone' => $r['phone_number'],
'contact_name' => $r['contact_name'] ?? '',
'company_name' => $companyMap[$cid] ?? '',
'last_message' => mb_substr($r['last_message'] ?? '', 0, 80),
'last_message' => $preview,
'last_time' => $r['last_time'],
'unread_count' => (int)$r['unread_count'],
];
@@ -2373,6 +2378,54 @@ HTML;
exit;
}
// ─── Helper: extract human-readable text from raw conversation content ───
private static function extractContent(string $raw, string $type): string
{
if ($raw === '') return '';
// Inbound button_reply: "button_reply: {...}"
if (str_starts_with($raw, 'button_reply: ')) {
$j = json_decode(substr($raw, 14), true);
return $j['title'] ?? $raw;
}
// Inbound list_reply: "list_reply: {...}"
if (str_starts_with($raw, 'list_reply: ')) {
$j = json_decode(substr($raw, 12), true);
return $j['title'] ?? $raw;
}
// Try to parse as JSON
$j = json_decode($raw, true);
if (!is_array($j)) return $raw;
// Outbound text: {"text": "..."}
if (isset($j['text']) && is_string($j['text'])) return $j['text'];
// Outbound interactive: {"interactive": {"body": {"text":"..."}, "action":{...}}}
if (isset($j['interactive'])) {
$body = $j['interactive']['body']['text'] ?? '';
$btns = [];
foreach ($j['interactive']['action']['buttons'] ?? [] as $b) {
$btns[] = $b['reply']['title'] ?? '';
}
foreach ($j['interactive']['action']['sections'] ?? [] as $s) {
foreach ($s['rows'] ?? [] as $row) {
$btns[] = $row['title'] ?? '';
}
}
$hint = $btns ? ' [' . implode(' | ', array_filter($btns)) . ']' : '';
return $body . $hint;
}
// Outbound image/document
if (isset($j['caption'])) return '📎 ' . $j['caption'];
if (isset($j['link'])) return '📎 ' . $j['link'];
return $raw;
}
// ─── API: lista de conversaciones ────────────────────────────────────────
public static function chatConversations(): void
@@ -2382,11 +2435,16 @@ HTML;
// Get unique conversations with last message and unread count
$rows = $db->query("
SELECT c.phone_number,
MAX(c.contact_name) as contact_name,
MAX(c.company_id) as company_id,
MAX(c.created_at) as last_time,
(SELECT content FROM conversations c2 WHERE c2.phone_number = c.phone_number ORDER BY c2.id DESC LIMIT 1) as last_message,
(SELECT COUNT(*) FROM conversations c3 WHERE c3.phone_number = c.phone_number AND c3.direction = 'inbound' AND c3.id > COALESCE((SELECT MAX(c4.id) FROM conversations c4 WHERE c4.phone_number = c.phone_number AND c4.direction = 'outbound'), 0)) as unread_count
MAX(c.contact_name) as contact_name,
MAX(c.company_id) as company_id,
MAX(c.created_at) as last_time,
(SELECT content FROM conversations c2 WHERE c2.phone_number = c.phone_number ORDER BY c2.id DESC LIMIT 1) as last_message,
(SELECT message_type FROM conversations c2 WHERE c2.phone_number = c.phone_number ORDER BY c2.id DESC LIMIT 1) as last_type,
(SELECT COUNT(*) FROM conversations c3
WHERE c3.phone_number = c.phone_number AND c3.direction = 'inbound'
AND c3.created_at > COALESCE(
(SELECT MAX(c4.created_at) FROM conversations c4
WHERE c4.phone_number = c.phone_number AND c4.direction = 'outbound'), '2000-01-01')) as unread_count
FROM conversations c
GROUP BY c.phone_number
ORDER BY last_time DESC
@@ -2399,12 +2457,14 @@ HTML;
$conversations = [];
foreach ($rows as $r) {
$cid = (int)$r['company_id'];
$cid = (int)$r['company_id'];
$raw = $r['last_message'] ?? '';
$preview = mb_substr(self::extractContent($raw, $r['last_type'] ?? 'text'), 0, 80);
$conversations[] = [
'phone' => $r['phone_number'],
'contact_name' => $r['contact_name'] ?? '',
'company_name' => $companies[$cid] ?? '',
'last_message' => mb_substr($r['last_message'] ?? '', 0, 80),
'last_message' => $preview,
'last_time' => $r['last_time'],
'unread_count' => (int)$r['unread_count'],
];
@@ -2430,26 +2490,13 @@ HTML;
$stmt->execute([$phone]);
$messages = $stmt->fetchAll();
// Also include outbound_queue items for this phone
$stmt2 = db()->prepare("
SELECT 'outbound' as direction, message_type, payload as content, NULL as media_id, status, created_at
FROM outbound_queue
WHERE to_number = ? AND status IN ('sent','delivered','failed')
AND id > COALESCE((SELECT MAX(c.id) FROM conversations c WHERE c.phone_number = ?), 0)
ORDER BY id ASC
");
$stmt2->execute([$phone, $phone]);
$queueMsgs = $stmt2->fetchAll();
foreach ($queueMsgs as &$qm) {
$pl = json_decode($qm['content'], true);
$qm['content'] = $pl['text'] ?? $pl['caption'] ?? $qm['content'];
// Extract readable text from raw WhatsApp payloads
foreach ($messages as &$m) {
$m['content'] = self::extractContent((string)($m['content'] ?? ''), (string)($m['message_type'] ?? 'text'));
}
unset($m);
$all = array_merge($messages, $queueMsgs);
usort($all, fn($a, $b) => strcmp($a['created_at'] ?? '', $b['created_at'] ?? ''));
jsonResponse(200, ['messages' => $all]);
jsonResponse(200, ['messages' => $messages]);
}
// ─── API: enviar mensaje desde el chat ───────────────────────────────────
@@ -2470,16 +2517,20 @@ HTML;
$conv = $stmt->fetch();
$companyId = $conv ? (int)$conv['company_id'] : 1;
// Enqueue the message
$db = db();
$payload = json_encode(['text' => $text]);
$qStmt = $db->prepare("INSERT INTO outbound_queue (company_id, to_number, message_type, payload, status, created_at) VALUES (?, ?, 'text', ?, 'queued', NOW())");
$qStmt->execute([$companyId, $phone, $payload]);
// Add to conversations
// Enqueue the message then process immediately so it's sent to WhatsApp right away
$payload = json_encode(['text' => $text]);
$db->prepare("INSERT INTO outbound_queue (company_id, to_number, message_type, payload, status, created_at) VALUES (?, ?, 'text', ?, 'queued', NOW())")
->execute([$companyId, $phone, $payload]);
// Add to conversations log
$msgId = 'admin_' . time() . '_' . $phone;
$cStmt = $db->prepare("INSERT INTO conversations (company_id, message_id, phone_number, direction, message_type, content, created_at) VALUES (?, ?, ?, 'outbound', 'text', ?, NOW())");
$cStmt->execute([$companyId, $msgId, $phone, $text]);
$db->prepare("INSERT INTO conversations (company_id, message_id, phone_number, direction, message_type, content, created_at) VALUES (?, ?, ?, 'outbound', 'text', ?, NOW())")
->execute([$companyId, $msgId, $phone, $text]);
// Send now — don't wait for the manual queue processor
OutboundWorker::processQueue();
jsonResponse(200, ['status' => 'sent']);
}