🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Clock } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface AcceptTaskButtonProps {
|
||||
taskId: string;
|
||||
onAccept: (taskId: string) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function AcceptTaskButton({ taskId, onAccept, isLoading }: AcceptTaskButtonProps) {
|
||||
const [accepting, setAccepting] = useState(false);
|
||||
|
||||
const handleAccept = async () => {
|
||||
try {
|
||||
setAccepting(true);
|
||||
await onAccept(taskId);
|
||||
toast.success('Tarefa aceita com sucesso!');
|
||||
} catch (error) {
|
||||
console.error('Erro ao aceitar tarefa:', error);
|
||||
toast.error('Erro ao aceitar tarefa');
|
||||
} finally {
|
||||
setAccepting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleAccept}
|
||||
disabled={accepting || isLoading}
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{accepting ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 mr-2 animate-spin" />
|
||||
Aceitando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Aceitar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface DeleteTaskDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
taskTitle: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function DeleteTaskDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
taskTitle,
|
||||
isLoading
|
||||
}: DeleteTaskDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="bg-slate-800 border-slate-700">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-white">Confirmar Exclusão</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-slate-400">
|
||||
Tem certeza que deseja excluir a tarefa "{taskTitle}"?
|
||||
Esta ação não pode ser desfeita.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
className="bg-slate-700 border-slate-600 text-white hover:bg-slate-600"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancelar
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
disabled={isLoading}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
{isLoading ? 'Excluindo...' : 'Excluir'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { Clock, MessageCircle, Paperclip, Edit, Trash2, MoreHorizontal, CheckSquare, Play, Eye, Archive } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { UserAvatar } from '@/components/ui/user-avatar';
|
||||
import { DeleteTaskDialog } from './DeleteTaskDialog';
|
||||
import { useTaskActions } from '@/hooks/useTaskActions';
|
||||
import { getTaskBorderColor, getStatusBadgeColor } from '@/utils/taskStatusColors';
|
||||
import { Task } from '@/types/tasks';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
onEdit: (task: Task) => void;
|
||||
onComplete: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onClick: (task: Task) => void;
|
||||
onView: (task: Task) => void;
|
||||
onArchive?: (id: string) => void;
|
||||
isCompleted?: boolean;
|
||||
showCreator?: boolean;
|
||||
canArchive?: boolean;
|
||||
}
|
||||
|
||||
const statusLabels = {
|
||||
a_fazer: 'A Fazer',
|
||||
em_andamento: 'Em Andamento',
|
||||
revisao: 'Revisão',
|
||||
pendente: 'Pendente',
|
||||
bloqueado: 'Bloqueado',
|
||||
concluido: 'Concluído'
|
||||
};
|
||||
|
||||
const priorityLabels = {
|
||||
baixa: 'Baixa',
|
||||
media: 'Média',
|
||||
alta: 'Alta',
|
||||
urgente: 'Urgente'
|
||||
};
|
||||
|
||||
const getPriorityBadgeColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgente':
|
||||
return 'bg-red-500 text-white border-red-500';
|
||||
case 'alta':
|
||||
return 'bg-orange-500 text-white border-orange-500';
|
||||
case 'media':
|
||||
return 'bg-yellow-500 text-white border-yellow-500';
|
||||
case 'baixa':
|
||||
return 'bg-blue-400 text-white border-blue-400';
|
||||
default:
|
||||
return 'bg-gray-500 text-white border-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBorderColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgente':
|
||||
return 'border-l-red-500';
|
||||
case 'alta':
|
||||
return 'border-l-orange-500';
|
||||
case 'media':
|
||||
return 'border-l-yellow-500';
|
||||
case 'baixa':
|
||||
return 'border-l-blue-400';
|
||||
default:
|
||||
return 'border-l-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getTaskRefColor = (task: Task): string => {
|
||||
// Verde para tarefas concluídas
|
||||
if (task.is_completed || task.status === 'concluido') {
|
||||
return 'text-green-500';
|
||||
}
|
||||
|
||||
// Lilás claro para tarefas aguardando aceite
|
||||
if (task.status === 'a_fazer') {
|
||||
return 'text-purple-300';
|
||||
}
|
||||
|
||||
// Se não tem prazo, usar azul claro
|
||||
if (!task.due_date) {
|
||||
return 'text-blue-300';
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const dueDate = new Date(task.due_date);
|
||||
const daysDiff = Math.ceil((dueDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Vermelho para tarefas vencidas
|
||||
if (daysDiff < 0) {
|
||||
return 'text-red-500';
|
||||
}
|
||||
|
||||
// Laranja para tarefas com menos de 2 dias
|
||||
if (daysDiff < 2) {
|
||||
return 'text-orange-500';
|
||||
}
|
||||
|
||||
// Amarelo para tarefas entre 2 e 5 dias
|
||||
if (daysDiff <= 5) {
|
||||
return 'text-yellow-500';
|
||||
}
|
||||
|
||||
// Azul claro para tarefas com mais de 5 dias
|
||||
return 'text-blue-300';
|
||||
};
|
||||
|
||||
// Fundo suave no modo claro de acordo com a prioridade (cor da borda)
|
||||
const getPriorityBgTintLight = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgente':
|
||||
return 'bg-red-50';
|
||||
case 'alta':
|
||||
return 'bg-orange-50';
|
||||
case 'media':
|
||||
return 'bg-yellow-50';
|
||||
case 'baixa':
|
||||
return 'bg-blue-50';
|
||||
default:
|
||||
return 'bg-slate-50';
|
||||
}
|
||||
};
|
||||
|
||||
// Borda fina do card (em todo o contorno) para claro e escuro
|
||||
const getPriorityFullBorderColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgente':
|
||||
return 'border-red-500 dark:border-red-400/60';
|
||||
case 'alta':
|
||||
return 'border-orange-500 dark:border-orange-400/60';
|
||||
case 'media':
|
||||
return 'border-yellow-500 dark:border-yellow-400/60';
|
||||
case 'baixa':
|
||||
return 'border-blue-400 dark:border-blue-300/60';
|
||||
default:
|
||||
return 'border-slate-300 dark:border-slate-600/60';
|
||||
}
|
||||
};
|
||||
|
||||
export function TaskCard({
|
||||
task,
|
||||
onEdit,
|
||||
onComplete,
|
||||
onDelete,
|
||||
onClick,
|
||||
onView,
|
||||
onArchive,
|
||||
isCompleted,
|
||||
showCreator = false,
|
||||
canArchive = false
|
||||
}: TaskCardProps) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const {
|
||||
updateTaskStatus,
|
||||
deleteTask,
|
||||
canEdit,
|
||||
canDelete,
|
||||
canMarkComplete,
|
||||
isUpdatingStatus,
|
||||
isDeleting
|
||||
} = useTaskActions();
|
||||
|
||||
const isOverdue = task.due_date && new Date(task.due_date) < new Date() && !task.is_completed;
|
||||
const commentsCount = task.task_comments?.[0]?.count || 0;
|
||||
const attachmentsCount = task.task_attachments?.[0]?.count || 0;
|
||||
const subtasksCount = task.task_subtasks?.[0]?.count || 0;
|
||||
const borderColor = getTaskBorderColor(task);
|
||||
const statusBadgeColor = getStatusBadgeColor(task.status || 'a_fazer');
|
||||
const taskRefColor = getTaskRefColor(task);
|
||||
const priorityBadgeColor = getPriorityBadgeColor(task.priority || 'media');
|
||||
const priorityBorderColor = getPriorityBorderColor(task.priority || 'media');
|
||||
|
||||
const handleStatusChange = (status: string) => {
|
||||
updateTaskStatus({
|
||||
id: task.id,
|
||||
status
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteTask(task.id);
|
||||
setShowDeleteDialog(false);
|
||||
};
|
||||
|
||||
const displayProfile = showCreator ? task.creator_profile : task.assigned_profiles?.[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
className={`
|
||||
cursor-pointer transition-all
|
||||
border ${getPriorityFullBorderColor(task.priority || 'media')}
|
||||
border-l-4 sm:border-l-8 ${priorityBorderColor}
|
||||
${getPriorityBgTintLight(task.priority || 'media')} dark:bg-card
|
||||
shadow-sm hover:shadow-lg dark:shadow-md dark:hover:shadow-xl
|
||||
${isCompleted ? 'opacity-80' : ''}
|
||||
`}
|
||||
onClick={() => onClick(task)}
|
||||
>
|
||||
<CardContent className="p-2 sm:p-3 space-y-1">
|
||||
{/* Mobile Layout: Stack everything vertically */}
|
||||
<div className="block sm:hidden">
|
||||
{/* Row 1: Task Ref and Priority */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={`text-xs font-medium ${taskRefColor}`}>
|
||||
{task.task_ref}
|
||||
</span>
|
||||
<Badge className={`text-xs px-1 py-0 ${priorityBadgeColor}`}>
|
||||
{priorityLabels[task.priority || 'media']}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Row 2: OF Number */}
|
||||
<div className="text-xs text-muted-foreground mb-1">
|
||||
OF: {task.of_number}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Title */}
|
||||
<h3 className={`text-xs font-semibold mb-1 text-card-foreground leading-tight ${isCompleted ? 'line-through' : ''}`}>
|
||||
{task.title}
|
||||
</h3>
|
||||
|
||||
{/* Row 4: Status and Due Date */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Badge className={`text-xs px-1 py-0 border ${statusBadgeColor}`}>
|
||||
{statusLabels[task.status || 'a_fazer']}
|
||||
</Badge>
|
||||
{task.due_date && (
|
||||
<div className={`flex items-center gap-1 text-xs ${isOverdue ? 'text-destructive' : 'text-muted-foreground'}`}>
|
||||
<Clock className="h-2 w-2" />
|
||||
<span>{format(new Date(task.due_date), 'dd/MM', { locale: ptBR })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 5: Bottom row with activities, category, profile and menu */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Activity indicators */}
|
||||
<div className="flex items-center gap-1">
|
||||
{commentsCount > 0 && (
|
||||
<div className="flex items-center gap-0.5 text-muted-foreground">
|
||||
<MessageCircle className="h-2 w-2" />
|
||||
<span className="text-xs">{commentsCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attachmentsCount > 0 && (
|
||||
<div className="flex items-center gap-0.5 text-muted-foreground">
|
||||
<Paperclip className="h-2 w-2" />
|
||||
<span className="text-xs">{attachmentsCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subtasksCount > 0 && (
|
||||
<div className="flex items-center gap-0.5 text-muted-foreground">
|
||||
<CheckSquare className="h-2 w-2" />
|
||||
<span className="text-xs">{subtasksCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
{task.category && (
|
||||
<Badge variant="outline" className="text-xs px-1 py-0">
|
||||
{task.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{displayProfile && (
|
||||
<UserAvatar
|
||||
imageUrl={displayProfile.profile_image_url || undefined}
|
||||
name={displayProfile.full_name}
|
||||
email={displayProfile.email}
|
||||
size="sm"
|
||||
className="h-9 w-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={e => e.stopPropagation()}>
|
||||
<Button variant="ghost" size="sm" className="h-4 w-4 p-0">
|
||||
<MoreHorizontal className="h-2 w-2" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="bg-slate-800 border-slate-700">
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
onView(task);
|
||||
}}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
Visualizar Detalhes
|
||||
</DropdownMenuItem>
|
||||
|
||||
{canEdit(task) && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
onEdit(task);
|
||||
}}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{canMarkComplete(task) && !task.is_completed && (
|
||||
<>
|
||||
{task.status !== 'em_andamento' && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleStatusChange('em_andamento');
|
||||
}} disabled={isUpdatingStatus}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Marcar Em Andamento
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleStatusChange('concluido');
|
||||
}} disabled={isUpdatingStatus}>
|
||||
<CheckSquare className="h-4 w-4 mr-2" />
|
||||
Marcar como Concluída
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{canArchive && onArchive && task.is_completed && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
onArchive(task.id);
|
||||
}}>
|
||||
<Archive className="h-4 w-4 mr-2" />
|
||||
Arquivar Tarefa
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{canDelete(task) && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
setShowDeleteDialog(true);
|
||||
}} className="text-destructive">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Deletar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Layout: Original horizontal layout */}
|
||||
<div className="hidden sm:block">
|
||||
{/* Task Reference and Priority */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={`text-sm font-medium ${taskRefColor}`}>
|
||||
Tarefa: {task.task_ref}
|
||||
</span>
|
||||
<Badge className={`text-xs ${priorityBadgeColor}`}>
|
||||
{priorityLabels[task.priority || 'media']}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Header: OF and Controls in single line */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
OF: {task.of_number}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{task.due_date && (
|
||||
<div className={`flex items-center gap-1 text-xs ${isOverdue ? 'text-destructive' : 'text-muted-foreground'}`}>
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>
|
||||
Prazo: {format(new Date(task.due_date), 'dd/MM', { locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Badge className={`text-xs border ${statusBadgeColor}`}>
|
||||
{statusLabels[task.status || 'a_fazer']}
|
||||
</Badge>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={e => e.stopPropagation()}>
|
||||
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
||||
<MoreHorizontal className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="bg-slate-800 border-slate-700">
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
onView(task);
|
||||
}}>
|
||||
<Eye className="h-4 w-4 mr-2" />
|
||||
Visualizar Detalhes
|
||||
</DropdownMenuItem>
|
||||
|
||||
{canEdit(task) && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
onEdit(task);
|
||||
}}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{canMarkComplete(task) && !task.is_completed && (
|
||||
<>
|
||||
{task.status !== 'em_andamento' && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleStatusChange('em_andamento');
|
||||
}} disabled={isUpdatingStatus}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Marcar Em Andamento
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleStatusChange('concluido');
|
||||
}} disabled={isUpdatingStatus}>
|
||||
<CheckSquare className="h-4 w-4 mr-2" />
|
||||
Marcar como Concluída
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{canArchive && onArchive && task.is_completed && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
onArchive(task.id);
|
||||
}}>
|
||||
<Archive className="h-4 w-4 mr-2" />
|
||||
Arquivar Tarefa
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{canDelete(task) && (
|
||||
<DropdownMenuItem onClick={e => {
|
||||
e.stopPropagation();
|
||||
setShowDeleteDialog(true);
|
||||
}} className="text-destructive">
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Deletar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{displayProfile && (
|
||||
<UserAvatar
|
||||
imageUrl={displayProfile.profile_image_url || undefined}
|
||||
name={displayProfile.full_name}
|
||||
email={displayProfile.email}
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task Title - Single line with ellipsis */}
|
||||
<h3 className={`text-sm font-semibold mb-1 text-card-foreground leading-tight truncate ${isCompleted ? 'line-through' : ''}`}>
|
||||
{task.title}
|
||||
</h3>
|
||||
|
||||
{/* Footer: Activity Icons and Category */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{commentsCount > 0 && (
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<MessageCircle className="h-3 w-3" />
|
||||
<span className="text-xs">{commentsCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attachmentsCount > 0 && (
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<Paperclip className="h-3 w-3" />
|
||||
<span className="text-xs">{attachmentsCount}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subtasksCount > 0 && (
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
<CheckSquare className="h-3 w-3" />
|
||||
<span className="text-xs">{subtasksCount}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.category && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{task.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DeleteTaskDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleDelete}
|
||||
taskTitle={task.title}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CheckSquare, UserCheck } from 'lucide-react';
|
||||
import { TaskCounters as TaskCountersType } from '@/types/tasks';
|
||||
interface TaskCountersProps {
|
||||
counters: TaskCountersType;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
export function TaskCounters({
|
||||
counters,
|
||||
isLoading
|
||||
}: TaskCountersProps) {
|
||||
if (isLoading) {
|
||||
return <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
Minhas Tarefas Pendentes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-muted-foreground">
|
||||
Carregando...
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<UserCheck className="h-4 w-4" />
|
||||
Tarefas que Atribuí
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-muted-foreground">
|
||||
Carregando...
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>;
|
||||
}
|
||||
return <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="bg-slate-800/50 border-slate-700">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<CheckSquare className="h-4 w-4" />
|
||||
Minhas Tarefas Pendentes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="bg-slate-800/50 border-slate-700">
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{counters.myPendingTasks}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
tarefas atribuídas a você
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="bg-slate-800/50 border-slate-700">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<UserCheck className="h-4 w-4" />
|
||||
Tarefas que Atribuí
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="bg-slate-800/50 border-slate-700">
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{counters.tasksIAssigned}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
pendentes de outros usuários
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { useState } from 'react';
|
||||
import { Filter, Search, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TaskFilters as TaskFiltersType } from '@/types/tasks';
|
||||
interface TaskFiltersProps {
|
||||
filters: TaskFiltersType;
|
||||
onFiltersChange: (filters: TaskFiltersType) => void;
|
||||
availableOFs: Array<{
|
||||
of_number: string;
|
||||
cliente: string;
|
||||
}>;
|
||||
availableUsers?: Array<{
|
||||
id: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
}>;
|
||||
showAssignedToFilter?: boolean;
|
||||
}
|
||||
const statusOptions = [{
|
||||
value: 'a_fazer',
|
||||
label: 'A Fazer'
|
||||
}, {
|
||||
value: 'em_andamento',
|
||||
label: 'Em Andamento'
|
||||
}, {
|
||||
value: 'revisao',
|
||||
label: 'Revisão'
|
||||
}, {
|
||||
value: 'pendente',
|
||||
label: 'Pendente'
|
||||
}, {
|
||||
value: 'bloqueado',
|
||||
label: 'Bloqueado'
|
||||
}, {
|
||||
value: 'concluido',
|
||||
label: 'Concluído'
|
||||
}];
|
||||
const priorityOptions = [{
|
||||
value: 'baixa',
|
||||
label: 'Baixa'
|
||||
}, {
|
||||
value: 'media',
|
||||
label: 'Média'
|
||||
}, {
|
||||
value: 'alta',
|
||||
label: 'Alta'
|
||||
}, {
|
||||
value: 'urgente',
|
||||
label: 'Urgente'
|
||||
}];
|
||||
const dueDateRangeOptions = [{
|
||||
value: 'all',
|
||||
label: 'Todas'
|
||||
}, {
|
||||
value: 'week',
|
||||
label: 'Próxima semana'
|
||||
}, {
|
||||
value: 'month',
|
||||
label: 'Próximo mês'
|
||||
}, {
|
||||
value: 'overdue',
|
||||
label: 'Em atraso'
|
||||
}];
|
||||
export function TaskFilters({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
availableOFs,
|
||||
availableUsers = [],
|
||||
showAssignedToFilter = false
|
||||
}: TaskFiltersProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState(filters.search || '');
|
||||
const [taskRefSearch, setTaskRefSearch] = useState(filters.task_ref || '');
|
||||
const updateFilters = (newFilters: Partial<TaskFiltersType>) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
...newFilters
|
||||
});
|
||||
};
|
||||
const clearFilters = () => {
|
||||
setSearchTerm('');
|
||||
setTaskRefSearch('');
|
||||
onFiltersChange({});
|
||||
};
|
||||
const handleSearchSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
updateFilters({
|
||||
search: searchTerm.trim() || undefined
|
||||
});
|
||||
};
|
||||
const handleTaskRefSearchSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
updateFilters({
|
||||
task_ref: taskRefSearch.trim() || undefined
|
||||
});
|
||||
};
|
||||
const activeFiltersCount = Object.values(filters).filter(value => value !== undefined && value !== '' && value !== 'all').length;
|
||||
return <div className="space-y-4">
|
||||
{/* Search Bars */}
|
||||
{/* Filter Toggle and Active Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs mx-0 py-0 px-[10px] font-normal rounded-sm
|
||||
bg-slate-50 border-slate-300 text-slate-900 hover:bg-slate-100 hover:border-slate-400
|
||||
dark:bg-sky-950 dark:border-slate-600 dark:text-slate-50 dark:hover:bg-sky-800"
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
Filtros
|
||||
{activeFiltersCount > 0 && <Badge variant="secondary" className="ml-2 bg-blue-600 text-white">
|
||||
{activeFiltersCount}
|
||||
</Badge>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-4 bg-slate-800 border-slate-700" align="start">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-semibold text-white">Filtros</h4>
|
||||
{activeFiltersCount > 0 && <Button variant="ghost" size="sm" onClick={clearFilters} className="text-slate-400 hover:text-white">
|
||||
Limpar
|
||||
</Button>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-300 mb-1 block">OF</label>
|
||||
<Select value={filters.of_number || 'all'} onValueChange={value => updateFilters({
|
||||
of_number: value === 'all' ? undefined : value
|
||||
})}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Todas as OFs" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600 max-h-60">
|
||||
<SelectItem value="all" className="text-white hover:bg-slate-600">Todas as OFs</SelectItem>
|
||||
{availableOFs.map(of => <SelectItem key={of.of_number} value={of.of_number} className="text-white hover:bg-slate-600">
|
||||
{of.of_number} - {of.cliente}
|
||||
</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-300 mb-1 block">Status</label>
|
||||
<Select value={filters.status || 'all'} onValueChange={value => updateFilters({
|
||||
status: value === 'all' ? undefined : value as any
|
||||
})}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Todos os status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
<SelectItem value="all" className="text-white hover:bg-slate-600">Todos os status</SelectItem>
|
||||
{statusOptions.map(status => <SelectItem key={status.value} value={status.value} className="text-white hover:bg-slate-600">
|
||||
{status.label}
|
||||
</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-300 mb-1 block">Prioridade</label>
|
||||
<Select value={filters.priority || 'all'} onValueChange={value => updateFilters({
|
||||
priority: value === 'all' ? undefined : value as any
|
||||
})}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Todas as prioridades" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
<SelectItem value="all" className="text-white hover:bg-slate-600">Todas as prioridades</SelectItem>
|
||||
{priorityOptions.map(priority => <SelectItem key={priority.value} value={priority.value} className="text-white hover:bg-slate-600">
|
||||
{priority.label}
|
||||
</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-300 mb-1 block">Prazo</label>
|
||||
<Select value={filters.due_date_range || 'all'} onValueChange={value => updateFilters({
|
||||
due_date_range: value === 'all' ? undefined : value as any
|
||||
})}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
{dueDateRangeOptions.map(option => <SelectItem key={option.value} value={option.value} className="text-white hover:bg-slate-600">
|
||||
{option.label}
|
||||
</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{showAssignedToFilter && availableUsers.length > 0 && <div>
|
||||
<label className="text-sm font-medium text-slate-300 mb-1 block">Atribuído para</label>
|
||||
<Select value={filters.assigned_to || 'all'} onValueChange={value => updateFilters({
|
||||
assigned_to: value === 'all' ? undefined : value
|
||||
})}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Todos os usuários" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
<SelectItem value="all" className="text-white hover:bg-slate-600">Todos os usuários</SelectItem>
|
||||
{availableUsers.map(user => <SelectItem key={user.id} value={user.id} className="text-white hover:bg-slate-600">
|
||||
{user.full_name || user.email}
|
||||
</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Display Active Filters */}
|
||||
{filters.of_number && <Badge variant="secondary" className="flex items-center gap-1">
|
||||
OF: {filters.of_number}
|
||||
<X className="h-3 w-3 cursor-pointer" onClick={() => updateFilters({
|
||||
of_number: undefined
|
||||
})} />
|
||||
</Badge>}
|
||||
|
||||
{filters.status && <Badge variant="secondary" className="flex items-center gap-1">
|
||||
Status: {statusOptions.find(s => s.value === filters.status)?.label}
|
||||
<X className="h-3 w-3 cursor-pointer" onClick={() => updateFilters({
|
||||
status: undefined
|
||||
})} />
|
||||
</Badge>}
|
||||
|
||||
{filters.priority && <Badge variant="secondary" className="flex items-center gap-1">
|
||||
Prioridade: {priorityOptions.find(p => p.value === filters.priority)?.label}
|
||||
<X className="h-3 w-3 cursor-pointer" onClick={() => updateFilters({
|
||||
priority: undefined
|
||||
})} />
|
||||
</Badge>}
|
||||
|
||||
{filters.task_ref && <Badge variant="secondary" className="flex items-center gap-1">
|
||||
ID: {filters.task_ref}
|
||||
<X className="h-3 w-3 cursor-pointer" onClick={() => {
|
||||
setTaskRefSearch('');
|
||||
updateFilters({
|
||||
task_ref: undefined
|
||||
});
|
||||
}} />
|
||||
</Badge>}
|
||||
|
||||
{filters.search && <Badge variant="secondary" className="flex items-center gap-1">
|
||||
Busca: {filters.search}
|
||||
<X className="h-3 w-3 cursor-pointer" onClick={() => {
|
||||
setSearchTerm('');
|
||||
updateFilters({
|
||||
search: undefined
|
||||
});
|
||||
}} />
|
||||
</Badge>}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { CalendarIcon, X, Plus } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Database } from '@/integrations/supabase/types';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
type Task = Database['public']['Tables']['tasks']['Row'];
|
||||
type TaskInsert = Database['public']['Tables']['tasks']['Insert'];
|
||||
|
||||
interface TaskModalProps {
|
||||
task?: Task | null;
|
||||
availableOFs: Array<{ of_number: string; cliente: string }>;
|
||||
availableUsers: Array<{ id: string; full_name: string; email: string }>;
|
||||
onSave: (data: Omit<TaskInsert, 'task_ref' | 'id' | 'created_at' | 'updated_at' | 'created_by'>) => void;
|
||||
onClose: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string().min(1, 'Título é obrigatório'),
|
||||
description: z.string().optional(),
|
||||
of_number: z.string().min(1, 'OF é obrigatória'),
|
||||
assigned_to: z.array(z.string()).optional(),
|
||||
due_date: z.date().optional(),
|
||||
status: z.enum(['a_fazer', 'em_andamento', 'revisao', 'pendente', 'bloqueado', 'concluido']),
|
||||
priority: z.enum(['baixa', 'media', 'alta', 'urgente']),
|
||||
category: z.string().optional(),
|
||||
});
|
||||
|
||||
type TaskFormData = z.infer<typeof taskSchema>;
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'a_fazer', label: 'A Fazer' },
|
||||
{ value: 'em_andamento', label: 'Em Andamento' },
|
||||
{ value: 'revisao', label: 'Revisão' },
|
||||
{ value: 'pendente', label: 'Pendente' },
|
||||
{ value: 'bloqueado', label: 'Bloqueado' },
|
||||
{ value: 'concluido', label: 'Concluído' },
|
||||
];
|
||||
|
||||
const priorityOptions = [
|
||||
{ value: 'baixa', label: 'Baixa' },
|
||||
{ value: 'media', label: 'Média' },
|
||||
{ value: 'alta', label: 'Alta' },
|
||||
{ value: 'urgente', label: 'Urgente' },
|
||||
];
|
||||
|
||||
export function TaskModal({ task, availableOFs, availableUsers, onSave, onClose, isLoading }: TaskModalProps) {
|
||||
const { user } = useAuth();
|
||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
||||
const [dueDate, setDueDate] = useState<Date>();
|
||||
const [allowCustomOF, setAllowCustomOF] = useState(true); // Iniciar marcado
|
||||
|
||||
const form = useForm<TaskFormData>({
|
||||
resolver: zodResolver(taskSchema),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
of_number: 'ADM-Compras', // Valor padrão
|
||||
status: 'a_fazer',
|
||||
priority: 'media',
|
||||
category: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (task) {
|
||||
form.reset({
|
||||
title: task.title,
|
||||
description: task.description || '',
|
||||
of_number: task.of_number,
|
||||
status: task.status || 'a_fazer',
|
||||
priority: task.priority || 'media',
|
||||
category: task.category || '',
|
||||
});
|
||||
setSelectedUsers(task.assigned_to || []);
|
||||
setDueDate(task.due_date ? new Date(task.due_date) : undefined);
|
||||
|
||||
// Check if OF exists in available OFs
|
||||
const ofExists = availableOFs.some(of => of.of_number === task.of_number);
|
||||
setAllowCustomOF(!ofExists);
|
||||
} else {
|
||||
form.reset({
|
||||
title: '',
|
||||
description: '',
|
||||
of_number: 'ADM-Compras', // Valor padrão para novas tarefas
|
||||
status: 'a_fazer',
|
||||
priority: 'media',
|
||||
category: '',
|
||||
});
|
||||
setSelectedUsers([]);
|
||||
setDueDate(undefined);
|
||||
setAllowCustomOF(true); // Sempre marcado para novas tarefas
|
||||
}
|
||||
}, [task, form, availableOFs]);
|
||||
|
||||
const onSubmit = (data: TaskFormData) => {
|
||||
console.log('📝 Form data submitted:', data);
|
||||
console.log('👥 Selected users:', selectedUsers);
|
||||
console.log('📅 Due date:', dueDate);
|
||||
|
||||
const taskData: Omit<TaskInsert, 'task_ref' | 'id' | 'created_at' | 'updated_at' | 'created_by'> = {
|
||||
title: data.title,
|
||||
description: data.description || null,
|
||||
of_number: data.of_number,
|
||||
status: data.status,
|
||||
priority: data.priority,
|
||||
category: data.category || null,
|
||||
assigned_to: selectedUsers.length > 0 ? selectedUsers : null,
|
||||
due_date: dueDate?.toISOString() || null,
|
||||
is_completed: false,
|
||||
completed_at: null,
|
||||
completed_by: null,
|
||||
};
|
||||
|
||||
console.log('📤 Final task data being sent:', taskData);
|
||||
onSave(taskData);
|
||||
};
|
||||
|
||||
const addUser = (userId: string) => {
|
||||
if (!selectedUsers.includes(userId)) {
|
||||
setSelectedUsers([...selectedUsers, userId]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeUser = (userId: string) => {
|
||||
setSelectedUsers(selectedUsers.filter(id => id !== userId));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto bg-slate-800 border-slate-700 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{task ? `Editar Tarefa ${task.task_ref}` : 'Nova Tarefa'}
|
||||
</DialogTitle>
|
||||
{task && (
|
||||
<p className="text-slate-400 text-sm">
|
||||
ID: {task.task_ref} | OF: {task.of_number}
|
||||
</p>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">OF * (Ordem de Fabricação)</Label>
|
||||
<div className="space-y-2">
|
||||
{!allowCustomOF && availableOFs.length > 0 ? (
|
||||
<Select
|
||||
value={form.watch('of_number')}
|
||||
onValueChange={(value) => form.setValue('of_number', value)}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Selecione uma OF existente" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600 max-h-60">
|
||||
{availableOFs.map((of) => (
|
||||
<SelectItem key={of.of_number} value={of.of_number} className="text-white hover:bg-slate-600">
|
||||
{of.of_number} - {of.cliente}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
{...form.register('of_number')}
|
||||
className="bg-slate-700 border-slate-600 text-white"
|
||||
placeholder="Digite o número da OF (ex: ADM-Compras)"
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allowCustomOF"
|
||||
checked={allowCustomOF}
|
||||
onChange={(e) => {
|
||||
setAllowCustomOF(e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
form.setValue('of_number', '');
|
||||
} else {
|
||||
form.setValue('of_number', 'ADM-Compras');
|
||||
}
|
||||
}}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor="allowCustomOF" className="text-sm text-slate-400">
|
||||
Permitir OF customizada (não listada)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{form.formState.errors.of_number && (
|
||||
<p className="text-red-400 text-sm mt-1">{form.formState.errors.of_number.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">Título *</Label>
|
||||
<Input
|
||||
{...form.register('title')}
|
||||
className="bg-slate-700 border-slate-600 text-white"
|
||||
placeholder="Digite o título da tarefa"
|
||||
/>
|
||||
{form.formState.errors.title && (
|
||||
<p className="text-red-400 text-sm mt-1">{form.formState.errors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">Descrição</Label>
|
||||
<Textarea
|
||||
{...form.register('description')}
|
||||
className="bg-slate-700 border-slate-600 text-white min-h-[100px]"
|
||||
placeholder="Digite a descrição detalhada da tarefa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-300">Status</Label>
|
||||
<Select
|
||||
value={form.watch('status')}
|
||||
onValueChange={(value) => form.setValue('status', value as any)}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
{statusOptions.map((status) => (
|
||||
<SelectItem key={status.value} value={status.value} className="text-white hover:bg-slate-600">
|
||||
{status.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-300">Prioridade</Label>
|
||||
<Select
|
||||
value={form.watch('priority')}
|
||||
onValueChange={(value) => form.setValue('priority', value as any)}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
{priorityOptions.map((priority) => (
|
||||
<SelectItem key={priority.value} value={priority.value} className="text-white hover:bg-slate-600">
|
||||
{priority.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-300">Categoria</Label>
|
||||
<Input
|
||||
{...form.register('category')}
|
||||
className="bg-slate-700 border-slate-600 text-white"
|
||||
placeholder="Ex: Desenvolvimento, Marketing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-300">Prazo</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal bg-slate-700 border-slate-600 text-white hover:bg-slate-600",
|
||||
!dueDate && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dueDate ? format(dueDate, "PPP", { locale: ptBR }) : "Selecione uma data"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0 bg-slate-700 border-slate-600" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dueDate}
|
||||
onSelect={setDueDate}
|
||||
initialFocus
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">Responsáveis</Label>
|
||||
<Select onValueChange={addUser}>
|
||||
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
|
||||
<SelectValue placeholder="Selecione usuários" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-700 border-slate-600">
|
||||
{availableUsers
|
||||
.filter(user => !selectedUsers.includes(user.id))
|
||||
.map((user) => (
|
||||
<SelectItem key={user.id} value={user.id} className="text-white hover:bg-slate-600">
|
||||
{user.full_name || user.email}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedUsers.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{selectedUsers.map((userId) => {
|
||||
const user = availableUsers.find(u => u.id === userId);
|
||||
return (
|
||||
<Badge key={userId} variant="secondary" className="flex items-center gap-1">
|
||||
{user?.full_name || user?.email}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => removeUser(userId)}
|
||||
/>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Salvando...' : 'Salvar'}
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { TaskFilters } from './TaskFilters';
|
||||
import { AcceptTaskButton } from './AcceptTaskButton';
|
||||
import { TaskModal } from './TaskModal';
|
||||
import { TaskViewModal } from './TaskViewModal';
|
||||
import { useTasksEnhanced } from '@/hooks/useTasksEnhanced';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Task, TaskFilters as TaskFiltersType } from '@/types/tasks';
|
||||
|
||||
interface TaskPanelProps {
|
||||
type: 'my_tasks' | 'my_completed' | 'assigned_tasks';
|
||||
title: string;
|
||||
description?: string;
|
||||
availableOFs: Array<{ of_number: string; cliente: string }>;
|
||||
availableUsers?: Array<{ id: string; full_name: string; email: string }>;
|
||||
}
|
||||
|
||||
export function TaskPanel({ type, title, description, availableOFs, availableUsers }: TaskPanelProps) {
|
||||
const { user } = useAuth();
|
||||
const { fetchMyTasks, fetchMyCompletedTasks, fetchAssignedTasks, acceptTask, updateTask, archiveTask, isAccepting, isUpdating, isArchiving } = useTasksEnhanced();
|
||||
const [filters, setFilters] = useState<TaskFiltersType>({});
|
||||
const [editingTask, setEditingTask] = useState<Task | null>(null);
|
||||
const [viewingTask, setViewingTask] = useState<Task | null>(null);
|
||||
|
||||
const fetchFunction = async (filters: TaskFiltersType): Promise<Task[]> => {
|
||||
if (type === 'my_tasks') {
|
||||
const tasks = await fetchMyTasks(filters);
|
||||
return tasks.map(task => ({
|
||||
...task,
|
||||
assigned_profiles: []
|
||||
}));
|
||||
} else if (type === 'my_completed') {
|
||||
const tasks = await fetchMyCompletedTasks(filters);
|
||||
return tasks.map(task => ({
|
||||
...task,
|
||||
assigned_profiles: []
|
||||
}));
|
||||
} else {
|
||||
return await fetchAssignedTasks(filters);
|
||||
}
|
||||
};
|
||||
|
||||
const { data: tasks = [], isLoading, error } = useQuery({
|
||||
queryKey: ['tasks', type, filters],
|
||||
queryFn: () => fetchFunction(filters),
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const sortTasks = (tasks: Task[]) => {
|
||||
return tasks.sort((a, b) => {
|
||||
// First by due date (nulls last)
|
||||
if (a.due_date && !b.due_date) return -1;
|
||||
if (!a.due_date && b.due_date) return 1;
|
||||
if (a.due_date && b.due_date) {
|
||||
const dateCompare = new Date(a.due_date).getTime() - new Date(b.due_date).getTime();
|
||||
if (dateCompare !== 0) return dateCompare;
|
||||
}
|
||||
// Then by priority
|
||||
const priorityOrder = { urgente: 0, alta: 1, media: 2, baixa: 3 };
|
||||
return priorityOrder[a.priority || 'media'] - priorityOrder[b.priority || 'media'];
|
||||
});
|
||||
};
|
||||
|
||||
const handleAcceptTask = (taskId: string) => {
|
||||
acceptTask(taskId);
|
||||
};
|
||||
|
||||
const handleEditTask = (task: Task) => {
|
||||
console.log('Edit task:', task.id);
|
||||
setEditingTask(task);
|
||||
};
|
||||
|
||||
const handleCompleteTask = (taskId: string) => {
|
||||
console.log('Complete task:', taskId);
|
||||
// This is now handled by the TaskCard component
|
||||
};
|
||||
|
||||
const handleDeleteTask = (taskId: string) => {
|
||||
console.log('Delete task:', taskId);
|
||||
// This is now handled by the TaskCard component
|
||||
};
|
||||
|
||||
const handleTaskClick = (task: Task) => {
|
||||
console.log('Task clicked:', task.id);
|
||||
// TODO: Open task details modal
|
||||
};
|
||||
|
||||
const handleViewTask = (task: Task) => {
|
||||
console.log('View task:', task.id);
|
||||
setViewingTask(task);
|
||||
};
|
||||
|
||||
const handleArchiveTask = (taskId: string) => {
|
||||
console.log('Archive task:', taskId);
|
||||
archiveTask(taskId);
|
||||
};
|
||||
|
||||
const handleSaveTask = (taskData: any) => {
|
||||
if (editingTask) {
|
||||
updateTask({
|
||||
id: editingTask.id,
|
||||
updates: taskData
|
||||
});
|
||||
setEditingTask(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseEditModal = () => {
|
||||
setEditingTask(null);
|
||||
};
|
||||
|
||||
const handleCloseViewModal = () => {
|
||||
setViewingTask(null);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-slate-900 dark:text-white text-lg sm:text-xl">{title}</CardTitle>
|
||||
{description && <p className="text-slate-600 dark:text-slate-400 text-sm">{description}</p>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-slate-600 dark:text-slate-400 text-center py-8">Carregando tarefas...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-slate-900 dark:text-white text-lg sm:text-xl">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-red-600 dark:text-red-400 text-center py-8">Erro ao carregar tarefas</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const sortedTasks = sortTasks(tasks);
|
||||
|
||||
return (
|
||||
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle className="text-slate-900 dark:text-white text-lg sm:text-xl">{title}</CardTitle>
|
||||
{description && <p className="text-slate-600 dark:text-slate-400 text-sm mt-1">{description}</p>}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<TaskFilters
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
availableOFs={availableOFs}
|
||||
availableUsers={availableUsers}
|
||||
showAssignedToFilter={type === 'assigned_tasks'}
|
||||
/>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400 whitespace-nowrap">
|
||||
{tasks.length} {tasks.length === 1 ? 'tarefa' : 'tarefas'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{sortedTasks.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-slate-600 dark:text-slate-400">Nenhuma tarefa encontrada.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2 sm:gap-3">
|
||||
{sortedTasks.map((task) => (
|
||||
<div key={task.id} className="space-y-2">
|
||||
<TaskCard
|
||||
task={task}
|
||||
onEdit={handleEditTask}
|
||||
onComplete={handleCompleteTask}
|
||||
onDelete={handleDeleteTask}
|
||||
onClick={handleTaskClick}
|
||||
onView={handleViewTask}
|
||||
onArchive={handleArchiveTask}
|
||||
isCompleted={type === 'my_completed'}
|
||||
showCreator={type === 'my_tasks' || type === 'my_completed'}
|
||||
canArchive={type === 'assigned_tasks'}
|
||||
/>
|
||||
{type === 'my_tasks' && task.status === 'a_fazer' && (
|
||||
<div className="ml-2 sm:ml-4">
|
||||
<AcceptTaskButton
|
||||
taskId={task.id}
|
||||
onAccept={handleAcceptTask}
|
||||
isLoading={isAccepting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{editingTask && (
|
||||
<TaskModal
|
||||
task={editingTask}
|
||||
availableOFs={availableOFs}
|
||||
availableUsers={availableUsers || []}
|
||||
onSave={handleSaveTask}
|
||||
onClose={handleCloseEditModal}
|
||||
isLoading={isUpdating}
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewingTask && (
|
||||
<TaskViewModal
|
||||
task={viewingTask}
|
||||
onClose={handleCloseViewModal}
|
||||
onEdit={type === 'assigned_tasks' ? handleEditTask : undefined}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { UserAvatar } from '@/components/ui/user-avatar';
|
||||
import { Task } from '@/types/tasks';
|
||||
|
||||
interface TaskViewModalProps {
|
||||
task: Task | null;
|
||||
onClose: () => void;
|
||||
onEdit?: (task: Task) => void;
|
||||
}
|
||||
|
||||
const statusLabels = {
|
||||
a_fazer: 'A Fazer',
|
||||
em_andamento: 'Em Andamento',
|
||||
revisao: 'Revisão',
|
||||
pendente: 'Pendente',
|
||||
bloqueado: 'Bloqueado',
|
||||
concluido: 'Concluído'
|
||||
};
|
||||
|
||||
const priorityLabels = {
|
||||
baixa: 'Baixa',
|
||||
media: 'Média',
|
||||
alta: 'Alta',
|
||||
urgente: 'Urgente'
|
||||
};
|
||||
|
||||
const getPriorityBadgeColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgente':
|
||||
return 'bg-red-500 text-white border-red-500';
|
||||
case 'alta':
|
||||
return 'bg-orange-500 text-white border-orange-500';
|
||||
case 'media':
|
||||
return 'bg-yellow-500 text-white border-yellow-500';
|
||||
case 'baixa':
|
||||
return 'bg-blue-400 text-white border-blue-400';
|
||||
default:
|
||||
return 'bg-gray-500 text-white border-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadgeColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'concluido':
|
||||
return 'bg-green-500 text-white border-green-500';
|
||||
case 'em_andamento':
|
||||
return 'bg-blue-500 text-white border-blue-500';
|
||||
case 'revisao':
|
||||
return 'bg-purple-500 text-white border-purple-500';
|
||||
case 'pendente':
|
||||
return 'bg-yellow-500 text-white border-yellow-500';
|
||||
case 'bloqueado':
|
||||
return 'bg-red-500 text-white border-red-500';
|
||||
default:
|
||||
return 'bg-gray-500 text-white border-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
export function TaskViewModal({ task, onClose, onEdit }: TaskViewModalProps) {
|
||||
if (!task) return null;
|
||||
|
||||
const priorityBadgeColor = getPriorityBadgeColor(task.priority || 'media');
|
||||
const statusBadgeColor = getStatusBadgeColor(task.status || 'a_fazer');
|
||||
|
||||
return (
|
||||
<Dialog open={!!task} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto bg-slate-800 border-slate-700 text-white">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-xl">
|
||||
Detalhes da Tarefa - {task.task_ref}
|
||||
</DialogTitle>
|
||||
<Button variant="ghost" size="sm" onClick={onClose} className="h-6 w-6 p-0">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-400">
|
||||
<span>OF: {task.of_number}</span>
|
||||
{task.revision && task.revision > 1 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Revisão {task.revision}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Status and Priority */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-slate-300 text-sm">Status</label>
|
||||
<Badge className={`${statusBadgeColor}`}>
|
||||
{statusLabels[task.status || 'a_fazer']}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-slate-300 text-sm">Prioridade</label>
|
||||
<Badge className={`${priorityBadgeColor}`}>
|
||||
{priorityLabels[task.priority || 'media']}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-slate-300 text-sm font-medium">Título</label>
|
||||
<p className="text-white text-lg">{task.title}</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{task.description && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-slate-300 text-sm font-medium">Descrição</label>
|
||||
<div className="bg-slate-700/50 rounded-lg p-4">
|
||||
<p className="text-white whitespace-pre-wrap">{task.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category */}
|
||||
{task.category && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-slate-300 text-sm font-medium">Categoria</label>
|
||||
<p className="text-white">{task.category}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Due Date */}
|
||||
{task.due_date && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-slate-300 text-sm font-medium">Prazo</label>
|
||||
<p className="text-white">
|
||||
{format(new Date(task.due_date), "dd 'de' MMMM 'de' yyyy", { locale: ptBR })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assigned Users */}
|
||||
{task.assigned_profiles && task.assigned_profiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-slate-300 text-sm font-medium">Responsáveis</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{task.assigned_profiles.map((profile) => (
|
||||
<div key={profile.id} className="flex items-center gap-2 bg-slate-700/50 rounded-lg p-2">
|
||||
<UserAvatar
|
||||
imageUrl={profile.profile_image_url || undefined}
|
||||
name={profile.full_name}
|
||||
email={profile.email}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-white text-sm">{profile.full_name || profile.email}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Creator */}
|
||||
{task.creator_profile && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-slate-300 text-sm font-medium">Criado por</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<UserAvatar
|
||||
imageUrl={task.creator_profile.profile_image_url || undefined}
|
||||
name={task.creator_profile.full_name}
|
||||
email={task.creator_profile.email}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-white">{task.creator_profile.full_name || task.creator_profile.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div className="space-y-1">
|
||||
<label className="text-slate-300">Criado em</label>
|
||||
<p className="text-white">
|
||||
{format(new Date(task.created_at), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}
|
||||
</p>
|
||||
</div>
|
||||
{task.updated_at && task.updated_at !== task.created_at && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-slate-300">Atualizado em</label>
|
||||
<p className="text-white">
|
||||
{format(new Date(task.updated_at), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{task.completed_at && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-slate-300">Concluído em</label>
|
||||
<p className="text-white">
|
||||
{format(new Date(task.completed_at), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-4 border-t border-slate-600">
|
||||
{onEdit && (
|
||||
<Button
|
||||
onClick={() => onEdit(task)}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Editar Tarefa
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Fechar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user