- DB: tasks table with scheduled_datetime, reminder_minutes_before, advance_notified, status - API: CRUD routes GET/POST /api/tasks, PATCH/DELETE /api/tasks/<id> - Scheduler: check_task_reminders() fires advance + at-time notifications, tracks advance_notified to prevent double-fire - Bot: handle_task() with add/list/done/cancel/delete actions + datetime resolution helper - AI: task interaction type + examples added to command_parser - Web: task list page with overdue/notified color coding + new task form with datetime-local picker - Nav: replaced Templates with Tasks in bottom nav Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
151 lines
6.1 KiB
TypeScript
151 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import api from '@/lib/api';
|
|
import { ArrowLeftIcon } from '@/components/ui/Icons';
|
|
|
|
const REMINDER_OPTIONS = [
|
|
{ label: 'No reminder', value: 0 },
|
|
{ label: '5 minutes before', value: 5 },
|
|
{ label: '10 minutes before', value: 10 },
|
|
{ label: '15 minutes before', value: 15 },
|
|
{ label: '30 minutes before', value: 30 },
|
|
{ label: '1 hour before', value: 60 },
|
|
];
|
|
|
|
function localDatetimeDefault(): string {
|
|
const now = new Date();
|
|
now.setMinutes(now.getMinutes() + 60, 0, 0);
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
}
|
|
|
|
export default function NewTaskPage() {
|
|
const router = useRouter();
|
|
const [title, setTitle] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [scheduledDatetime, setScheduledDatetime] = useState(localDatetimeDefault);
|
|
const [reminderMinutes, setReminderMinutes] = useState(15);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!title.trim()) {
|
|
setError('Title is required.');
|
|
return;
|
|
}
|
|
if (!scheduledDatetime) {
|
|
setError('Date and time are required.');
|
|
return;
|
|
}
|
|
setIsSubmitting(true);
|
|
setError('');
|
|
try {
|
|
await api.tasks.create({
|
|
title: title.trim(),
|
|
description: description.trim() || undefined,
|
|
scheduled_datetime: scheduledDatetime,
|
|
reminder_minutes_before: reminderMinutes,
|
|
});
|
|
router.push('/dashboard/tasks');
|
|
} catch (err) {
|
|
console.error('Failed to create task:', err);
|
|
setError('Failed to create task. Please try again.');
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
|
|
<div className="flex items-center gap-3 px-4 py-3">
|
|
<Link href="/dashboard/tasks" className="p-1 -ml-1 text-gray-500 dark:text-gray-400">
|
|
<ArrowLeftIcon size={20} />
|
|
</Link>
|
|
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">New Task</h1>
|
|
</div>
|
|
</header>
|
|
|
|
<form onSubmit={handleSubmit} className="p-4 space-y-5 max-w-lg mx-auto">
|
|
{error && (
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-sm text-red-600 dark:text-red-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Title <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={e => setTitle(e.target.value)}
|
|
placeholder="e.g. Doctor appointment"
|
|
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Description <span className="text-gray-400 font-normal">(optional)</span>
|
|
</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
placeholder="Add details..."
|
|
rows={2}
|
|
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm resize-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Date & Time <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={scheduledDatetime}
|
|
onChange={e => setScheduledDatetime(e.target.value)}
|
|
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Advance Reminder
|
|
</label>
|
|
<select
|
|
value={reminderMinutes}
|
|
onChange={e => setReminderMinutes(Number(e.target.value))}
|
|
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-sm"
|
|
>
|
|
{REMINDER_OPTIONS.map(opt => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
You'll also get a notification exactly at the scheduled time.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:opacity-60 text-white font-semibold py-3 rounded-xl transition-colors"
|
|
>
|
|
{isSubmitting ? 'Saving…' : 'Add Task'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|