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
+138 -15
View File
@@ -17,7 +17,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X } from "lucide-react";
import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X, Lock } from "lucide-react";
import { Task, Label } from '@shared/schema';
import { useQuery } from '@tanstack/react-query';
import { Checkbox } from "@/components/ui/checkbox";
@@ -43,9 +43,85 @@ interface TaskCardProps {
isDragging?: boolean;
}
import { ShareTaskModal } from './ShareTaskModal';
import { Share2, Users } from "lucide-react";
import { User } from "@shared/schema";
function SharedTaskIcon({ task }: { task: Task }) {
const { t } = useTranslation();
const { data: user } = useQuery<User>({ queryKey: ["/api/user"], retry: false });
const isOwner = user?.id === task.userId;
// If owner, fetch shared users
const { data: sharedUsers } = useQuery<any[]>({
queryKey: [`/api/tasks/${task.id}/shared-users`],
enabled: !!isOwner && !!task.id,
retry: false
});
if (!user) return null;
// Case 1: Owner and task is shared
if (isOwner && sharedUsers && sharedUsers.length > 0) {
const names = sharedUsers.map(u => u.username).join(", ");
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-indigo-100 dark:bg-indigo-900 cursor-help">
<Share2 className="w-3 h-3 text-indigo-600 dark:text-indigo-300" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('share.sharedWith')}: {names}</p>
</TooltipContent>
</Tooltip>
);
}
// Case 2: Not owner (Shared WITH me)
if (!isOwner && task.userId) {
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center justify-center w-5 h-5 rounded-full bg-orange-100 dark:bg-orange-900 cursor-help">
<Users className="w-3 h-3 text-orange-600 dark:text-orange-300" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('share.sharedWithMe') || "Shared with me"}</p>
</TooltipContent>
</Tooltip>
);
}
return null;
}
function SharedMenuItem({ task, onShare }: { task: Task, onShare: () => void }) {
const { t } = useTranslation();
const { data: user } = useQuery<User>({ queryKey: ["/api/user"], retry: false });
// Only owner can share
if (user?.id !== task.userId) return null;
return (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onShare();
}}
>
<Share2 className="w-4 h-4 mr-2" />
{t('share.taskTitle')}
</DropdownMenuItem>
);
}
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, onUpdate, isDragging }: TaskCardProps) {
const { t } = useTranslation();
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const handleAIMagic = async (e: React.MouseEvent) => {
e.stopPropagation();
@@ -66,6 +142,14 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
}
};
const { data: allTasks = [] } = useQuery<Task[]>({
queryKey: ['/api/tasks'],
staleTime: 5 * 60 * 1000,
});
const blockingTasks = task.dependencies?.map(dId => allTasks.find(t => t.id === dId)).filter(t => t && t.status !== 'done') || [];
const isBlocked = blockingTasks.length > 0;
// Fetch labels to get the label color
const { data: labels = [] } = useQuery<Label[]>({
queryKey: ['/api/labels'],
@@ -116,6 +200,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
const handleToggleComplete = (e: React.MouseEvent) => {
e.stopPropagation();
if (isBlocked) return;
if (task.status === 'done') {
onStatusChange?.('todo');
} else {
@@ -129,6 +214,10 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
const handleDragEnd = async (event: any, info: PanInfo) => {
if (info.offset.x > 100) {
if (isBlocked) {
controls.start({ x: 0 });
return;
}
// Swiped right -> Complete
triggerConfetti(0.5, 0.5);
playSuccessSound();
@@ -173,10 +262,17 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
onClick={handleToggleComplete}
data-testid={`checkbox-complete-${task.id}`}
className="mt-0.5"
disabled={isBlocked}
/>
</TooltipTrigger>
<TooltipContent>
<p>{t('taskCard.toggleComplete')}</p>
{isBlocked ? (
<p className="text-destructive font-medium">
Blocked by: {blockingTasks.map(t => t?.title).join(", ")}
</p>
) : (
<p>{t('taskCard.toggleComplete')}</p>
)}
</TooltipContent>
</Tooltip>
@@ -213,6 +309,9 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
{t(`status.${task.status}`)}
</Badge>
{/* Shared Status Icon */}
<SharedTaskIcon task={task} />
{task.dueDate && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="w-3 h-3" />
@@ -232,6 +331,20 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
{task.estimatedDuration}m
</Badge>
)}
{isBlocked && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="outline" className="text-xs border-destructive text-destructive gap-1 animate-pulse">
<Lock className="w-3 h-3" />
Blocked
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Waiting for: {blockingTasks.map(t => t?.title).join(", ")}</p>
</TooltipContent>
</Tooltip>
)}
</div>
{(task.timeTracked > 0 || task.isTracking) && (
@@ -308,21 +421,25 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
</DropdownMenuItem>
{/* Share Option - Check if owner */}
<SharedMenuItem task={task} onShare={() => setIsShareModalOpen(true)} />
<DropdownMenuSeparator />
{task.status !== 'done' && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onStatusChange?.('done');
console.log(`Mark task as done: ${task.title}`);
}}
data-testid={`menu-complete-${task.id}`}
>
<CheckCircle className="w-4 h-4 mr-2" />
{t('taskCard.markAsDone')}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
if (isBlocked) return;
onStatusChange?.('done');
console.log(`Mark task as done: ${task.title}`);
}}
disabled={isBlocked}
data-testid={`menu-complete-${task.id}`}
>
{isBlocked ? <Lock className="w-4 h-4 mr-2" /> : <CheckCircle className="w-4 h-4 mr-2" />}
{t('taskCard.markAsDone')}
</DropdownMenuItem>
{task.status === 'todo' && (
<DropdownMenuItem
@@ -482,6 +599,12 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
)}
</ContextMenuContent>
</ContextMenu>
<ShareTaskModal
taskId={task.id}
open={isShareModalOpen}
onOpenChange={setIsShareModalOpen}
/>
</motion.div>
);
}