first commit

This commit is contained in:
rodrigo committed 2025-04-10 23:07:21 -03:00
commit c9fb67e751
423 files changed
+53287

No files matched your search

+94
View File
@@ -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>
);
};
+88
View File
@@ -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>
);
};
+152
View File
@@ -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>
);
};
+71
View File
@@ -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>
);
};
+241
View File
@@ -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>
);
};
+140
View File
@@ -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>
);
};
+95
View File
@@ -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>
);
};