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:
co-authored by
Claude Sonnet 4.6
parent
2c8b597b28
commit
9ef9eeffcb
@@ -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']);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user