feat: Implement AI Chat Agent, Email Notifications, and UI enhancements
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -7,7 +7,10 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { CalendarIcon, Plus, Tag } from 'lucide-react';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CalendarIcon, Plus, Tag, Check, Link2 } from 'lucide-react';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { parseTaskInput } from '../lib/nlp';
|
||||
@@ -27,7 +30,10 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
const [energyLevel, setEnergyLevel] = useState<'low' | 'medium' | 'high'>('medium');
|
||||
const [estimatedDuration, setEstimatedDuration] = useState<number | undefined>();
|
||||
const [dueDate, setDueDate] = useState<Date | undefined>();
|
||||
|
||||
const [labelId, setLabelId] = useState<string | undefined>();
|
||||
const [dependencies, setDependencies] = useState<string[]>([]);
|
||||
const [isDependenciesOpen, setIsDependenciesOpen] = useState(false);
|
||||
|
||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -37,6 +43,10 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
queryKey: ['/api/labels'],
|
||||
});
|
||||
|
||||
const { data: tasks = [] } = useQuery<Task[]>({
|
||||
queryKey: ['/api/tasks'],
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
if (!title.trim()) {
|
||||
setError(t('taskCreation.titleRequired') || 'Title is required');
|
||||
@@ -51,6 +61,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
estimatedDuration,
|
||||
dueDate,
|
||||
labelId,
|
||||
dependencies,
|
||||
status: 'todo',
|
||||
timeTracked: 0,
|
||||
isTracking: false
|
||||
@@ -64,7 +75,9 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
setDescription('');
|
||||
setPriority('medium');
|
||||
setDueDate(undefined);
|
||||
setDueDate(undefined);
|
||||
setLabelId(undefined);
|
||||
setDependencies([]);
|
||||
setError(null);
|
||||
onClose();
|
||||
};
|
||||
@@ -81,14 +94,16 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
setDescription('');
|
||||
setPriority('medium');
|
||||
setDueDate(undefined);
|
||||
setDueDate(undefined);
|
||||
setLabelId(undefined);
|
||||
setDependencies([]);
|
||||
setError(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-md mx-4">
|
||||
<Dialog open={isOpen} onOpenChange={handleClose} >
|
||||
<DialogContent className="sm:max-w-md mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
@@ -99,7 +114,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('taskCreation.titlePlaceholder')}
|
||||
placeholder={t('smartTask.placeholder')}
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
@@ -118,10 +133,12 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
{title.includes('!') || title.includes('#') || title.toLowerCase().includes('tomorrow') ? (
|
||||
{title.includes('!') || title.includes('#') ||
|
||||
title.toLowerCase().includes('tomorrow') || title.toLowerCase().includes('morgen') ||
|
||||
title.toLowerCase().includes('today') || title.toLowerCase().includes('heute') ? (
|
||||
<div className="bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-300 text-xs px-2 py-0.5 rounded-full flex items-center gap-1 animate-pulse">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
Smart Input Active
|
||||
{t('taskCreation.smartInputActive')}
|
||||
</div>
|
||||
) : null}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
@@ -139,108 +156,185 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex-1">
|
||||
<Select value={priority} onValueChange={(value: 'low' | 'medium' | 'high') => setPriority(value)}>
|
||||
<SelectTrigger data-testid="select-task-priority">
|
||||
<SelectValue placeholder={t('taskCreation.priority')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">{t('priority.low')}</SelectItem>
|
||||
<SelectItem value="medium">{t('priority.medium')}</SelectItem>
|
||||
<SelectItem value="high">{t('priority.high')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Select value={priority} onValueChange={(value: 'low' | 'medium' | 'high') => setPriority(value)}>
|
||||
<SelectTrigger data-testid="select-task-priority">
|
||||
<SelectValue placeholder={t('taskCreation.priority')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">{t('priority.low')}</SelectItem>
|
||||
<SelectItem value="medium">{t('priority.medium')}</SelectItem>
|
||||
<SelectItem value="high">{t('priority.high')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex-1">
|
||||
<Select value={energyLevel} onValueChange={(value: 'low' | 'medium' | 'high') => setEnergyLevel(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Energy" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">⚡ Low Energy</SelectItem>
|
||||
<SelectItem value="medium">⚡⚡ Medium Energy</SelectItem>
|
||||
<SelectItem value="high">⚡⚡⚡ High Energy</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select value={energyLevel} onValueChange={(value: 'low' | 'medium' | 'high') => setEnergyLevel(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('taskCreation.energy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">{t('gamification.energy.low')}</SelectItem>
|
||||
<SelectItem value="medium">{t('gamification.energy.medium')}</SelectItem>
|
||||
<SelectItem value="high">{t('gamification.energy.high')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="w-24">
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
placeholder={t('taskCreation.minutesPlaceholder')}
|
||||
value={estimatedDuration || ''}
|
||||
onChange={(e) => setEstimatedDuration(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Select value={labelId || 'none'} onValueChange={(value) => setLabelId(value === 'none' ? undefined : value)}>
|
||||
<SelectTrigger data-testid="select-task-label">
|
||||
<SelectValue placeholder={t('taskCreation.label')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('taskCreation.noLabel')}</SelectItem>
|
||||
{labels.map((label) => (
|
||||
<SelectItem key={label.id} value={label.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded"
|
||||
style={{ backgroundColor: label.color }}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select value={labelId || 'none'} onValueChange={(value) => setLabelId(value === 'none' ? undefined : value)}>
|
||||
<SelectTrigger data-testid="select-task-label">
|
||||
<SelectValue placeholder={t('taskCreation.label')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('taskCreation.noLabel')}</SelectItem>
|
||||
{labels.map((label) => (
|
||||
<SelectItem key={label.id} value={label.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded"
|
||||
style={{ backgroundColor: label.color }}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
||||
<div className="col-span-2">
|
||||
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left font-normal"
|
||||
data-testid="button-due-date"
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dueDate ? dueDate.toLocaleDateString() : t('taskCreation.dueDate')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dueDate}
|
||||
onSelect={(date) => {
|
||||
setDueDate(date);
|
||||
setIsCalendarOpen(false);
|
||||
}}
|
||||
disabled={(date) => date < new Date()}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dependencies Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium flex items-center gap-2">
|
||||
<Link2 className="w-4 h-4" />
|
||||
{t('dependencies.label')}
|
||||
</label>
|
||||
<Popover open={isDependenciesOpen} onOpenChange={setIsDependenciesOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2"
|
||||
data-testid="button-due-date"
|
||||
role="combobox"
|
||||
className="w-full justify-between h-auto min-h-[40px]"
|
||||
>
|
||||
<CalendarIcon className="w-4 h-4" />
|
||||
{dueDate ? dueDate.toLocaleDateString() : t('taskCreation.dueDate')}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{dependencies.length > 0 ? (
|
||||
dependencies.map((depId) => {
|
||||
const task = tasks.find((t) => t.id === depId);
|
||||
return (
|
||||
<Badge key={depId} variant="secondary" className="mr-1">
|
||||
{task?.title || 'Unknown Task'}
|
||||
<div
|
||||
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDependencies(dependencies.filter((id) => id !== depId));
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3 rotate-45 hover:text-destructive" />
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<span className="text-muted-foreground">{t('dependencies.selectPlaceholder')}</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="end">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dueDate}
|
||||
onSelect={(date) => {
|
||||
setDueDate(date);
|
||||
setIsCalendarOpen(false);
|
||||
}}
|
||||
disabled={(date) => date < new Date()}
|
||||
initialFocus
|
||||
/>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search tasks..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No task found.</CommandEmpty>
|
||||
<CommandGroup heading="Available Tasks">
|
||||
{tasks
|
||||
.filter(t => t.status !== 'done') // Only active tasks
|
||||
.map((task) => (
|
||||
<CommandItem
|
||||
key={task.id}
|
||||
value={task.title}
|
||||
onSelect={() => {
|
||||
if (dependencies.includes(task.id)) {
|
||||
setDependencies(dependencies.filter((id) => id !== task.id));
|
||||
} else {
|
||||
setDependencies([...dependencies, task.id]);
|
||||
}
|
||||
// Keep the popover open for multiple selection
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
dependencies.includes(task.id)
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible"
|
||||
)}>
|
||||
<Check className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
<span>{task.title}</span>
|
||||
<Badge variant="outline" className="ml-auto text-[10px] h-4 px-1 py-0 capitalize">
|
||||
{task.status.replace('_', ' ')}
|
||||
</Badge>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
className="flex-1"
|
||||
data-testid="button-cancel"
|
||||
>
|
||||
{t('taskCreation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="flex-1"
|
||||
data-testid="button-save-task"
|
||||
>
|
||||
{t('taskCreation.create')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
className="flex-1"
|
||||
data-testid="button-cancel"
|
||||
>
|
||||
{t('taskCreation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="flex-1"
|
||||
data-testid="button-save-task"
|
||||
>
|
||||
{t('taskCreation.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user