// Natural-language quick add, Todoist style: // "Pay rent tomorrow p1 @home #Sydney ~30m monthly" // → title "Pay rent", due tomorrow, priority 1, context @home, // project "Sydney" (matched by name prefix), estimate 30 min, repeats monthly. // // The add form parses the title field live and mirrors every recognized token // into a visible form control, so a false positive (e.g. "fri" in a name) is // seen and fixable, never silent. import type { Project, Repeat, Task } from "./types"; export interface ParsedQuickAdd { title: string; priority?: Task["priority"]; due_date?: string; context?: string; project_id?: string; estimate_min?: number; repeat?: Repeat; } const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; const REPEAT_WORDS: Record = { daily: "daily", weekdays: "weekdays", weekly: "weekly", monthly: "monthly", yearly: "yearly", }; function iso(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function parseDateWord(word: string): string | undefined { const w = word.toLowerCase(); const today = new Date(); if (w === "today" || w === "tod") return iso(today); // No "tom" alias — it's a name far more often than a date. if (w === "tomorrow" || w === "tmr") { const d = new Date(today); d.setDate(d.getDate() + 1); return iso(d); } if (/^\d{4}-\d{2}-\d{2}$/.test(w)) return w; const wd = WEEKDAYS.findIndex((n) => n === w || n.slice(0, 3) === w); if (wd >= 0) { const d = new Date(today); const delta = (wd - d.getDay() + 7) % 7 || 7; // always the *next* occurrence d.setDate(d.getDate() + delta); return iso(d); } return undefined; } function parseEstimate(token: string): number | undefined { const m = token.match(/^~(?:(\d+)h)?(\d+)?m?$/i); if (!m || (!m[1] && !m[2])) return undefined; return (Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) || undefined; } export function parseQuickAdd(input: string, projects: Project[]): ParsedQuickAdd { const out: ParsedQuickAdd = { title: "" }; const rest: string[] = []; for (const token of input.trim().split(/\s+/)) { const p = token.match(/^p([1-4])$/i); if (p && out.priority === undefined) { out.priority = Number(p[1]) as Task["priority"]; continue; } if (token.startsWith("@") && token.length > 1 && out.context === undefined) { out.context = token; continue; } if (token.startsWith("#") && token.length > 1 && out.project_id === undefined) { const name = token.slice(1).toLowerCase(); const match = projects.find( (pr) => pr.status === "active" && pr.name.toLowerCase().startsWith(name), ); if (match) { out.project_id = match.id; continue; } } const est = parseEstimate(token); if (est && out.estimate_min === undefined) { out.estimate_min = est; continue; } const rep = REPEAT_WORDS[token.toLowerCase()]; if (rep && out.repeat === undefined) { out.repeat = rep; continue; } if (out.due_date === undefined) { const due = parseDateWord(token); if (due) { out.due_date = due; continue; } } rest.push(token); } out.title = rest.join(" "); return out; }