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