mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: email intelligence — action item detection, task/appt creation, admin EMAIL tab, full voice intents
This commit is contained in:
+101
-54
@@ -300,80 +300,127 @@ if (!$reply && preg_match('/(is|are|what.s|status|state).*(on|off|light|switch|p
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Email queries ─────────────────────────────────────────────────────────
|
// ── Email + Planner voice intents (action items, create task/appt from email) ─
|
||||||
if (!$reply && preg_match('/\b(email|emails|inbox|gmail|outlook|mail|unread|messages)\b/i', $message)) {
|
if (!$reply && preg_match('/\b(email|emails|inbox|gmail|outlook|mail|unread|messages)\b/i', $message)) {
|
||||||
$emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email';
|
$lc = strtolower($message);
|
||||||
$account = 'all';
|
|
||||||
if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail';
|
|
||||||
if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook';
|
|
||||||
if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud';
|
|
||||||
|
|
||||||
$ch = curl_init($emailUrl . '?account=' . $account);
|
// ── Action items from email ───────────────────────────────────────────────
|
||||||
curl_setopt_array($ch, [
|
if (preg_match('/\b(action item|action required|need.*attention|follow.?up|things to do.*email|email.*task)\b/i', $message)) {
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
$rows = JarvisDB::query(
|
||||||
CURLOPT_HTTPHEADER => ['X-Session-Token: ' . ($_SESSION['jarvis_token'] ?? '')],
|
"SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 5"
|
||||||
CURLOPT_TIMEOUT => 20,
|
) ?? [];
|
||||||
CURLOPT_CONNECTTIMEOUT => 5,
|
if (!$rows) {
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
$reply = "No email action items pending, {$userAddr}.";
|
||||||
]);
|
} else {
|
||||||
$emailJson = curl_exec($ch);
|
$items = array_map(fn($r) =>
|
||||||
$emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
ucfirst($r['action_type']) . ': "' . mb_substr($r['subject'], 0, 60) . '" from ' . ($r['from_name'] ?: $r['from_email']),
|
||||||
curl_close($ch);
|
$rows
|
||||||
|
);
|
||||||
|
$reply = count($rows) . " email action item" . (count($rows)>1?'s':'') . " need attention, {$userAddr}: " . implode('. ', $items) . '.';
|
||||||
|
}
|
||||||
|
$source = 'email:action_items';
|
||||||
|
|
||||||
if ($emailCode === 200 && $emailJson) {
|
// ── Create task from most recent action email ─────────────────────────────
|
||||||
$ed = json_decode($emailJson, true) ?? [];
|
} elseif (preg_match('/\b(create task|add task|make.*task|task.*from.*email)\b/i', $message)) {
|
||||||
$summary = $ed['summary'] ?? [];
|
$fromMatch = null;
|
||||||
$unread = (int)($summary['total_unread'] ?? 0);
|
if (preg_match('/\bfrom\s+([a-z][\w\s\.]+)/i', $message, $fm)) $fromMatch = trim($fm[1]);
|
||||||
$recent = $summary['recent'] ?? [];
|
$where = "WHERE dismissed=0 AND task_id IS NULL AND action_type IN ('task','appointment')";
|
||||||
$accts = $ed['accounts'] ?? [];
|
$params = [];
|
||||||
|
if ($fromMatch) { $where .= ' AND (from_name LIKE ? OR from_email LIKE ?)'; $params[] = "%{$fromMatch}%"; $params[] = "%{$fromMatch}%"; }
|
||||||
|
$ea = JarvisDB::single("SELECT * FROM email_actions {$where} ORDER BY received_at DESC LIMIT 1", $params);
|
||||||
|
if ($ea) {
|
||||||
|
$title = mb_substr($ea['subject'], 0, 255);
|
||||||
|
$notes = "From: {$ea['from_name']} <{$ea['from_email']}>";
|
||||||
|
$taskId = JarvisDB::insert('INSERT INTO tasks (title,notes,category,priority) VALUES (?,?,?,?)',
|
||||||
|
[$title, $notes, 'work', 'normal']);
|
||||||
|
JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?', [$taskId, $ea['id']]);
|
||||||
|
$reply = "Task created from email: \"{$title}\", {$userAddr}.";
|
||||||
|
$source = 'email:create_task';
|
||||||
|
} else {
|
||||||
|
$reply = "No action-required emails found to create a task from, {$userAddr}.";
|
||||||
|
$source = 'email:create_task_none';
|
||||||
|
}
|
||||||
|
|
||||||
// "How many" / "any" / check → just count
|
// ── Create appointment from meeting email ─────────────────────────────────
|
||||||
if (preg_match('/\b(how many|any|check|count|unread)\b/i', $message) && !preg_match('/\bread\b/i', $message)) {
|
} elseif (preg_match('/\b(schedule|appointment|meeting|calendar).*\bemail\b|\bemail\b.*(schedule|appointment|meeting|calendar)\b/i', $message)) {
|
||||||
|
$ea = JarvisDB::single(
|
||||||
|
"SELECT * FROM email_actions WHERE dismissed=0 AND appointment_id IS NULL AND action_type='appointment' ORDER BY received_at DESC LIMIT 1"
|
||||||
|
);
|
||||||
|
if ($ea) {
|
||||||
|
$start = $ea['suggested_date'] ? $ea['suggested_date'] . ' 09:00:00' : date('Y-m-d') . ' 09:00:00';
|
||||||
|
$title = mb_substr($ea['subject'], 0, 255);
|
||||||
|
$apptId = JarvisDB::insert('INSERT INTO appointments (title,description,category,start_at) VALUES (?,?,?,?)',
|
||||||
|
[$title, "From: {$ea['from_name']}", 'work', $start]);
|
||||||
|
JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?', [$apptId, $ea['id']]);
|
||||||
|
$reply = "Appointment scheduled from email: \"{$title}\" on " . date('l, M j', strtotime($start)) . ", {$userAddr}.";
|
||||||
|
$source = 'email:create_appt';
|
||||||
|
} else {
|
||||||
|
$reply = "No meeting emails found to schedule, {$userAddr}.";
|
||||||
|
$source = 'email:create_appt_none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unread count ──────────────────────────────────────────────────────────
|
||||||
|
} elseif (preg_match('/\b(how many|any|check|count|unread)\b/i', $message) && !preg_match('/\bread\b/i', $message)) {
|
||||||
|
$emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email';
|
||||||
|
$account = 'all';
|
||||||
|
if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail';
|
||||||
|
if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook';
|
||||||
|
if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud';
|
||||||
|
$ch = curl_init($emailUrl . '?account=' . $account);
|
||||||
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_HTTPHEADER=>['X-Session-Token: '.($_SESSION['jarvis_token']??'')], CURLOPT_TIMEOUT=>20, CURLOPT_CONNECTTIMEOUT=>5, CURLOPT_SSL_VERIFYPEER=>false]);
|
||||||
|
$emailJson = curl_exec($ch); $emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
|
||||||
|
if ($emailCode === 200 && $emailJson) {
|
||||||
|
$ed = json_decode($emailJson, true) ?? [];
|
||||||
|
$unread = (int)($ed['summary']['total_unread'] ?? 0);
|
||||||
|
$aiCount = (int)($ed['action_items_count'] ?? 0);
|
||||||
|
$parts = [];
|
||||||
|
foreach ($ed['accounts'] ?? [] as $a => $r) {
|
||||||
|
if (!empty($r['unread'])) $parts[] = $r['unread'] . ' on ' . ucfirst($a);
|
||||||
|
}
|
||||||
if ($unread === 0) {
|
if ($unread === 0) {
|
||||||
$reply = "No unread emails, {$userAddr}.";
|
$reply = "No unread emails, {$userAddr}.";
|
||||||
} else {
|
} else {
|
||||||
// break down by account
|
$bd = $parts ? ' (' . implode(', ', $parts) . ')' : '';
|
||||||
$parts = [];
|
$reply = "You have {$unread} unread email" . ($unread>1?'s':'') . "{$bd}, {$userAddr}.";
|
||||||
foreach ($accts as $a => $r) {
|
|
||||||
if (!empty($r['unread'])) $parts[] = $r['unread'] . ' on ' . ucfirst($a);
|
|
||||||
}
|
|
||||||
$breakdown = $parts ? ' (' . implode(', ', $parts) . ')' : '';
|
|
||||||
$reply = "You have {$unread} unread email" . ($unread>1?'s':'') . "{$breakdown}, {$userAddr}.";
|
|
||||||
}
|
}
|
||||||
|
if ($aiCount > 0) $reply .= " {$aiCount} require action.";
|
||||||
$source = 'email:count';
|
$source = 'email:count';
|
||||||
|
}
|
||||||
|
|
||||||
// "emails from [person]" → filter by sender
|
// ── Read recent emails ────────────────────────────────────────────────────
|
||||||
} elseif (preg_match('/\bfrom\s+(.+)/i', $message, $fm)) {
|
} else {
|
||||||
$sender = strtolower(trim($fm[1]));
|
$emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email';
|
||||||
$matches = array_filter($recent, fn($m) =>
|
$account = 'all';
|
||||||
stripos($m['from_name']??'', $sender) !== false ||
|
if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail';
|
||||||
stripos($m['from_email']??'', $sender) !== false
|
if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook';
|
||||||
);
|
if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud';
|
||||||
if ($matches) {
|
$ch = curl_init($emailUrl . '?account=' . $account);
|
||||||
$m = array_values($matches)[0];
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_HTTPHEADER=>['X-Session-Token: '.($_SESSION['jarvis_token']??'')], CURLOPT_TIMEOUT=>20, CURLOPT_CONNECTTIMEOUT=>5, CURLOPT_SSL_VERIFYPEER=>false]);
|
||||||
$reply = "Email from {$m['from_name']}: \"{$m['subject']}\" — {$m['date']}.";
|
$emailJson = curl_exec($ch); $emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
|
||||||
if (!empty($m['preview'])) $reply .= ' Preview: ' . mb_substr($m['preview'], 0, 150);
|
if ($emailCode === 200 && $emailJson) {
|
||||||
} else {
|
$ed = json_decode($emailJson, true) ?? [];
|
||||||
$reply = "No recent emails from {$sender}, {$userAddr}.";
|
$recent = $ed['summary']['recent'] ?? [];
|
||||||
|
$unread = (int)($ed['summary']['total_unread'] ?? 0);
|
||||||
|
$aiCount = (int)($ed['action_items_count'] ?? 0);
|
||||||
|
// filter by sender if mentioned
|
||||||
|
if (preg_match('/\bfrom\s+(.+)/i', $message, $fm)) {
|
||||||
|
$sender = strtolower(trim($fm[1]));
|
||||||
|
$recent = array_values(array_filter($recent, fn($m) =>
|
||||||
|
stripos($m['from_name']??'', $sender)!==false || stripos($m['from_email']??'', $sender)!==false));
|
||||||
}
|
}
|
||||||
$source = 'email:search';
|
$toRead = array_filter($recent, fn($m) => $m['unread']) ?: $recent;
|
||||||
|
$toRead = array_slice(array_values($toRead), 0, 3);
|
||||||
// "read" / "latest" / "recent" → read top emails
|
|
||||||
} else {
|
|
||||||
$unreadOnly = array_values(array_filter($recent, fn($m) => $m['unread']));
|
|
||||||
$toRead = $unreadOnly ?: $recent;
|
|
||||||
$toRead = array_slice($toRead, 0, 3);
|
|
||||||
if (empty($toRead)) {
|
if (empty($toRead)) {
|
||||||
$reply = "No emails to report, {$userAddr}.";
|
$reply = "No emails to report, {$userAddr}.";
|
||||||
} else {
|
} else {
|
||||||
$lines = [];
|
$lines = [];
|
||||||
foreach ($toRead as $m) {
|
foreach ($toRead as $m) {
|
||||||
$flag = $m['unread'] ? '' : '';
|
$acct = isset($m['account']) ? ' [' . strtoupper($m['account']) . ']' : '';
|
||||||
$acct = isset($m['account']) ? ' [' . strtoupper($m['account']) . ']' : '';
|
|
||||||
$lines[] = "From {$m['from_name']}: \"{$m['subject']}\", {$m['date']}{$acct}.";
|
$lines[] = "From {$m['from_name']}: \"{$m['subject']}\", {$m['date']}{$acct}.";
|
||||||
}
|
}
|
||||||
$intro = $unread > 0 ? "You have {$unread} unread. " : "";
|
$intro = $unread > 0 ? "You have {$unread} unread. " : "";
|
||||||
$reply = $intro . implode(' ', $lines);
|
$reply = $intro . implode(' ', $lines);
|
||||||
|
if ($aiCount > 0) $reply .= " {$aiCount} require action — say 'email action items' for details.";
|
||||||
}
|
}
|
||||||
$source = 'email:read';
|
$source = 'email:read';
|
||||||
}
|
}
|
||||||
|
|||||||
+173
-74
@@ -1,139 +1,238 @@
|
|||||||
<?php
|
<?php
|
||||||
// JARVIS Email endpoint — IMAP reader for Gmail, Outlook, iCloud.
|
// JARVIS Email — IMAP reader + action item extractor for Gmail, Outlook, iCloud.
|
||||||
// GET ?action=email → recent + unread summary (cached 5 min)
|
// GET ?account=all|gmail|outlook|icloud → recent + unread summary (cached 5 min)
|
||||||
// GET ?action=email&account=gmail → Gmail only
|
// GET ?action=action_items → detected tasks/appointments from emails
|
||||||
// GET ?action=email&force=1 → bypass cache, fetch live
|
// POST action=create_task → create task from email_action id
|
||||||
// GET ?action=email&search=from:john → search emails
|
// POST action=create_appt → create appointment from email_action id
|
||||||
|
// POST action=dismiss → dismiss email_action
|
||||||
|
// GET ?force=1 → bypass cache
|
||||||
|
|
||||||
function imapFetch(string $host, string $user, string $pass, int $maxMsgs = 10): array {
|
// ── IMAP fetch ────────────────────────────────────────────────────────────────
|
||||||
|
function imapFetch(string $host, string $user, string $pass, int $maxMsgs = 15): array {
|
||||||
if (!$user || !$pass) return ['error' => 'not_configured'];
|
if (!$user || !$pass) return ['error' => 'not_configured'];
|
||||||
|
|
||||||
$mbox = @imap_open($host, $user, $pass, 0, 1, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']);
|
$mbox = @imap_open($host, $user, $pass, 0, 1, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']);
|
||||||
if (!$mbox) return ['error' => imap_last_error() ?: 'connection_failed'];
|
if (!$mbox) return ['error' => imap_last_error() ?: 'connection_failed'];
|
||||||
|
$total = imap_num_msg($mbox);
|
||||||
$total = imap_num_msg($mbox);
|
$unseen = imap_search($mbox, 'UNSEEN') ?: [];
|
||||||
$unseen = imap_search($mbox, 'UNSEEN') ?: [];
|
$unread = count($unseen);
|
||||||
$unread = count($unseen);
|
$msgs = [];
|
||||||
|
for ($i = $total; $i >= max(1, $total - $maxMsgs + 1); $i--) {
|
||||||
// Fetch most recent messages (newest first)
|
$hdr = @imap_headerinfo($mbox, $i);
|
||||||
$start = max(1, $total - $maxMsgs + 1);
|
|
||||||
$msgs = [];
|
|
||||||
for ($i = $total; $i >= $start; $i--) {
|
|
||||||
$hdr = @imap_headerinfo($mbox, $i);
|
|
||||||
if (!$hdr) continue;
|
if (!$hdr) continue;
|
||||||
|
$from = $hdr->from[0] ?? null;
|
||||||
$from = $hdr->from[0] ?? null;
|
$fromName = $from && isset($from->personal) ? imap_utf8($from->personal) : '';
|
||||||
$fromName = $from ? (isset($from->personal) ? imap_utf8($from->personal) : '') : '';
|
|
||||||
$fromEmail = $from ? ($from->mailbox . '@' . ($from->host ?? '')) : '';
|
$fromEmail = $from ? ($from->mailbox . '@' . ($from->host ?? '')) : '';
|
||||||
$subject = isset($hdr->subject) ? imap_utf8($hdr->subject) : '(no subject)';
|
$subject = isset($hdr->subject) ? imap_utf8($hdr->subject) : '(no subject)';
|
||||||
$date = isset($hdr->date) ? date('M j g:ia', strtotime($hdr->date)) : '';
|
$date = isset($hdr->date) ? date('M j g:ia', strtotime($hdr->date)) : '';
|
||||||
|
$dateRaw = isset($hdr->date) ? date('Y-m-d H:i:s', strtotime($hdr->date)) : null;
|
||||||
$isUnread = in_array($i, $unseen);
|
$isUnread = in_array($i, $unseen);
|
||||||
|
$msgId = md5(($hdr->message_id ?? '') ?: ($fromEmail . $subject . $date));
|
||||||
// Fetch plain text preview (first 300 chars)
|
// Preview
|
||||||
$preview = '';
|
$preview = '';
|
||||||
$struct = @imap_fetchstructure($mbox, $i);
|
$struct = @imap_fetchstructure($mbox, $i);
|
||||||
if ($struct) {
|
if ($struct) {
|
||||||
if ($struct->type === 0) {
|
if ($struct->type === 0) {
|
||||||
// Single-part message
|
|
||||||
$raw = @imap_fetchbody($mbox, $i, '1');
|
$raw = @imap_fetchbody($mbox, $i, '1');
|
||||||
$enc = $struct->encoding ?? 0;
|
$enc = $struct->encoding ?? 0;
|
||||||
if ($enc === 3) $raw = base64_decode($raw);
|
if ($enc === 3) $raw = base64_decode($raw);
|
||||||
elseif ($enc === 4) $raw = quoted_printable_decode($raw);
|
elseif ($enc === 4) $raw = quoted_printable_decode($raw);
|
||||||
$preview = mb_substr(strip_tags($raw), 0, 300);
|
$preview = mb_substr(strip_tags($raw), 0, 400);
|
||||||
} else {
|
} else {
|
||||||
// Multi-part — find first text/plain part
|
|
||||||
foreach (($struct->parts ?? []) as $idx => $part) {
|
foreach (($struct->parts ?? []) as $idx => $part) {
|
||||||
if ($part->type === 0) { // text
|
if ($part->type === 0) {
|
||||||
$raw = @imap_fetchbody($mbox, $i, (string)($idx + 1));
|
$raw = @imap_fetchbody($mbox, $i, (string)($idx + 1));
|
||||||
$enc = $part->encoding ?? 0;
|
$enc = $part->encoding ?? 0;
|
||||||
if ($enc === 3) $raw = base64_decode($raw);
|
if ($enc === 3) $raw = base64_decode($raw);
|
||||||
elseif ($enc === 4) $raw = quoted_printable_decode($raw);
|
elseif ($enc === 4) $raw = quoted_printable_decode($raw);
|
||||||
$preview = mb_substr(strip_tags($raw), 0, 300);
|
$preview = mb_substr(strip_tags($raw), 0, 400);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$preview = trim(preg_replace('/\s+/', ' ', $preview));
|
$preview = trim(preg_replace('/\s+/', ' ', $preview));
|
||||||
|
|
||||||
$msgs[] = [
|
$msgs[] = [
|
||||||
'id' => $i,
|
'id' => $i,
|
||||||
'from_name' => $fromName ?: $fromEmail,
|
'msg_id' => $msgId,
|
||||||
'from_email'=> $fromEmail,
|
'from_name' => $fromName ?: $fromEmail,
|
||||||
'subject' => $subject,
|
'from_email' => $fromEmail,
|
||||||
'date' => $date,
|
'subject' => $subject,
|
||||||
'unread' => $isUnread,
|
'date' => $date,
|
||||||
'preview' => $preview,
|
'date_raw' => $dateRaw,
|
||||||
|
'unread' => $isUnread,
|
||||||
|
'preview' => $preview,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
imap_close($mbox);
|
imap_close($mbox);
|
||||||
return ['total' => $total, 'unread' => $unread, 'messages' => $msgs];
|
return ['total' => $total, 'unread' => $unread, 'messages' => $msgs];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Action item detection ─────────────────────────────────────────────────────
|
||||||
|
function detectActionItem(string $subject, string $preview): array {
|
||||||
|
$text = strtolower($subject . ' ' . mb_substr($preview, 0, 500));
|
||||||
|
$apptKw = ['meeting','zoom call','teams call','video call','conference call','join us','calendar invite',
|
||||||
|
'appointment','interview','webinar','you are invited','let\'s meet','lets meet',
|
||||||
|
'scheduled for','scheduled at','set up a call','book a call','at \d{1,2}(:\d{2})?\s*(am|pm)'];
|
||||||
|
$taskKw = ['action required','action needed','please review','please respond','please confirm',
|
||||||
|
'please send','please provide','please complete','please sign','please approve',
|
||||||
|
'follow up','follow-up','reminder:','deadline','due by','due date',
|
||||||
|
'asap','urgent','your attention','needs your','waiting for you',
|
||||||
|
'can you','could you','would you please','i need you to','kindly'];
|
||||||
|
$apptScore = 0;
|
||||||
|
$taskScore = 0;
|
||||||
|
foreach ($apptKw as $kw) {
|
||||||
|
if (@preg_match('/' . $kw . '/i', $text)) { $apptScore += 25; break; }
|
||||||
|
}
|
||||||
|
foreach ($taskKw as $kw) {
|
||||||
|
if (strpos($text, $kw) !== false) { $taskScore += 20; break; }
|
||||||
|
}
|
||||||
|
// Bonus: explicit deadline date in subject
|
||||||
|
if (preg_match('/\b(by\s+)?(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|\d{1,2}\/\d{1,2})\b/i', $subject)) {
|
||||||
|
$taskScore += 15;
|
||||||
|
}
|
||||||
|
// Suggested date extraction
|
||||||
|
$sugDate = null;
|
||||||
|
if (preg_match('/\b(tomorrow|today|next\s+\w+|this\s+(monday|tuesday|wednesday|thursday|friday)|monday|tuesday|wednesday|thursday|friday|\d{1,2}[\/\-]\d{1,2}(?:[\/\-]\d{2,4})?)\b/i', $subject . ' ' . mb_substr($preview, 0, 200), $dm)) {
|
||||||
|
$ts = strtotime($dm[0]);
|
||||||
|
if ($ts !== false && $ts > time() - 86400) $sugDate = date('Y-m-d', $ts);
|
||||||
|
}
|
||||||
|
$type = null;
|
||||||
|
if ($apptScore >= 25 && $apptScore >= $taskScore) $type = 'appointment';
|
||||||
|
elseif ($taskScore >= 20) $type = 'task';
|
||||||
|
return ['type' => $type, 'suggested_date' => $sugDate];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Upsert email actions ──────────────────────────────────────────────────────
|
||||||
|
function storeEmailActions(string $account, array $messages): void {
|
||||||
|
foreach ($messages as $msg) {
|
||||||
|
$ai = detectActionItem($msg['subject'], $msg['preview'] ?? '');
|
||||||
|
if (!$ai['type']) continue;
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO email_actions (account,message_uid,from_email,from_name,subject,preview,received_at,action_type,suggested_title,suggested_date)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||||
|
ON DUPLICATE KEY UPDATE action_type=VALUES(action_type), suggested_title=VALUES(suggested_title),
|
||||||
|
suggested_date=VALUES(suggested_date), from_name=VALUES(from_name),
|
||||||
|
preview=VALUES(preview), received_at=VALUES(received_at)",
|
||||||
|
[$account, $msg['msg_id'], $msg['from_email'], $msg['from_name'], $msg['subject'],
|
||||||
|
mb_substr($msg['preview'] ?? '', 0, 500), $msg['date_raw'],
|
||||||
|
$ai['type'], mb_substr($msg['subject'], 0, 255), $ai['suggested_date']]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Route ─────────────────────────────────────────────────────────────────────
|
||||||
|
$actionType = $data['action'] ?? $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
// ── Create task from email action ─────────────────────────────────────────────
|
||||||
|
if ($method === 'POST' && $actionType === 'create_task') {
|
||||||
|
$id = (int)($data['id'] ?? 0);
|
||||||
|
if (!$id) { echo json_encode(['error' => 'Missing id']); exit; }
|
||||||
|
$ea = JarvisDB::single('SELECT * FROM email_actions WHERE id=?', [$id]);
|
||||||
|
if (!$ea) { echo json_encode(['error' => 'Not found']); exit; }
|
||||||
|
$title = trim($data['title'] ?? $ea['suggested_title']);
|
||||||
|
$due_date = trim($data['due_date'] ?? $ea['suggested_date'] ?? '');
|
||||||
|
$notes = "From: {$ea['from_name']} <{$ea['from_email']}>\nSubject: {$ea['subject']}";
|
||||||
|
$taskId = JarvisDB::insert(
|
||||||
|
'INSERT INTO tasks (title,notes,category,priority,due_date) VALUES (?,?,?,?,?)',
|
||||||
|
[$title, $notes, 'work', 'normal', $due_date ?: null]
|
||||||
|
);
|
||||||
|
JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?', [$taskId, $id]);
|
||||||
|
echo json_encode(['success' => true, 'task_id' => $taskId]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Create appointment from email action ──────────────────────────────────────
|
||||||
|
if ($method === 'POST' && $actionType === 'create_appt') {
|
||||||
|
$id = (int)($data['id'] ?? 0);
|
||||||
|
if (!$id) { echo json_encode(['error' => 'Missing id']); exit; }
|
||||||
|
$ea = JarvisDB::single('SELECT * FROM email_actions WHERE id=?', [$id]);
|
||||||
|
if (!$ea) { echo json_encode(['error' => 'Not found']); exit; }
|
||||||
|
$title = trim($data['title'] ?? $ea['suggested_title']);
|
||||||
|
$start_at = trim($data['start_at'] ?? '');
|
||||||
|
if (!$start_at && $ea['suggested_date']) $start_at = $ea['suggested_date'] . ' 09:00:00';
|
||||||
|
if (!$start_at) $start_at = date('Y-m-d') . ' 09:00:00';
|
||||||
|
$desc = "From: {$ea['from_name']} <{$ea['from_email']}>";
|
||||||
|
$apptId = JarvisDB::insert(
|
||||||
|
'INSERT INTO appointments (title,description,category,start_at) VALUES (?,?,?,?)',
|
||||||
|
[$title, $desc, 'work', $start_at]
|
||||||
|
);
|
||||||
|
JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?', [$apptId, $id]);
|
||||||
|
echo json_encode(['success' => true, 'appointment_id' => $apptId]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dismiss email action ──────────────────────────────────────────────────────
|
||||||
|
if ($method === 'POST' && $actionType === 'dismiss') {
|
||||||
|
$id = (int)($data['id'] ?? 0);
|
||||||
|
if ($id) JarvisDB::execute('UPDATE email_actions SET dismissed=1 WHERE id=?', [$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List detected action items ────────────────────────────────────────────────
|
||||||
|
if ($actionType === 'action_items') {
|
||||||
|
$rows = JarvisDB::query(
|
||||||
|
"SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL
|
||||||
|
ORDER BY received_at DESC LIMIT 50"
|
||||||
|
) ?? [];
|
||||||
|
echo json_encode(['action_items' => $rows, 'count' => count($rows)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inbox fetch (main) ────────────────────────────────────────────────────────
|
||||||
$account = $data['account'] ?? $_GET['account'] ?? 'all';
|
$account = $data['account'] ?? $_GET['account'] ?? 'all';
|
||||||
$force = !empty($data['force']) || !empty($_GET['force']);
|
$force = !empty($data['force']) || !empty($_GET['force']);
|
||||||
$search = trim($data['search'] ?? $_GET['search'] ?? '');
|
|
||||||
|
|
||||||
$cacheKey = 'email_' . $account;
|
$cacheKey = 'email_' . $account;
|
||||||
$cacheTtl = 300; // 5 minutes
|
$cacheTtl = 300;
|
||||||
|
|
||||||
if (!$force) {
|
if (!$force) {
|
||||||
$cached = JarvisDB::single(
|
$cached = JarvisDB::single("SELECT data, UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key=?", [$cacheKey]);
|
||||||
"SELECT data, UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key=?",
|
|
||||||
[$cacheKey]
|
|
||||||
);
|
|
||||||
if ($cached && (time() - (int)$cached['ts']) < $cacheTtl) {
|
if ($cached && (time() - (int)$cached['ts']) < $cacheTtl) {
|
||||||
$out = json_decode($cached['data'], true);
|
$out = json_decode($cached['data'], true);
|
||||||
$out['cache_age_s'] = time() - (int)$cached['ts'];
|
$out['cache_age_s'] = time() - (int)$cached['ts'];
|
||||||
|
// Merge unactioned email_actions count
|
||||||
|
$out['action_items_count'] = (int)(JarvisDB::single("SELECT COUNT(*) c FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL")['c'] ?? 0);
|
||||||
echo json_encode($out);
|
echo json_encode($out);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = ['accounts' => [], 'summary' => []];
|
$result = ['accounts' => []];
|
||||||
|
|
||||||
if (in_array($account, ['all', 'gmail']) && defined('GMAIL_USER') && GMAIL_USER) {
|
$accounts_to_fetch = [];
|
||||||
$r = imapFetch('{imap.gmail.com:993/imap/ssl}INBOX', GMAIL_USER, GMAIL_PASS);
|
if (in_array($account, ['all', 'gmail']) && defined('GMAIL_USER') && GMAIL_USER) $accounts_to_fetch['gmail'] = ['{imap.gmail.com:993/imap/ssl}INBOX', GMAIL_USER, GMAIL_PASS];
|
||||||
$result['accounts']['gmail'] = $r;
|
if (in_array($account, ['all', 'outlook']) && defined('OUTLOOK_USER') && OUTLOOK_USER) $accounts_to_fetch['outlook'] = ['{outlook.office365.com:993/imap/ssl}INBOX', OUTLOOK_USER, OUTLOOK_PASS];
|
||||||
|
if (in_array($account, ['all', 'icloud']) && defined('ICLOUD_USER') && ICLOUD_USER) $accounts_to_fetch['icloud'] = ['{imap.mail.me.com:993/imap/ssl}INBOX', ICLOUD_USER, ICLOUD_PASS];
|
||||||
|
|
||||||
|
foreach ($accounts_to_fetch as $acct => [$host, $user, $pass]) {
|
||||||
|
$r = imapFetch($host, $user, $pass, 15);
|
||||||
|
$result['accounts'][$acct] = $r;
|
||||||
|
if (!empty($r['messages'])) storeEmailActions($acct, $r['messages']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($account, ['all', 'outlook']) && defined('OUTLOOK_USER') && OUTLOOK_USER) {
|
// Build summary
|
||||||
$r = imapFetch('{outlook.office365.com:993/imap/ssl}INBOX', OUTLOOK_USER, OUTLOOK_PASS);
|
|
||||||
$result['accounts']['outlook'] = $r;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($account, ['all', 'icloud']) && defined('ICLOUD_USER') && ICLOUD_USER) {
|
|
||||||
$r = imapFetch('{imap.mail.me.com:993/imap/ssl}INBOX', ICLOUD_USER, ICLOUD_PASS);
|
|
||||||
$result['accounts']['icloud'] = $r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build summary across all accounts
|
|
||||||
$totalUnread = 0;
|
$totalUnread = 0;
|
||||||
$allMessages = [];
|
$allMessages = [];
|
||||||
foreach ($result['accounts'] as $acct => $r) {
|
foreach ($result['accounts'] as $acct => $r) {
|
||||||
if (isset($r['unread'])) $totalUnread += (int)$r['unread'];
|
if (isset($r['unread'])) $totalUnread += (int)$r['unread'];
|
||||||
if (!empty($r['messages'])) {
|
foreach ($r['messages'] ?? [] as $m) {
|
||||||
foreach ($r['messages'] as $m) {
|
$m['account'] = $acct;
|
||||||
$m['account'] = $acct;
|
$allMessages[] = $m;
|
||||||
$allMessages[] = $m;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Sort by date descending
|
||||||
|
usort($allMessages, fn($a,$b) => strtotime($b['date_raw']??'0') - strtotime($a['date_raw']??'0'));
|
||||||
|
|
||||||
// Sort by date descending (approximate — already newest first per account)
|
|
||||||
$result['summary'] = [
|
$result['summary'] = [
|
||||||
'total_unread' => $totalUnread,
|
'total_unread' => $totalUnread,
|
||||||
'recent' => array_slice($allMessages, 0, 10),
|
'recent' => array_slice($allMessages, 0, 15),
|
||||||
'fetched_at' => date('c'),
|
'fetched_at' => date('c'),
|
||||||
];
|
];
|
||||||
|
$result['action_items_count'] = (int)(JarvisDB::single("SELECT COUNT(*) c FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL")['c'] ?? 0);
|
||||||
|
|
||||||
// Cache result
|
|
||||||
JarvisDB::execute(
|
JarvisDB::execute(
|
||||||
"INSERT INTO api_cache (cache_key, data, updated_at) VALUES (?,?,NOW())
|
"INSERT INTO api_cache (cache_key,data,updated_at) VALUES (?,?,NOW()) ON DUPLICATE KEY UPDATE data=VALUES(data),updated_at=NOW()",
|
||||||
ON DUPLICATE KEY UPDATE data=VALUES(data), updated_at=NOW()",
|
|
||||||
[$cacheKey, json_encode($result)]
|
[$cacheKey, json_encode($result)]
|
||||||
);
|
);
|
||||||
|
|
||||||
$result['cache_age_s'] = 0;
|
$result['cache_age_s'] = 0;
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
|
|||||||
@@ -320,6 +320,49 @@ if ($action) {
|
|||||||
j(['vms'=>$pve['vms']??[],'containers'=>$pve['containers']??[],'node_info'=>$pve['node_info']??[],'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
|
j(['vms'=>$pve['vms']??[],'containers'=>$pve['containers']??[],'node_info'=>$pve['node_info']??[],'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
|
||||||
|
|
||||||
// ── USERS ────────────────────────────────────────────────────────────
|
// ── USERS ────────────────────────────────────────────────────────────
|
||||||
|
case 'email_inbox':
|
||||||
|
// Call via server's own IP — REMOTE_ADDR matches JARVIS_IP so auth bypass applies
|
||||||
|
$acct = $_GET['account'] ?? 'all';
|
||||||
|
$force = !empty($_GET['force']) ? '&force=1' : '';
|
||||||
|
$ch = curl_init('https://165.22.1.228/api/email?account=' . $acct . $force);
|
||||||
|
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>25,
|
||||||
|
CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>false,
|
||||||
|
CURLOPT_HTTPHEADER=>['Host: jarvis.orbishosting.com']]);
|
||||||
|
$r = curl_exec($ch); $code = curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
|
||||||
|
if($code===200 && $r) j(json_decode($r,true));
|
||||||
|
else j(['error'=>'Email fetch failed (HTTP '.$code.')']);
|
||||||
|
|
||||||
|
case 'email_action_items':
|
||||||
|
$rows = JarvisDB::query("SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 100") ?? [];
|
||||||
|
j(['action_items'=>$rows]);
|
||||||
|
|
||||||
|
case 'email_create_task':
|
||||||
|
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
|
||||||
|
$ea=JarvisDB::single('SELECT * FROM email_actions WHERE id=?',[$id]); if(!$ea) bad('Not found');
|
||||||
|
$title=trim($_POST['title']??$ea['suggested_title']);
|
||||||
|
$due=trim($_POST['due_date']??$ea['suggested_date']??'');
|
||||||
|
$notes="From: {$ea['from_name']} <{$ea['from_email']}>\nSubject: {$ea['subject']}";
|
||||||
|
$tid=JarvisDB::insert('INSERT INTO tasks(title,notes,category,priority,due_date)VALUES(?,?,?,?,?)',
|
||||||
|
[$title,$notes,'work','normal',$due?:null]);
|
||||||
|
JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?',[$tid,$id]);
|
||||||
|
j(['ok'=>true,'task_id'=>$tid]);
|
||||||
|
|
||||||
|
case 'email_create_appt':
|
||||||
|
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
|
||||||
|
$ea=JarvisDB::single('SELECT * FROM email_actions WHERE id=?',[$id]); if(!$ea) bad('Not found');
|
||||||
|
$title=trim($_POST['title']??$ea['suggested_title']);
|
||||||
|
$start=trim($_POST['start_at']??'');
|
||||||
|
if(!$start) $start=($ea['suggested_date']??date('Y-m-d')).' 09:00:00';
|
||||||
|
$aid=JarvisDB::insert('INSERT INTO appointments(title,description,category,start_at)VALUES(?,?,?,?)',
|
||||||
|
[$title,"From: {$ea['from_name']} <{$ea['from_email']}>",'work',$start]);
|
||||||
|
JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?',[$aid,$id]);
|
||||||
|
j(['ok'=>true,'appointment_id'=>$aid]);
|
||||||
|
|
||||||
|
case 'email_dismiss':
|
||||||
|
$id=(int)($_POST['id']??0);
|
||||||
|
if($id) JarvisDB::execute('UPDATE email_actions SET dismissed=1 WHERE id=?',[$id]);
|
||||||
|
j(['ok'=>true]);
|
||||||
|
|
||||||
case 'task_list':
|
case 'task_list':
|
||||||
$status = trim($_GET['status'] ?? '');
|
$status = trim($_GET['status'] ?? '');
|
||||||
$category = trim($_GET['category'] ?? '');
|
$category = trim($_GET['category'] ?? '');
|
||||||
@@ -612,6 +655,8 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
|||||||
<div class="nav-item" data-tab="ha" onclick="nav(this)">HOME ASSISTANT</div>
|
<div class="nav-item" data-tab="ha" onclick="nav(this)">HOME ASSISTANT</div>
|
||||||
<div class="nav-item" data-tab="news" onclick="nav(this)">NEWS</div>
|
<div class="nav-item" data-tab="news" onclick="nav(this)">NEWS</div>
|
||||||
<div class="nav-item" data-tab="vms" onclick="nav(this)">PROXMOX VMs</div>
|
<div class="nav-item" data-tab="vms" onclick="nav(this)">PROXMOX VMs</div>
|
||||||
|
<div class="nav-section">COMMUNICATIONS</div>
|
||||||
|
<div class="nav-item" data-tab="email" onclick="nav(this)">📧 EMAIL</div>
|
||||||
<div class="nav-section">PLANNER</div>
|
<div class="nav-section">PLANNER</div>
|
||||||
<div class="nav-item" data-tab="tasks" onclick="nav(this)">📋 TASKS</div>
|
<div class="nav-item" data-tab="tasks" onclick="nav(this)">📋 TASKS</div>
|
||||||
<div class="nav-item" data-tab="appointments" onclick="nav(this)">📅 APPOINTMENTS</div>
|
<div class="nav-item" data-tab="appointments" onclick="nav(this)">📅 APPOINTMENTS</div>
|
||||||
@@ -772,6 +817,29 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
|||||||
<div id="sites-content"><div class="loading">SCANNING...</div></div>
|
<div id="sites-content"><div class="loading">SCANNING...</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- EMAIL -->
|
||||||
|
<div class="tab" id="tab-email">
|
||||||
|
<div class="page-title">EMAIL INTELLIGENCE
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-sm" id="email-tab-inbox" onclick="emailShowTab('inbox')" style="background:rgba(0,212,255,0.15)">📥 INBOX</button>
|
||||||
|
<button class="btn btn-sm" id="email-tab-actions" onclick="emailShowTab('actions')">⚡ ACTION ITEMS <span id="email-ai-badge" style="background:var(--orange);color:#000;border-radius:10px;padding:0 5px;font-size:0.6rem;margin-left:4px"></span></button>
|
||||||
|
<select id="email-acct-filter" onchange="loadEmailInbox()" class="filter-sel">
|
||||||
|
<option value="all">ALL ACCOUNTS</option>
|
||||||
|
<option value="gmail">Gmail</option>
|
||||||
|
<option value="outlook">Outlook</option>
|
||||||
|
<option value="icloud">iCloud</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm" onclick="loadEmailInbox(true)">↺ REFRESH</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="email-inbox-view">
|
||||||
|
<div class="tbl-wrap" id="email-tbl"></div>
|
||||||
|
</div>
|
||||||
|
<div id="email-actions-view" style="display:none">
|
||||||
|
<div class="tbl-wrap" id="email-actions-tbl"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- TASKS -->
|
<!-- TASKS -->
|
||||||
<div class="tab" id="tab-tasks">
|
<div class="tab" id="tab-tasks">
|
||||||
<div class="page-title">TASKS
|
<div class="page-title">TASKS
|
||||||
@@ -919,6 +987,7 @@ function loadTab(tab) {
|
|||||||
vms: loadVMs,
|
vms: loadVMs,
|
||||||
sites: loadSites,
|
sites: loadSites,
|
||||||
users: loadUsers,
|
users: loadUsers,
|
||||||
|
email: ()=>{ loadEmailInbox(); loadEmailActionItems(); },
|
||||||
tasks: loadTasks,
|
tasks: loadTasks,
|
||||||
appointments: loadAppts,
|
appointments: loadAppts,
|
||||||
})[tab]?.();
|
})[tab]?.();
|
||||||
@@ -1658,6 +1727,86 @@ document.getElementById('app').style.display='flex';
|
|||||||
document.getElementById('adminUser').textContent = '<?= htmlspecialchars($_SESSION['admin_name'] ?? $_SESSION['admin_user']) ?>'.toUpperCase();
|
document.getElementById('adminUser').textContent = '<?= htmlspecialchars($_SESSION['admin_name'] ?? $_SESSION['admin_user']) ?>'.toUpperCase();
|
||||||
initApp();
|
initApp();
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
// ── EMAIL ───────────────────────────────────────────────────────────────────
|
||||||
|
let _emailCurrentTab = 'inbox';
|
||||||
|
|
||||||
|
function emailShowTab(tab) {
|
||||||
|
_emailCurrentTab = tab;
|
||||||
|
document.getElementById('email-inbox-view').style.display = tab==='inbox' ? '' : 'none';
|
||||||
|
document.getElementById('email-actions-view').style.display = tab==='actions' ? '' : 'none';
|
||||||
|
document.getElementById('email-tab-inbox').style.background = tab==='inbox' ? 'rgba(0,212,255,0.15)' : '';
|
||||||
|
document.getElementById('email-tab-actions').style.background = tab==='actions' ? 'rgba(0,212,255,0.15)' : '';
|
||||||
|
if (tab === 'actions') loadEmailActionItems();
|
||||||
|
else loadEmailInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEmailInbox(force=false) {
|
||||||
|
const acct = document.getElementById('email-acct-filter')?.value || 'all';
|
||||||
|
const el = document.getElementById('email-tbl');
|
||||||
|
if (el) el.innerHTML = '<div class="loading">FETCHING EMAIL…</div>';
|
||||||
|
const d = await api('email_inbox', {account: acct, ...(force?{force:1}:{})});
|
||||||
|
if (d.error) { el.innerHTML = `<div class="loading text-red">${d.error}</div>`; return; }
|
||||||
|
// Update action item badge
|
||||||
|
const badge = document.getElementById('email-ai-badge');
|
||||||
|
if (badge && d.action_items_count) badge.textContent = d.action_items_count; else if(badge) badge.textContent = '';
|
||||||
|
const msgs = d.summary?.recent || [];
|
||||||
|
if (!msgs.length) { el.innerHTML='<div class="loading">No messages.</div>'; return; }
|
||||||
|
const rows = msgs.map(m => {
|
||||||
|
const ai = m.action_type ? `<span style="background:${m.action_type==='appointment'?'var(--cyan)':'var(--orange)'};color:#000;border-radius:3px;padding:0 4px;font-size:0.55rem">${m.action_type.toUpperCase()}</span> ` : '';
|
||||||
|
const unread = m.unread ? `<span style="color:var(--cyan);font-weight:700">●</span> ` : '';
|
||||||
|
const acctBadge = m.account ? `<span style="color:var(--text-dim);font-size:0.58rem">[${m.account.toUpperCase()}]</span>` : '';
|
||||||
|
return `<tr${m.unread?' style="background:rgba(0,212,255,0.04)"':''}>
|
||||||
|
<td style="width:16px">${unread}</td>
|
||||||
|
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(m.from_name||m.from_email||'')}</td>
|
||||||
|
<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${ai}${esc(m.subject||'')}</td>
|
||||||
|
<td style="color:var(--text-dim);font-size:0.62rem;white-space:nowrap">${esc(m.date||'')} ${acctBadge}</td>
|
||||||
|
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:0.62rem">${esc((m.preview||'').substring(0,120))}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
el.innerHTML = `<table><thead><tr><th></th><th>FROM</th><th>SUBJECT</th><th>DATE</th><th>PREVIEW</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEmailActionItems() {
|
||||||
|
const el = document.getElementById('email-actions-tbl');
|
||||||
|
if (!el) return;
|
||||||
|
const d = await api('email_action_items');
|
||||||
|
const items = d.action_items || [];
|
||||||
|
const badge = document.getElementById('email-ai-badge');
|
||||||
|
if (badge) badge.textContent = items.length || '';
|
||||||
|
if (!items.length) { el.innerHTML='<div class="loading">No action items pending — inbox is clear.</div>'; return; }
|
||||||
|
const rows = items.map(it => {
|
||||||
|
const typeColor = it.action_type==='appointment' ? 'var(--cyan)' : 'var(--orange)';
|
||||||
|
const sugDate = it.suggested_date ? `<input type="date" id="ead-${it.id}" value="${it.suggested_date}" style="background:#060a0e;border:1px solid var(--border2);color:var(--text);padding:2px 4px;font-size:0.6rem;width:110px">` : `<input type="date" id="ead-${it.id}" style="background:#060a0e;border:1px solid var(--border2);color:var(--text);padding:2px 4px;font-size:0.6rem;width:110px">`;
|
||||||
|
const titleIn = `<input id="eat-${it.id}" value="${esc((it.suggested_title||it.subject||'').substring(0,80))}" style="background:#060a0e;border:1px solid var(--border2);color:var(--text);padding:2px 6px;font-size:0.65rem;width:200px">`;
|
||||||
|
const btnTask = `<button class="btn btn-xs" style="border-color:var(--orange);color:var(--orange)" onclick="emailMakeTask(${it.id})">+ TASK</button>`;
|
||||||
|
const btnAppt = `<button class="btn btn-xs" style="border-color:var(--cyan);color:var(--cyan)" onclick="emailMakeAppt(${it.id})">📅 APPT</button>`;
|
||||||
|
const btnDismiss = `<button class="btn btn-xs" onclick="emailDismiss(${it.id})">✗ DISMISS</button>`;
|
||||||
|
return `<tr>
|
||||||
|
<td style="white-space:nowrap"><span style="background:${typeColor};color:#000;border-radius:3px;padding:1px 5px;font-size:0.6rem">${it.action_type.toUpperCase()}</span></td>
|
||||||
|
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.from_name||it.from_email||'')}</td>
|
||||||
|
<td>${titleIn}</td>
|
||||||
|
<td>${sugDate}</td>
|
||||||
|
<td style="white-space:nowrap">${btnTask} ${btnAppt} ${btnDismiss}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
el.innerHTML = `<table><thead><tr><th>TYPE</th><th>FROM</th><th>TITLE</th><th>DATE</th><th>ACTIONS</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailMakeTask(id) {
|
||||||
|
const title = document.getElementById('eat-'+id)?.value || '';
|
||||||
|
const due = document.getElementById('ead-'+id)?.value || '';
|
||||||
|
apiPost('email_create_task',{id,title,due_date:due},()=>{ toast('Task created','ok'); loadEmailActionItems(); loadTasks(); });
|
||||||
|
}
|
||||||
|
function emailMakeAppt(id) {
|
||||||
|
const title = document.getElementById('eat-'+id)?.value || '';
|
||||||
|
const dateVal = document.getElementById('ead-'+id)?.value || '';
|
||||||
|
const start = dateVal ? dateVal + 'T09:00' : '';
|
||||||
|
apiPost('email_create_appt',{id,title,start_at:start},()=>{ toast('Appointment created','ok'); loadEmailActionItems(); loadAppts(); });
|
||||||
|
}
|
||||||
|
function emailDismiss(id) {
|
||||||
|
apiPost('email_dismiss',{id},()=>{ toast('Dismissed','ok'); loadEmailActionItems(); });
|
||||||
|
}
|
||||||
|
|
||||||
// ── PLANNER ─────────────────────────────────────────────────────────────────
|
// ── PLANNER ─────────────────────────────────────────────────────────────────
|
||||||
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user