mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: JARVIS Planner — tasks/appointments with voice intents, status bar badge, admin CRUD
This commit is contained in:
@@ -512,6 +512,155 @@ if (!$reply) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Tier 0.7: Planner — tasks & appointments ──────────────────────────────
|
||||
if (!$reply) {
|
||||
$lc = strtolower($message);
|
||||
|
||||
// ── Daily briefing / "what's my day" ─────────────────────────────────
|
||||
if (preg_match('/\b(briefing|daily summary|my day|schedule today|what.*today|morning|good morning)\b/i', $message)
|
||||
&& !preg_match('/\b(weather|news|temperature)\b/i', $message)) {
|
||||
$today = date('Y-m-d');
|
||||
$tasks_today = JarvisDB::query("SELECT title,priority FROM tasks WHERE due_date=? AND status NOT IN ('done','cancelled') ORDER BY FIELD(priority,'urgent','high','normal','low')", [$today]) ?? [];
|
||||
$tasks_overdue = JarvisDB::query("SELECT title FROM tasks WHERE due_date < ? AND status NOT IN ('done','cancelled')", [$today]) ?? [];
|
||||
$appts_today = JarvisDB::query("SELECT title,start_at FROM appointments WHERE DATE(start_at)=? ORDER BY start_at ASC", [$today]) ?? [];
|
||||
|
||||
$parts = [];
|
||||
if ($appts_today) {
|
||||
$ap = array_map(fn($a) => $a['title'] . ' at ' . date('g:i A', strtotime($a['start_at'])), $appts_today);
|
||||
$parts[] = count($appts_today) . ' appointment' . (count($appts_today)>1?'s':'') . ': ' . implode(', ', $ap);
|
||||
}
|
||||
if ($tasks_today) {
|
||||
$tl = array_map(fn($t) => $t['title'], $tasks_today);
|
||||
$parts[] = count($tasks_today) . ' task' . (count($tasks_today)>1?'s':'') . ' due: ' . implode(', ', $tl);
|
||||
}
|
||||
if ($tasks_overdue) {
|
||||
$parts[] = count($tasks_overdue) . ' overdue item' . (count($tasks_overdue)>1?'s':'');
|
||||
}
|
||||
if ($parts) {
|
||||
$reply = "Good morning, {$userAddr}. Today — " . implode('. ', $parts) . '.';
|
||||
} else {
|
||||
$reply = "Good morning, {$userAddr}. Your schedule is clear today — nothing due or scheduled.";
|
||||
}
|
||||
$source = 'planner:briefing';
|
||||
}
|
||||
|
||||
// ── Add task ──────────────────────────────────────────────────────────
|
||||
if (!$reply && preg_match('/\b(add task|remind me to|todo:|to do:|i need to|don.?t forget to|create task|new task)\b/i', $message)) {
|
||||
// Extract title — text after the trigger phrase
|
||||
$title = preg_replace('/^.*(add task|remind me to|todo:|to do:|i need to|don.?t forget to|create task|new task)\s*/i', '', $message);
|
||||
$title = trim($title, '. ');
|
||||
// Check for due date: "by tomorrow", "on Monday", "due Friday"
|
||||
$dueDate = null;
|
||||
if (preg_match('/\b(?:by|on|due|before)\s+(.+)$/i', $title, $dm)) {
|
||||
$ts = strtotime($dm[1]);
|
||||
if ($ts !== false) {
|
||||
$dueDate = date('Y-m-d', $ts);
|
||||
$title = trim(preg_replace('/\s+(?:by|on|due|before)\s+.+$/i', '', $title));
|
||||
}
|
||||
}
|
||||
// Detect category
|
||||
$category = preg_match('/\b(work|meeting|project|client|office)\b/i', $title) ? 'work' : 'personal';
|
||||
// Detect priority
|
||||
$priority = 'normal';
|
||||
if (preg_match('/\b(urgent|asap|emergency|critical)\b/i', $title)) $priority = 'urgent';
|
||||
elseif (preg_match('/\b(important|high priority)\b/i', $title)) $priority = 'high';
|
||||
if ($title) {
|
||||
JarvisDB::execute(
|
||||
'INSERT INTO tasks (title,category,priority,due_date) VALUES (?,?,?,?)',
|
||||
[$title, $category, $priority, $dueDate]
|
||||
);
|
||||
$duePart = $dueDate ? ', due ' . date('l, M j', strtotime($dueDate)) : '';
|
||||
$reply = "Task added: \"{$title}\"{$duePart}, {$userAddr}.";
|
||||
$source = 'planner:task_add';
|
||||
}
|
||||
}
|
||||
|
||||
// ── List tasks ────────────────────────────────────────────────────────
|
||||
if (!$reply && preg_match('/\b(my tasks|todo list|to.?do|pending tasks|what.*tasks|show.*tasks|task list)\b/i', $message)) {
|
||||
$today = date('Y-m-d');
|
||||
$rows = JarvisDB::query(
|
||||
"SELECT title,priority,due_date FROM tasks WHERE status NOT IN ('done','cancelled') ORDER BY FIELD(priority,'urgent','high','normal','low'), due_date ASC LIMIT 8"
|
||||
) ?? [];
|
||||
if (!$rows) {
|
||||
$reply = "Your task list is clear, {$userAddr}. Nothing pending.";
|
||||
} else {
|
||||
$items = array_map(function($r) {
|
||||
$due = $r['due_date'] ? ' (due ' . date('M j', strtotime($r['due_date'])) . ')' : '';
|
||||
return $r['title'] . $due;
|
||||
}, $rows);
|
||||
$reply = "You have " . count($rows) . " pending task" . (count($rows)>1?'s':'') . ", {$userAddr}: " . implode('; ', $items) . '.';
|
||||
}
|
||||
$source = 'planner:task_list';
|
||||
}
|
||||
|
||||
// ── Mark task done ────────────────────────────────────────────────────
|
||||
if (!$reply && preg_match('/\b(mark|complete|finished|done|completed)\b.*\btask\b|\btask\b.*\b(done|complete|finished)\b/i', $message)) {
|
||||
// extract keyword to search by
|
||||
$search = preg_replace('/\b(mark|complete|finished|done|completed|task|as|the)\b/i', ' ', $message);
|
||||
$search = '%' . trim(preg_replace('/\s+/', '%', trim($search))) . '%';
|
||||
$row = JarvisDB::single("SELECT id, title FROM tasks WHERE title LIKE ? AND status NOT IN ('done','cancelled') LIMIT 1", [$search]);
|
||||
if ($row) {
|
||||
JarvisDB::execute("UPDATE tasks SET status='done', completed_at=NOW() WHERE id=?", [$row['id']]);
|
||||
$reply = "Marked \"{$row['title']}\" as complete, {$userAddr}.";
|
||||
$source = 'planner:task_done';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add appointment ───────────────────────────────────────────────────
|
||||
if (!$reply && preg_match('/\b(schedule|appointment|meeting|add.*calendar|book|event)\b/i', $message)) {
|
||||
// Extract title and time: "schedule dentist Tuesday at 2pm"
|
||||
$raw = preg_replace('/^.*(schedule|appointment|meeting|add.*calendar|book|event)\s*/i', '', $message);
|
||||
$raw = trim($raw);
|
||||
// Try to find a datetime in the text
|
||||
$dtParsed = null;
|
||||
$title = $raw;
|
||||
// Look for time/date keywords
|
||||
if (preg_match('/\b(tomorrow|today|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next\s+\w+|jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b.*?(\d{1,2}(?::\d{2})?\s*(?:am|pm)?)?/i', $raw, $dm)) {
|
||||
$dtStr = $dm[0];
|
||||
$ts = strtotime($dtStr);
|
||||
if ($ts !== false && $ts > time()) {
|
||||
$dtParsed = date('Y-m-d H:i:s', $ts);
|
||||
$title = trim(str_replace($dtStr, '', $raw));
|
||||
}
|
||||
}
|
||||
if (!$dtParsed && preg_match('/\b(\d{1,2}(?::\d{2})?\s*(?:am|pm))\b/i', $raw, $tm)) {
|
||||
$ts = strtotime('today ' . $tm[1]);
|
||||
if ($ts !== false) { $dtParsed = date('Y-m-d H:i:s', $ts); }
|
||||
}
|
||||
$title = trim($title, ' .');
|
||||
if ($title && $dtParsed) {
|
||||
$category = preg_match('/\b(work|meeting|office|client|project)\b/i', $title) ? 'work' : 'personal';
|
||||
JarvisDB::execute(
|
||||
'INSERT INTO appointments (title,category,start_at) VALUES (?,?,?)',
|
||||
[$title, $category, $dtParsed]
|
||||
);
|
||||
$reply = "Appointment scheduled: \"{$title}\" on " . date('l, M j \a\t g:i A', strtotime($dtParsed)) . ", {$userAddr}.";
|
||||
$source = 'planner:appt_add';
|
||||
} elseif ($title && !$dtParsed) {
|
||||
$reply = "I'll add that appointment, {$userAddr} — what date and time?";
|
||||
$source = 'planner:appt_need_time';
|
||||
}
|
||||
}
|
||||
|
||||
// ── View appointments / calendar ──────────────────────────────────────
|
||||
if (!$reply && preg_match('/\b(appointments|calendar|my schedule|what.*scheduled|upcoming.*events?)\b/i', $message)) {
|
||||
$from = date('Y-m-d');
|
||||
$to = date('Y-m-d', strtotime('+7 days'));
|
||||
$rows = JarvisDB::query(
|
||||
"SELECT title, start_at FROM appointments WHERE DATE(start_at) BETWEEN ? AND ? ORDER BY start_at ASC LIMIT 6",
|
||||
[$from, $to]
|
||||
) ?? [];
|
||||
if (!$rows) {
|
||||
$reply = "No appointments in the next 7 days, {$userAddr}.";
|
||||
} else {
|
||||
$items = array_map(fn($r) => $r['title'] . ' — ' . date('D M j \a\t g:i A', strtotime($r['start_at'])), $rows);
|
||||
$reply = "Upcoming appointments, {$userAddr}: " . implode('; ', $items) . '.';
|
||||
}
|
||||
$source = 'planner:appt_list';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier 0.8: Weather & News intents ─────────────────────────────────────
|
||||
if (!$reply && preg_match('/\b(weather|forecast|temperature|temp|rain|snow|storm|outside|how.?s it (out|look)|what.?s it like outside)\b/i', $message)) {
|
||||
$wRow = JarvisDB::query("SELECT data FROM api_cache WHERE cache_key='weather' LIMIT 1");
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
// JARVIS Planner — tasks, appointments, daily briefing
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
function parseNaturalDate(string $text): ?string {
|
||||
$text = trim($text);
|
||||
if (!$text || strtolower($text) === 'none') return null;
|
||||
$ts = strtotime($text);
|
||||
return ($ts !== false && $ts > 0) ? date('Y-m-d', $ts) : null;
|
||||
}
|
||||
function parseNaturalDatetime(string $text): ?string {
|
||||
$text = trim($text);
|
||||
if (!$text) return null;
|
||||
$ts = strtotime($text);
|
||||
return ($ts !== false && $ts > 0) ? date('Y-m-d H:i:s', $ts) : null;
|
||||
}
|
||||
|
||||
// ── Route ─────────────────────────────────────────────────────────────────────
|
||||
// $action from api.php: tasks | appointments | today | done | summary
|
||||
|
||||
switch ($action) {
|
||||
|
||||
// ── Tasks ─────────────────────────────────────────────────────────────────
|
||||
case 'tasks':
|
||||
if ($method === 'GET') {
|
||||
$status = $_GET['status'] ?? '';
|
||||
$category = $_GET['category'] ?? '';
|
||||
$where = '1=1';
|
||||
$params = [];
|
||||
if ($status) { $where .= ' AND status=?'; $params[] = $status; }
|
||||
if ($category) { $where .= ' AND category=?'; $params[] = $category; }
|
||||
else { $where .= " AND status NOT IN ('done','cancelled')"; }
|
||||
$rows = JarvisDB::query(
|
||||
"SELECT * FROM tasks WHERE {$where} ORDER BY
|
||||
FIELD(priority,'urgent','high','normal','low'), due_date ASC, created_at DESC",
|
||||
$params
|
||||
) ?? [];
|
||||
echo json_encode(['tasks' => $rows]);
|
||||
} elseif ($method === 'POST') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$title = trim($data['title'] ?? '');
|
||||
$notes = trim($data['notes'] ?? '');
|
||||
$category = $data['category'] ?? 'personal';
|
||||
$priority = $data['priority'] ?? 'normal';
|
||||
$status = $data['status'] ?? 'pending';
|
||||
$due_date = parseNaturalDate($data['due_date'] ?? '');
|
||||
$due_time = !empty($data['due_time']) ? $data['due_time'] : null;
|
||||
if (!$title) { echo json_encode(['error' => 'Title required']); exit; }
|
||||
if ($id) {
|
||||
JarvisDB::execute(
|
||||
'UPDATE tasks SET title=?,notes=?,category=?,priority=?,status=?,due_date=?,due_time=?,updated_at=NOW() WHERE id=?',
|
||||
[$title,$notes,$category,$priority,$status,$due_date,$due_time,$id]
|
||||
);
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
} else {
|
||||
$newId = JarvisDB::insert(
|
||||
'INSERT INTO tasks (title,notes,category,priority,due_date,due_time) VALUES (?,?,?,?,?,?)',
|
||||
[$title,$notes,$category,$priority,$due_date,$due_time]
|
||||
);
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
}
|
||||
} elseif ($method === 'DELETE') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id) { JarvisDB::execute('DELETE FROM tasks WHERE id=?', [$id]); }
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── Mark task done ────────────────────────────────────────────────────────
|
||||
case 'done':
|
||||
$id = (int)($data['id'] ?? $_GET['id'] ?? 0);
|
||||
if ($id) {
|
||||
JarvisDB::execute(
|
||||
"UPDATE tasks SET status='done', completed_at=NOW() WHERE id=?", [$id]
|
||||
);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
// ── Appointments ──────────────────────────────────────────────────────────
|
||||
case 'appointments':
|
||||
if ($method === 'GET') {
|
||||
$from = $_GET['from'] ?? date('Y-m-d');
|
||||
$to = $_GET['to'] ?? date('Y-m-d', strtotime('+90 days'));
|
||||
$rows = JarvisDB::query(
|
||||
"SELECT * FROM appointments WHERE DATE(start_at) BETWEEN ? AND ? ORDER BY start_at ASC",
|
||||
[$from, $to]
|
||||
) ?? [];
|
||||
echo json_encode(['appointments' => $rows]);
|
||||
} elseif ($method === 'POST') {
|
||||
$id = (int)($data['id'] ?? 0);
|
||||
$title = trim($data['title'] ?? '');
|
||||
$description = trim($data['description'] ?? '');
|
||||
$category = $data['category'] ?? 'personal';
|
||||
$location = trim($data['location'] ?? '');
|
||||
$all_day = (int)($data['all_day'] ?? 0);
|
||||
$reminder = (int)($data['reminder_min'] ?? 30);
|
||||
$start_raw = trim($data['start_at'] ?? '');
|
||||
$end_raw = trim($data['end_at'] ?? '');
|
||||
if (!$title || !$start_raw) { echo json_encode(['error' => 'Title and start time required']); exit; }
|
||||
$start_at = parseNaturalDatetime($start_raw) ?? date('Y-m-d H:i:s', strtotime($start_raw));
|
||||
$end_at = $end_raw ? (parseNaturalDatetime($end_raw) ?? null) : null;
|
||||
if ($id) {
|
||||
JarvisDB::execute(
|
||||
'UPDATE appointments SET title=?,description=?,category=?,start_at=?,end_at=?,location=?,all_day=?,reminder_min=?,alerted=0,updated_at=NOW() WHERE id=?',
|
||||
[$title,$description,$category,$start_at,$end_at,$location,$all_day,$reminder,$id]
|
||||
);
|
||||
echo json_encode(['success' => true, 'id' => $id]);
|
||||
} else {
|
||||
$newId = JarvisDB::insert(
|
||||
'INSERT INTO appointments (title,description,category,start_at,end_at,location,all_day,reminder_min) VALUES (?,?,?,?,?,?,?,?)',
|
||||
[$title,$description,$category,$start_at,$end_at,$location,$all_day,$reminder]
|
||||
);
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
}
|
||||
} elseif ($method === 'DELETE') {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id) { JarvisDB::execute('DELETE FROM appointments WHERE id=?', [$id]); }
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
break;
|
||||
|
||||
// ── Today / briefing summary ──────────────────────────────────────────────
|
||||
case 'today':
|
||||
default:
|
||||
$today = date('Y-m-d');
|
||||
$tomorrow = date('Y-m-d', strtotime('+1 day'));
|
||||
|
||||
$tasks_today = JarvisDB::query(
|
||||
"SELECT * FROM tasks WHERE due_date=? AND status NOT IN ('done','cancelled') ORDER BY FIELD(priority,'urgent','high','normal','low')",
|
||||
[$today]
|
||||
) ?? [];
|
||||
$tasks_overdue = JarvisDB::query(
|
||||
"SELECT * FROM tasks WHERE due_date < ? AND status NOT IN ('done','cancelled') ORDER BY due_date ASC",
|
||||
[$today]
|
||||
) ?? [];
|
||||
$tasks_pending = JarvisDB::query(
|
||||
"SELECT COUNT(*) cnt FROM tasks WHERE status='pending' AND (due_date IS NULL OR due_date >= ?)",
|
||||
[$today]
|
||||
);
|
||||
$appts_today = JarvisDB::query(
|
||||
"SELECT * FROM appointments WHERE DATE(start_at)=? ORDER BY start_at ASC",
|
||||
[$today]
|
||||
) ?? [];
|
||||
$appts_upcoming = JarvisDB::query(
|
||||
"SELECT * FROM appointments WHERE start_at > NOW() ORDER BY start_at ASC LIMIT 5",
|
||||
[]
|
||||
) ?? [];
|
||||
|
||||
echo json_encode([
|
||||
'date' => $today,
|
||||
'tasks_today' => $tasks_today,
|
||||
'tasks_overdue' => $tasks_overdue,
|
||||
'pending_count' => (int)($tasks_pending[0]['cnt'] ?? 0),
|
||||
'appts_today' => $appts_today,
|
||||
'appts_upcoming' => $appts_upcoming,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
Reference in New Issue
Block a user