🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
+49
View File
@@ -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>
);
}
+56
View File
@@ -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>
);
}
+519
View File
@@ -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}
/>
</>
);
}
+78
View File
@@ -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>;
}
+263
View File
@@ -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>;
}
+375
View File
@@ -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>
);
}
+227
View File
@@ -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>
);
}
+228
View File
@@ -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>
);
}