first commit
This commit is contained in:
commit
c9fb67e751
423 files changed
+53287
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { Anexo } from '../../types/entities';
|
||||
import {
|
||||
DocumentIcon,
|
||||
XMarkIcon,
|
||||
ArrowDownTrayIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
interface AnexosListProps {
|
||||
anexos: Anexo[];
|
||||
onChange: (anexos: Anexo[]) => void;
|
||||
}
|
||||
|
||||
export const AnexosList: React.FC<AnexosListProps> = ({ anexos, onChange }) => {
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
const novosAnexos: Anexo[] = acceptedFiles.map(file => ({
|
||||
id: Math.random().toString(),
|
||||
nome: file.name,
|
||||
tipo: file.type,
|
||||
tamanho: file.size,
|
||||
url: URL.createObjectURL(file),
|
||||
tarefaId: ''
|
||||
}));
|
||||
|
||||
onChange([...anexos, ...novosAnexos]);
|
||||
}, [anexos, onChange]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer
|
||||
${isDragActive
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600'}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{isDragActive
|
||||
? 'Solte os arquivos aqui...'
|
||||
: 'Arraste e solte arquivos aqui, ou clique para selecionar'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{anexos.map((anexo) => (
|
||||
<li
|
||||
key={anexo.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<DocumentIcon className="h-6 w-6 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{anexo.nome}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatFileSize(anexo.tamanho)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(anexo.url, '_blank')}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
>
|
||||
<ArrowDownTrayIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(anexos.filter(a => a.id !== anexo.id))}
|
||||
className="text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Comentario } from '../../types/entities';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
interface ComentariosListProps {
|
||||
comentarios: Comentario[];
|
||||
onChange: (comentarios: Comentario[]) => void;
|
||||
}
|
||||
|
||||
export const ComentariosList: React.FC<ComentariosListProps> = ({ comentarios, onChange }) => {
|
||||
const [novoComentario, setNovoComentario] = useState('');
|
||||
const { user } = useAuth();
|
||||
|
||||
const adicionarComentario = () => {
|
||||
if (!novoComentario.trim()) return;
|
||||
|
||||
const comentario: Comentario = {
|
||||
id: Math.random().toString(),
|
||||
texto: novoComentario,
|
||||
criadoPorId: user?.id || '',
|
||||
criadoEm: new Date(),
|
||||
tarefaId: ''
|
||||
};
|
||||
|
||||
onChange([...comentarios, comentario]);
|
||||
setNovoComentario('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<textarea
|
||||
value={novoComentario}
|
||||
onChange={(e) => setNovoComentario(e.target.value)}
|
||||
placeholder="Adicione um comentário..."
|
||||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={adicionarComentario}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600
|
||||
hover:bg-blue-700 rounded-lg disabled:opacity-50"
|
||||
disabled={!novoComentario.trim()}
|
||||
>
|
||||
Comentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{comentarios.map((comentario) => (
|
||||
<div
|
||||
key={comentario.id}
|
||||
className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src={`https://ui-avatars.com/api/?name=${user?.nome}&background=0D8ABC&color=fff`}
|
||||
alt={user?.nome}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{user?.nome}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{format(new Date(comentario.criadoEm), "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-gray-700 dark:text-gray-300">
|
||||
{comentario.texto}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PlusIcon, XMarkIcon, CheckIcon } from '@heroicons/react/24/outline';
|
||||
import { SubTarefa } from '../../types/entities';
|
||||
|
||||
interface SubtarefasListProps {
|
||||
subtarefas: SubTarefa[];
|
||||
onChange: (subtarefas: SubTarefa[]) => void;
|
||||
}
|
||||
|
||||
export const SubtarefasList: React.FC<SubtarefasListProps> = ({ subtarefas, onChange }) => {
|
||||
const [novaTarefa, setNovaTarefa] = useState('');
|
||||
|
||||
const adicionarSubtarefa = () => {
|
||||
if (!novaTarefa.trim()) return;
|
||||
|
||||
onChange([
|
||||
...subtarefas,
|
||||
{
|
||||
id: Math.random().toString(),
|
||||
titulo: novaTarefa,
|
||||
concluida: false,
|
||||
tarefaId: ''
|
||||
}
|
||||
]);
|
||||
setNovaTarefa('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={novaTarefa}
|
||||
onChange={(e) => setNovaTarefa(e.target.value)}
|
||||
placeholder="Nova subtarefa"
|
||||
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
onKeyPress={(e) => e.key === 'Enter' && adicionarSubtarefa()}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={adicionarSubtarefa}
|
||||
className="p-2 text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/50 rounded-lg"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{subtarefas.map((subtarefa, index) => (
|
||||
<li
|
||||
key={subtarefa.id}
|
||||
className="flex items-center justify-between p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const novasSubtarefas = [...subtarefas];
|
||||
novasSubtarefas[index].concluida = !novasSubtarefas[index].concluida;
|
||||
onChange(novasSubtarefas);
|
||||
}}
|
||||
className={`h-5 w-5 rounded border ${
|
||||
subtarefa.concluida
|
||||
? 'bg-blue-600 border-blue-600 text-white'
|
||||
: 'border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{subtarefa.concluida && <CheckIcon className="h-4 w-4" />}
|
||||
</button>
|
||||
<span className={`text-gray-900 dark:text-white ${
|
||||
subtarefa.concluida ? 'line-through text-gray-500 dark:text-gray-400' : ''
|
||||
}`}>
|
||||
{subtarefa.titulo}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(subtarefas.filter(s => s.id !== subtarefa.id))}
|
||||
className="text-gray-400 hover:text-red-600 dark:text-gray-500 dark:hover:text-red-400"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { Tarefa } from '../../types/entities';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { PencilIcon, CheckIcon, XMarkIcon, ExclamationTriangleIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTarefas } from '../../hooks/useTarefas';
|
||||
|
||||
interface TarefaCardProps {
|
||||
tarefa: Tarefa;
|
||||
onEdit: (tarefa: Tarefa) => void;
|
||||
onStatusChange: (id: string, status: string, progresso?: number) => void;
|
||||
}
|
||||
|
||||
export const TarefaCard: React.FC<TarefaCardProps> = ({ tarefa, onEdit, onStatusChange }) => {
|
||||
const { user } = useAuth();
|
||||
const { deleteTarefa } = useTarefas();
|
||||
|
||||
const canDelete = user && ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user.perfil);
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!window.confirm('Tem certeza que deseja excluir esta tarefa?')) return;
|
||||
|
||||
try {
|
||||
await deleteTarefa.mutateAsync(tarefa.id);
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir tarefa:', error);
|
||||
alert('Erro ao excluir tarefa');
|
||||
}
|
||||
};
|
||||
|
||||
const getPrioridadeColor = () => {
|
||||
switch (tarefa.prioridade) {
|
||||
case 'BAIXA': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||
case 'MEDIA': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
||||
case 'ALTA': return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
|
||||
case 'URGENTE': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-700 rounded-lg shadow p-4">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-gray-900 dark:text-white font-medium">
|
||||
{tarefa.titulo}
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => onEdit(tarefa)}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
title="Editar tarefa"
|
||||
>
|
||||
<PencilIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${getPrioridadeColor()}`}>
|
||||
{tarefa.prioridade}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
|
||||
{tarefa.descricao}
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${tarefa.progresso}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between text-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex -space-x-2">
|
||||
{tarefa.subtarefas.map((subtarefa, index) => (
|
||||
<div
|
||||
key={subtarefa.id}
|
||||
className={`h-6 w-6 rounded-full flex items-center justify-center text-xs
|
||||
${subtarefa.concluida
|
||||
? 'bg-green-100 text-green-600 dark:bg-green-900 dark:text-green-200'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'}`}
|
||||
>
|
||||
{subtarefa.concluida ? <CheckIcon className="h-3 w-3" /> : index + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{format(new Date(tarefa.dataVencimento), "dd/MM/yyyy", { locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(tarefa);
|
||||
}}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex space-x-2">
|
||||
{tarefa.status !== 'CONCLUIDA' && tarefa.status !== 'CANCELADA' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onStatusChange(tarefa.id, 'CONCLUIDA', 100)}
|
||||
className="p-2 text-green-600 hover:bg-green-50 rounded-lg"
|
||||
title="Concluir tarefa"
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onStatusChange(tarefa.id, 'CANCELADA', 0)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg"
|
||||
title="Cancelar tarefa"
|
||||
>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const novoProgresso = prompt('Digite o percentual de conclusão (0-100):', tarefa.progresso?.toString());
|
||||
if (novoProgresso !== null) {
|
||||
const progresso = Math.min(100, Math.max(0, parseInt(novoProgresso)));
|
||||
onStatusChange(tarefa.id, 'EM_ANDAMENTO', progresso);
|
||||
}
|
||||
}}
|
||||
className="p-2 text-yellow-600 hover:bg-yellow-50 rounded-lg"
|
||||
title="Atualizar progresso"
|
||||
>
|
||||
<ExclamationTriangleIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg"
|
||||
title="Excluir tarefa"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { Tarefa } from '../../types/entities';
|
||||
import { TarefaCard } from './TarefaCard';
|
||||
import api from '../../services/api';
|
||||
|
||||
interface TarefaKanbanProps {
|
||||
onEdit: (tarefa: Tarefa) => void;
|
||||
onStatusChange: (id: string, status: string, progresso?: number) => void;
|
||||
}
|
||||
|
||||
const colunas = [
|
||||
{ id: 'PENDENTE', titulo: 'Pendentes' },
|
||||
{ id: 'EM_ANDAMENTO', titulo: 'Em Andamento' },
|
||||
{ id: 'CONCLUIDA', titulo: 'Concluídas' },
|
||||
];
|
||||
|
||||
export const TarefaKanban: React.FC<TarefaKanbanProps> = ({ onEdit, onStatusChange }) => {
|
||||
const { data: tarefas = [], refetch } = useQuery('tarefas', async () => {
|
||||
const response = await api.get('/tarefas');
|
||||
return response.data;
|
||||
});
|
||||
|
||||
const handleDragEnd = async (result: any) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const { draggableId, source, destination } = result;
|
||||
|
||||
if (source.droppableId !== destination.droppableId) {
|
||||
onStatusChange(draggableId, destination.droppableId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{colunas.map((coluna) => (
|
||||
<div key={coluna.id} className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
{coluna.titulo}
|
||||
</h3>
|
||||
|
||||
<Droppable droppableId={coluna.id}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className="space-y-4 min-h-[200px]"
|
||||
>
|
||||
{tarefas
|
||||
.filter((tarefa: Tarefa) => tarefa.status === coluna.id)
|
||||
.map((tarefa: Tarefa, index: number) => (
|
||||
<TarefaCard
|
||||
key={tarefa.id}
|
||||
tarefa={tarefa}
|
||||
index={index}
|
||||
onEdit={onEdit}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dialog } from '@headlessui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useQuery } from 'react-query';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Tarefa } from '../../types/entities';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { SubtarefasList } from './SubtarefasList';
|
||||
import { ComentariosList } from './ComentariosList';
|
||||
import api from '../../services/api';
|
||||
import { useTarefas } from '../../hooks/useTarefas';
|
||||
|
||||
interface TarefaModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
tarefa?: Tarefa | null;
|
||||
}
|
||||
|
||||
export const TarefaModal: React.FC<TarefaModalProps> = ({ isOpen, onClose, tarefa }) => {
|
||||
const [activeTab, setActiveTab] = useState('detalhes');
|
||||
const { register, handleSubmit, reset, setValue, watch } = useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { createTarefa, updateTarefa } = useTarefas();
|
||||
|
||||
// Buscar usuários para atribuição
|
||||
const { data: usuarios = [] } = useQuery('usuarios', async () => {
|
||||
const response = await api.get('/usuarios');
|
||||
return response.data;
|
||||
});
|
||||
|
||||
// Buscar processos ativos
|
||||
const { data: processos = [] } = useQuery('processos', async () => {
|
||||
const response = await api.get('/processos');
|
||||
return response.data;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (tarefa) {
|
||||
reset(tarefa);
|
||||
} else {
|
||||
reset({
|
||||
status: 'PENDENTE',
|
||||
prioridade: 'MEDIA',
|
||||
progresso: 0,
|
||||
etiquetas: [],
|
||||
subtarefas: [],
|
||||
comentarios: []
|
||||
});
|
||||
}
|
||||
}, [tarefa, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tarefa && isOpen) {
|
||||
reset({
|
||||
...tarefa,
|
||||
dataInicio: new Date(tarefa.dataInicio),
|
||||
dataVencimento: new Date(tarefa.dataVencimento)
|
||||
});
|
||||
}
|
||||
}, [tarefa, isOpen, reset]);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Preparar dados removendo campos que não devem ser enviados
|
||||
const dadosParaEnviar = {
|
||||
...data,
|
||||
responsavelId: data.responsavelId,
|
||||
// Remover campos que não devem ser enviados
|
||||
id: undefined,
|
||||
criadoEm: undefined,
|
||||
atualizadoEm: undefined
|
||||
};
|
||||
|
||||
if (tarefa) {
|
||||
// Para edição, enviar id separadamente
|
||||
await updateTarefa.mutateAsync({
|
||||
id: tarefa.id,
|
||||
data: dadosParaEnviar
|
||||
});
|
||||
} else {
|
||||
await createTarefa.mutateAsync(dadosParaEnviar);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar tarefa:', error);
|
||||
alert('Erro ao salvar tarefa. Por favor, tente novamente.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'detalhes', label: 'Detalhes' },
|
||||
{ id: 'subtarefas', label: 'Subtarefas' },
|
||||
{ id: 'comentarios', label: 'Comentários' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
|
||||
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm" />
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4">
|
||||
<Dialog.Panel className="w-full max-w-4xl bg-white dark:bg-gray-800 rounded-xl shadow-xl">
|
||||
{/* Header */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<Dialog.Title className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{tarefa ? 'Editar Tarefa' : 'Nova Tarefa'}
|
||||
</Dialog.Title>
|
||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex space-x-4 mt-4">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors
|
||||
${activeTab === tab.id
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'
|
||||
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-6">
|
||||
{/* Conteúdo das tabs */}
|
||||
<div className="space-y-6">
|
||||
{activeTab === 'detalhes' && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<input
|
||||
type="text"
|
||||
{...register('titulo')}
|
||||
placeholder="Título da tarefa"
|
||||
className="w-full px-4 py-2 text-xl font-medium border-0 border-b-2 border-gray-200 dark:border-gray-700 bg-transparent text-gray-900 dark:text-white focus:ring-0 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<textarea
|
||||
{...register('descricao')}
|
||||
rows={3}
|
||||
placeholder="Descrição detalhada"
|
||||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<select
|
||||
{...register('prioridade')}
|
||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="BAIXA">Baixa</option>
|
||||
<option value="MEDIA">Média</option>
|
||||
<option value="ALTA">Alta</option>
|
||||
<option value="URGENTE">Urgente</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
{...register('responsavelId')}
|
||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="">Selecione um responsável</option>
|
||||
{usuarios.map((usuario: any) => (
|
||||
<option key={usuario.id} value={usuario.id}>
|
||||
{usuario.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<DatePicker
|
||||
label="Data de Início"
|
||||
value={watch('dataInicio')}
|
||||
onChange={(date) => setValue('dataInicio', date)}
|
||||
className="w-full"
|
||||
slotProps={{ textField: { size: 'small' } }}
|
||||
/>
|
||||
|
||||
<DatePicker
|
||||
label="Data de Vencimento"
|
||||
value={watch('dataVencimento')}
|
||||
onChange={(date) => setValue('dataVencimento', date)}
|
||||
className="w-full"
|
||||
slotProps={{ textField: { size: 'small' } }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'subtarefas' && (
|
||||
<SubtarefasList
|
||||
subtarefas={watch('subtarefas') || []}
|
||||
onChange={(subtarefas) => setValue('subtarefas', subtarefas)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'comentarios' && (
|
||||
<ComentariosList
|
||||
comentarios={watch('comentarios') || []}
|
||||
onChange={(comentarios) => setValue('comentarios', comentarios)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Salvando...' : tarefa ? 'Salvar' : 'Criar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Tarefa } from '../../types/entities';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
|
||||
import api from '../../services/api';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTarefas } from '../../hooks/useTarefas';
|
||||
|
||||
interface TarefasListProps {
|
||||
onEdit: (tarefa: Tarefa) => void;
|
||||
onStatusChange: (id: string, status: string, progresso?: number) => void;
|
||||
}
|
||||
|
||||
export const TarefasList: React.FC<TarefasListProps> = ({ onEdit, onStatusChange }) => {
|
||||
const { user } = useAuth();
|
||||
const { tarefas, isLoading, deleteTarefa } = useTarefas();
|
||||
|
||||
const getPrioridadeColor = (prioridade: string) => {
|
||||
const colors = {
|
||||
'BAIXA': 'bg-green-100 text-green-800',
|
||||
'MEDIA': 'bg-yellow-100 text-yellow-800',
|
||||
'ALTA': 'bg-orange-100 text-orange-800',
|
||||
'URGENTE': 'bg-red-100 text-red-800'
|
||||
};
|
||||
return colors[prioridade as keyof typeof colors] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
'PENDENTE': 'bg-gray-100 text-gray-800',
|
||||
'EM_ANDAMENTO': 'bg-blue-100 text-blue-800',
|
||||
'CONCLUIDA': 'bg-green-100 text-green-800',
|
||||
'CANCELADA': 'bg-red-100 text-red-800'
|
||||
};
|
||||
return colors[status as keyof typeof colors] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
const canDelete = user && ['ADVOGADO_ADMIN', 'ADVOGADO'].includes(user.perfil);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm('Tem certeza que deseja excluir esta tarefa?')) return;
|
||||
|
||||
try {
|
||||
await deleteTarefa.mutateAsync(id);
|
||||
// Não precisa fazer nada aqui, o React Query vai atualizar automaticamente
|
||||
} catch (error) {
|
||||
console.error('Erro ao excluir tarefa:', error);
|
||||
alert('Erro ao excluir tarefa');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Título/Descrição
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Prioridade
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Vencimento
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Progresso
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Ações</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{tarefas.map((tarefa: Tarefa) => (
|
||||
<tr key={tarefa.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{tarefa.titulo}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{tarefa.descricao}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${getStatusColor(tarefa.status)}`}>
|
||||
{tarefa.status.replace('_', ' ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${getPrioridadeColor(tarefa.prioridade)}`}>
|
||||
{tarefa.prioridade}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{format(new Date(tarefa.dataVencimento), 'dd/MM/yyyy', { locale: ptBR })}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mb-1">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full"
|
||||
style={{ width: `${tarefa.progresso}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{tarefa.progresso}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => onEdit(tarefa)}
|
||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
|
||||
>
|
||||
<PencilIcon className="h-5 w-5" />
|
||||
</button>
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={() => handleDelete(tarefa.id)}
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TarefasList } from './TarefasList';
|
||||
import { TarefaModal } from './TarefaModal';
|
||||
import { TarefaKanban } from './TarefaKanban';
|
||||
import { ListBulletIcon, Squares2X2Icon } from '@heroicons/react/24/outline';
|
||||
import { useTarefas } from '../../hooks/useTarefas';
|
||||
|
||||
export const TarefasPage: React.FC = () => {
|
||||
const { updateStatus } = useTarefas();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
|
||||
const [selectedTarefa, setSelectedTarefa] = useState(null);
|
||||
|
||||
const handleStatusChange = async (id: string, status: string, progresso?: number) => {
|
||||
try {
|
||||
await updateStatus.mutateAsync({ id, status, progresso });
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
Tarefas
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Toggle de visualização */}
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`px-3 py-2 rounded-md ${
|
||||
viewMode === 'list'
|
||||
? 'bg-white dark:bg-gray-700 shadow'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<ListBulletIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('kanban')}
|
||||
className={`px-3 py-2 rounded-md ${
|
||||
viewMode === 'kanban'
|
||||
? 'bg-white dark:bg-gray-700 shadow'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
<Squares2X2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTarefa(null);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 dark:hover:bg-blue-500"
|
||||
>
|
||||
Nova Tarefa
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visualização alternada entre lista e kanban */}
|
||||
{viewMode === 'list' ? (
|
||||
<TarefasList
|
||||
onEdit={(tarefa) => {
|
||||
setSelectedTarefa(tarefa);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
onStatusChange={handleStatusChange}
|
||||
/>
|
||||
) : (
|
||||
<TarefaKanban
|
||||
onEdit={(tarefa) => {
|
||||
setSelectedTarefa(tarefa);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
onStatusChange={handleStatusChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TarefaModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedTarefa(null);
|
||||
}}
|
||||
tarefa={selectedTarefa}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user