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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user