Add one-off tasks/appointments feature
- 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>
This commit is contained in:
150
synculous-client/src/app/dashboard/tasks/new/page.tsx
Normal file
150
synculous-client/src/app/dashboard/tasks/new/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user