feat: Implement AI Chat Agent, Email Notifications, and UI enhancements
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-12 08:35:48 +01:00
parent ccfb674318
commit 5d8976b1cd
58 changed files with 7867 additions and 844 deletions
+123 -31
View File
@@ -8,7 +8,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar, Tag } from 'lucide-react';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from "@/lib/utils";
import { useQuery } from '@tanstack/react-query';
import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar, Tag, Link2, Check } from 'lucide-react';
import { Task, Label } from '@shared/schema';
import { useTranslation } from 'react-i18next';
@@ -27,15 +31,15 @@ interface TaskDetailsModalProps {
labels?: Label[];
}
export default function TaskDetailsModal({
isOpen,
onClose,
task,
export default function TaskDetailsModal({
isOpen,
onClose,
task,
onSave,
labels = []
}: TaskDetailsModalProps) {
const { t } = useTranslation();
// Edit form state
const [editedTitle, setEditedTitle] = useState('');
const [editedDescription, setEditedDescription] = useState('');
@@ -43,11 +47,17 @@ export default function TaskDetailsModal({
const [editedPriority, setEditedPriority] = useState<'low' | 'medium' | 'high'>('medium');
const [editedLabelId, setEditedLabelId] = useState<string | null>(null);
const [editedDueDate, setEditedDueDate] = useState('');
const [editedDependencies, setEditedDependencies] = useState<string[]>([]);
const [isDependenciesOpen, setIsDependenciesOpen] = useState(false);
const { data: tasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
});
// Notes state
const [notes, setNotes] = useState('');
const [isEditingNotes, setIsEditingNotes] = useState(false);
// Time reporting state
const [isAddingTime, setIsAddingTime] = useState(false);
const [hours, setHours] = useState(0);
@@ -55,7 +65,7 @@ export default function TaskDetailsModal({
const [manualHours, setManualHours] = useState('');
const [timeDescription, setTimeDescription] = useState('');
const [activeTimeTab, setActiveTimeTab] = useState('clock');
// Time entries state - simulate from timeTracked for display
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]);
@@ -68,7 +78,8 @@ export default function TaskDetailsModal({
setEditedPriority(task.priority as 'low' | 'medium' | 'high');
setEditedLabelId(task.labelId || null);
setEditedDueDate(task.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : '');
setEditedDependencies(task.dependencies || []);
setNotes(task.notes || '');
// Create a single time entry from timeTracked for display purposes
if (task.timeTracked > 0) {
@@ -105,22 +116,23 @@ export default function TaskDetailsModal({
const handleSaveTaskEdit = () => {
if (!task) return;
const updatedTask: Task = {
const updatedTask: Task = {
...task,
title: editedTitle,
description: editedDescription || null,
status: editedStatus,
priority: editedPriority,
labelId: editedLabelId,
dueDate: editedDueDate ? new Date(editedDueDate) : null
dueDate: editedDueDate ? new Date(editedDueDate) : null,
dependencies: editedDependencies
};
onSave(updatedTask);
};
const handleSaveNotes = () => {
if (!task) return;
const updatedTask = { ...task, notes };
onSave(updatedTask);
setIsEditingNotes(false);
@@ -130,7 +142,7 @@ export default function TaskDetailsModal({
if (!task) return;
let totalMinutes = 0;
if (activeTimeTab === 'clock') {
totalMinutes = hours * 60 + minutes;
} else {
@@ -149,22 +161,22 @@ export default function TaskDetailsModal({
const updatedTimeEntries = [...timeEntries, newTimeEntry];
const updatedTimeTracked = (task.timeTracked || 0) + totalMinutes;
const updatedTask = {
...task,
const updatedTask = {
...task,
timeTracked: updatedTimeTracked
};
setTimeEntries(updatedTimeEntries);
onSave(updatedTask);
// Reset time input
setHours(0);
setMinutes(0);
setManualHours('');
setTimeDescription('');
setIsAddingTime(false);
console.log(`Time entry saved: ${totalMinutes} minutes for task: ${task.title}`);
};
@@ -173,18 +185,18 @@ export default function TaskDetailsModal({
// Don't allow deleting the main tracked time entry
return;
}
const entryToDelete = timeEntries.find(entry => entry.id === entryId);
if (!entryToDelete) return;
const updatedTimeEntries = timeEntries.filter(entry => entry.id !== entryId);
const updatedTimeTracked = Math.max(0, (task.timeTracked || 0) - entryToDelete.timeSpent);
const updatedTask = {
...task,
const updatedTask = {
...task,
timeTracked: updatedTimeTracked
};
setTimeEntries(updatedTimeEntries);
onSave(updatedTask);
};
@@ -251,13 +263,13 @@ export default function TaskDetailsModal({
<h3 className="font-semibold text-lg" data-testid="text-task-details-title">
{task.title}
</h3>
{task.description && (
<p className="text-sm text-muted-foreground" data-testid="text-task-details-description">
{task.description}
</p>
)}
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className={`text-xs ${getPriorityColor(task.priority)}`}>
{t(`taskDetails.priorityValue.${task.priority}`)}
@@ -384,6 +396,86 @@ export default function TaskDetailsModal({
/>
</div>
{/* Dependencies */}
<div>
<label className="text-sm font-medium flex items-center gap-2 mb-2">
<Link2 className="w-4 h-4" />
Blocked By
</label>
<Popover open={isDependenciesOpen} onOpenChange={setIsDependenciesOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="w-full justify-between h-auto min-h-[40px]"
>
<div className="flex flex-wrap gap-1">
{editedDependencies.length > 0 ? (
editedDependencies.map((depId) => {
const depTask = tasks.find((t) => t.id === depId);
return (
<Badge key={depId} variant="secondary" className="mr-1">
{depTask?.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();
setEditedDependencies(editedDependencies.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-[400px] p-0" align="start">
<Command>
<CommandInput placeholder="Search tasks..." />
<CommandList>
<CommandEmpty>No task found.</CommandEmpty>
<CommandGroup heading="Available Tasks">
{tasks
.filter(t => t.id !== task.id && t.status !== 'done') // Cannot depend on self or done tasks (optional logic)
.map((tItem) => (
<CommandItem
key={tItem.id}
value={tItem.title}
onSelect={() => {
if (editedDependencies.includes(tItem.id)) {
setEditedDependencies(editedDependencies.filter((id) => id !== tItem.id));
} else {
setEditedDependencies([...editedDependencies, tItem.id]);
}
}}
>
<div className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
editedDependencies.includes(tItem.id)
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}>
<Check className={cn("h-4 w-4")} />
</div>
<span>{tItem.title}</span>
<Badge variant="outline" className="ml-auto text-[10px] h-4 px-1 py-0 capitalize">
{tItem.status.replace('_', ' ')}
</Badge>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* Save Button */}
<div className="flex gap-2 pt-2">
<Button
@@ -522,7 +614,7 @@ export default function TaskDetailsModal({
{formatTime(hours, minutes)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Hours Picker */}
<div className="space-y-2">
@@ -542,7 +634,7 @@ export default function TaskDetailsModal({
))}
</div>
</div>
{/* Minutes Picker */}
<div className="space-y-2">
<label className="text-sm font-medium text-center block">Minutes</label>
@@ -572,7 +664,7 @@ export default function TaskDetailsModal({
{getCurrentTimeDisplay()}
</div>
</div>
<div>
<label className="text-sm font-medium block mb-2">Hours (decimal)</label>
<Input