feat: Initialize ComFi project with Vite
Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
This commit is contained in:
498
components/KanbanView.tsx
Normal file
498
components/KanbanView.tsx
Normal file
@@ -0,0 +1,498 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, MoreHorizontal, Clock, UserCircle, X, DollarSign, GripVertical, Trash2, Building2 } from 'lucide-react';
|
||||
import { KanbanColumn, KanbanTask, Company, Receivable } from '../types';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { CustomSelect } from './CustomSelect';
|
||||
|
||||
const initialColumns: KanbanColumn[] = [
|
||||
{
|
||||
id: 'todo',
|
||||
title: 'A Fazer',
|
||||
tasks: [
|
||||
{ id: 't1', title: 'Criar contrato Uda Studios', priority: 'high', dueDate: '2024-05-15', value: 12000, description: 'Negociação referente ao projeto de redesign completo.' },
|
||||
{ id: 't2', title: 'Revisar balanço trimestral', priority: 'medium', dueDate: '2024-05-20', value: 0, description: 'Verificar lançamentos de março e abril.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'progress',
|
||||
title: 'Em Progresso',
|
||||
tasks: [
|
||||
{ id: 't3', title: 'Design do Dashboard', priority: 'high', dueDate: '2024-05-18', value: 5000 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
title: 'Revisão',
|
||||
tasks: [
|
||||
{ id: 't4', title: 'Aprovação de Orçamento', priority: 'low', dueDate: '2024-05-12', value: 3500 },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
title: 'Concluído',
|
||||
tasks: [
|
||||
{ id: 't5', title: 'Onboarding Angels Healthcare', priority: 'medium', dueDate: '2024-05-10', value: 15000 },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
interface KanbanViewProps {
|
||||
companies: Company[];
|
||||
onAddReceivable: (receivable: Receivable) => void;
|
||||
}
|
||||
|
||||
export const KanbanView: React.FC<KanbanViewProps> = ({ companies, onAddReceivable }) => {
|
||||
const { addToast } = useToast();
|
||||
const [columns, setColumns] = useState<KanbanColumn[]>(initialColumns);
|
||||
|
||||
// States para Modais
|
||||
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
||||
const [isColumnModalOpen, setIsColumnModalOpen] = useState(false);
|
||||
|
||||
// State de Edição/Criação
|
||||
const [currentTask, setCurrentTask] = useState<Partial<KanbanTask>>({});
|
||||
const [currentColumnId, setCurrentColumnId] = useState<string>('todo');
|
||||
const [newColumnTitle, setNewColumnTitle] = useState('');
|
||||
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
||||
const [draggedSourceColumnId, setDraggedSourceColumnId] = useState<string | null>(null);
|
||||
|
||||
// --- Drag and Drop Logic ---
|
||||
|
||||
const handleDragStart = (e: React.DragEvent, taskId: string, columnId: string) => {
|
||||
setDraggedTaskId(taskId);
|
||||
setDraggedSourceColumnId(columnId);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault(); // Necessary to allow dropping
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, targetColumnId: string) => {
|
||||
e.preventDefault();
|
||||
if (!draggedTaskId || !draggedSourceColumnId) return;
|
||||
|
||||
if (draggedSourceColumnId === targetColumnId) {
|
||||
setDraggedTaskId(null);
|
||||
setDraggedSourceColumnId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move logic
|
||||
const sourceCol = columns.find(c => c.id === draggedSourceColumnId);
|
||||
const targetCol = columns.find(c => c.id === targetColumnId);
|
||||
const taskToMove = sourceCol?.tasks.find(t => t.id === draggedTaskId);
|
||||
|
||||
if (sourceCol && targetCol && taskToMove) {
|
||||
const newColumns = columns.map(col => {
|
||||
if (col.id === draggedSourceColumnId) {
|
||||
return { ...col, tasks: col.tasks.filter(t => t.id !== draggedTaskId) };
|
||||
}
|
||||
if (col.id === targetColumnId) {
|
||||
return { ...col, tasks: [...col.tasks, taskToMove] };
|
||||
}
|
||||
return col;
|
||||
});
|
||||
setColumns(newColumns);
|
||||
|
||||
// INTEGRAÇÃO FINANCEIRA: Se moveu para "Concluído" (done), tem valor e cliente, sugere faturar
|
||||
if (targetColumnId === 'done' && taskToMove.value && taskToMove.value > 0 && taskToMove.clientId) {
|
||||
const client = companies.find(c => c.id === taskToMove.clientId);
|
||||
|
||||
// Use toast with action instead of window.confirm for better UI?
|
||||
// For now, simpler confirmation but using toast for success
|
||||
if (window.confirm(`A tarefa "${taskToMove.title}" foi concluída.\n\nDeseja gerar automaticamente uma Conta a Receber no valor de R$ ${taskToMove.value.toLocaleString('pt-BR')}?`)) {
|
||||
|
||||
const newReceivable: Receivable = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
description: taskToMove.title,
|
||||
companyName: client?.fantasyName || client?.name || 'Cliente Kanban',
|
||||
category: 'Serviços', // Default category
|
||||
value: taskToMove.value,
|
||||
dueDate: taskToMove.dueDate || new Date().toISOString().split('T')[0],
|
||||
status: 'pending',
|
||||
type: 'one-time'
|
||||
};
|
||||
|
||||
onAddReceivable(newReceivable);
|
||||
addToast({ type: 'success', title: 'Faturamento Gerado', message: `Conta a receber criada para ${client?.fantasyName}.` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDraggedTaskId(null);
|
||||
setDraggedSourceColumnId(null);
|
||||
};
|
||||
|
||||
// --- CRUD Logic ---
|
||||
|
||||
const openNewTaskModal = () => {
|
||||
setCurrentTask({ priority: 'medium', dueDate: new Date().toISOString().split('T')[0], value: 0, description: '' });
|
||||
setCurrentColumnId(columns[0].id); // Default to first column
|
||||
setIsTaskModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditTaskModal = (task: KanbanTask, colId: string) => {
|
||||
setCurrentTask(task);
|
||||
setCurrentColumnId(colId);
|
||||
setIsTaskModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveTask = () => {
|
||||
if (!currentTask.title) {
|
||||
addToast({ type: 'warning', title: 'Título Obrigatório', message: 'Dê um nome para a tarefa.' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if updating existing or creating new
|
||||
if (currentTask.id) {
|
||||
// Logic to update existing task (potentially moving columns)
|
||||
const newColumns = columns.map(col => {
|
||||
// Remove from all columns first (in case it moved)
|
||||
const filteredTasks = col.tasks.filter(t => t.id !== currentTask.id);
|
||||
|
||||
// If this is the target column, add the updated task
|
||||
if (col.id === currentColumnId) {
|
||||
return { ...col, tasks: [...filteredTasks, currentTask as KanbanTask] };
|
||||
}
|
||||
return { ...col, tasks: filteredTasks };
|
||||
});
|
||||
setColumns(newColumns);
|
||||
addToast({ type: 'success', title: 'Tarefa Atualizada' });
|
||||
} else {
|
||||
// Create new
|
||||
const newTask: KanbanTask = {
|
||||
...currentTask,
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
} as KanbanTask;
|
||||
|
||||
const newColumns = columns.map(col => {
|
||||
if (col.id === currentColumnId) {
|
||||
return { ...col, tasks: [newTask, ...col.tasks] }; // Add to top
|
||||
}
|
||||
return col;
|
||||
});
|
||||
setColumns(newColumns);
|
||||
addToast({ type: 'success', title: 'Tarefa Criada' });
|
||||
}
|
||||
setIsTaskModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteTask = () => {
|
||||
if (!currentTask.id) return;
|
||||
if (window.confirm('Tem certeza que deseja excluir esta tarefa?')) {
|
||||
const newColumns = columns.map(col => ({
|
||||
...col,
|
||||
tasks: col.tasks.filter(t => t.id !== currentTask.id)
|
||||
}));
|
||||
setColumns(newColumns);
|
||||
setIsTaskModalOpen(false);
|
||||
addToast({ type: 'info', title: 'Tarefa Excluída' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateColumn = () => {
|
||||
if (!newColumnTitle) return;
|
||||
const newCol: KanbanColumn = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
title: newColumnTitle,
|
||||
tasks: []
|
||||
};
|
||||
setColumns([...columns, newCol]);
|
||||
setNewColumnTitle('');
|
||||
setIsColumnModalOpen(false);
|
||||
addToast({ type: 'success', title: 'Coluna Adicionada' });
|
||||
};
|
||||
|
||||
// Styles for Inputs (High Contrast)
|
||||
const labelClass = "block text-sm font-bold text-slate-800 mb-1";
|
||||
const inputClass = "w-full p-2.5 bg-white border border-slate-300 rounded-xl text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-shadow";
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col animate-fade-in relative">
|
||||
|
||||
{/* Task Modal (Create & Edit) */}
|
||||
{isTaskModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsTaskModalOpen(false)}></div>
|
||||
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-lg p-0 animate-scale-up flex flex-col max-h-[90vh]">
|
||||
<div className="flex justify-between items-center px-6 py-4 border-b border-slate-100 bg-slate-50 flex-shrink-0">
|
||||
<h3 className="font-bold text-slate-800 text-lg">
|
||||
{currentTask.id ? 'Detalhes da Tarefa' : 'Nova Tarefa'}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{currentTask.id && (
|
||||
<button onClick={handleDeleteTask} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setIsTaskModalOpen(false)} className="p-2 text-slate-400 hover:text-slate-600 rounded-lg transition-colors">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-5 overflow-y-auto">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className={labelClass}>Título da Tarefa</label>
|
||||
<input
|
||||
type="text"
|
||||
className={inputClass}
|
||||
value={currentTask.title || ''}
|
||||
onChange={e => setCurrentTask({...currentTask, title: e.target.value})}
|
||||
placeholder="Ex: Reunião com Cliente X"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status & Priority Row */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Coluna (Status)</label>
|
||||
<CustomSelect
|
||||
value={currentColumnId}
|
||||
onChange={(val) => setCurrentColumnId(val)}
|
||||
options={columns.map(col => ({ value: col.id, label: col.title }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelClass}>Prioridade</label>
|
||||
<CustomSelect
|
||||
value={currentTask.priority || 'medium'}
|
||||
onChange={(val) => setCurrentTask({...currentTask, priority: val})}
|
||||
options={[
|
||||
{ value: 'low', label: 'Baixa' },
|
||||
{ value: 'medium', label: 'Média' },
|
||||
{ value: 'high', label: 'Alta' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CRM Linkage */}
|
||||
<div>
|
||||
<label className={labelClass}>Vincular Cliente (CRM)</label>
|
||||
<CustomSelect
|
||||
value={currentTask.clientId || ''}
|
||||
onChange={(val) => setCurrentTask({...currentTask, clientId: val})}
|
||||
placeholder="Sem vínculo"
|
||||
icon={<Building2 size={16}/>}
|
||||
options={[
|
||||
{ value: '', label: 'Sem vínculo' },
|
||||
...companies.map(c => ({ value: c.id, label: c.fantasyName || c.name }))
|
||||
]}
|
||||
/>
|
||||
<p className="text-[10px] text-slate-400 mt-1">Ao concluir a tarefa, o sistema oferecerá gerar cobrança para este cliente.</p>
|
||||
</div>
|
||||
|
||||
{/* Commercial Data Section */}
|
||||
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 space-y-4">
|
||||
<h4 className="font-bold text-slate-700 text-sm border-b border-slate-200 pb-2 mb-2 flex items-center gap-2">
|
||||
Dados Comerciais & Agenda
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className={labelClass}>Valor da Negociação</label>
|
||||
<div className="relative">
|
||||
<DollarSign size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
type="number"
|
||||
className={`${inputClass} pl-9`}
|
||||
value={currentTask.value || ''}
|
||||
onChange={e => setCurrentTask({...currentTask, value: Number(e.target.value)})}
|
||||
placeholder="0,00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Prazo / Data</label>
|
||||
<div className="relative">
|
||||
<Clock size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
type="date"
|
||||
className={`${inputClass} pl-10`}
|
||||
value={currentTask.dueDate || ''}
|
||||
onChange={e => setCurrentTask({...currentTask, dueDate: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass}>Descrição / Notas</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
className={inputClass}
|
||||
value={currentTask.description || ''}
|
||||
onChange={e => setCurrentTask({...currentTask, description: e.target.value})}
|
||||
placeholder="Detalhes sobre a negociação, pauta da reunião, etc..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex justify-end gap-3 flex-shrink-0 rounded-b-2xl">
|
||||
<button onClick={() => setIsTaskModalOpen(false)} className="px-4 py-2 text-slate-600 font-medium hover:bg-slate-200 rounded-xl transition-colors">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveTask}
|
||||
className="px-6 py-2 bg-primary-500 text-white font-bold rounded-xl hover:bg-primary-600 shadow-lg shadow-primary-200/50 transition-all"
|
||||
>
|
||||
{currentTask.id ? 'Salvar Alterações' : 'Criar Tarefa'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New Column Modal */}
|
||||
{isColumnModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsColumnModalOpen(false)}></div>
|
||||
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-sm p-6 animate-scale-up">
|
||||
<h3 className="font-bold text-slate-800 text-lg mb-4">Nova Coluna</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className={labelClass}>Nome da Coluna</label>
|
||||
<input
|
||||
type="text"
|
||||
className={inputClass}
|
||||
value={newColumnTitle}
|
||||
onChange={e => setNewColumnTitle(e.target.value)}
|
||||
placeholder="Ex: Em Aprovação"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setIsColumnModalOpen(false)} className="px-4 py-2 text-slate-500 font-medium hover:bg-slate-100 rounded-xl">Cancelar</button>
|
||||
<button onClick={handleCreateColumn} className="px-4 py-2 bg-primary-500 text-white font-bold rounded-xl hover:bg-primary-600">Criar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kanban Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">Projetos & Tarefas</h1>
|
||||
<p className="text-slate-500">Gestão visual do fluxo de trabalho.</p>
|
||||
</div>
|
||||
<button onClick={openNewTaskModal} className="flex items-center gap-2 px-5 py-3 bg-primary-500 text-white rounded-xl shadow-lg shadow-primary-200/50 hover:bg-primary-600 font-bold transition-all">
|
||||
<Plus size={20} /> Nova Tarefa
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Kanban Board Area */}
|
||||
<div className="flex-1 overflow-x-auto">
|
||||
<div className="flex gap-6 h-full pb-4">
|
||||
{columns.map(col => {
|
||||
// Calculate total value for the column
|
||||
const totalValue = col.tasks.reduce((sum, task) => sum + (task.value || 0), 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={col.id}
|
||||
className="flex-shrink-0 flex flex-col w-[300px]"
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, col.id)}
|
||||
>
|
||||
{/* Column Header */}
|
||||
<div className="flex justify-between items-start mb-4 p-1">
|
||||
<div>
|
||||
<h3 className="font-bold text-slate-700 flex items-center gap-2 truncate">
|
||||
{col.title}
|
||||
<span className="bg-slate-200 text-slate-600 text-[10px] px-2 py-0.5 rounded-full">{col.tasks.length}</span>
|
||||
</h3>
|
||||
{totalValue > 0 && (
|
||||
<div className="text-xs font-bold text-slate-400 mt-1 pl-1">
|
||||
Total: R$ {totalValue.toLocaleString('pt-BR')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="text-slate-400 hover:text-slate-600"><MoreHorizontal size={18} /></button>
|
||||
</div>
|
||||
|
||||
{/* Tasks Container */}
|
||||
<div className="bg-slate-100/50 rounded-2xl p-2 flex-1 border border-slate-100/50 overflow-y-auto">
|
||||
<div className="space-y-3 min-h-[50px]">
|
||||
{col.tasks.map(task => (
|
||||
<div
|
||||
key={task.id}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, task.id, col.id)}
|
||||
onClick={() => openEditTaskModal(task, col.id)}
|
||||
className={`bg-white p-4 rounded-xl shadow-sm border border-slate-100 hover:shadow-md hover:border-primary-200 cursor-pointer transition-all group relative ${draggedTaskId === task.id ? 'opacity-50 border-dashed border-slate-400' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className={`text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded ${
|
||||
task.priority === 'high' ? 'bg-red-50 text-red-600' :
|
||||
task.priority === 'medium' ? 'bg-amber-50 text-amber-600' :
|
||||
'bg-blue-50 text-blue-600'
|
||||
}`}>
|
||||
{task.priority === 'high' ? 'Alta' : task.priority === 'medium' ? 'Média' : 'Baixa'}
|
||||
</span>
|
||||
<div className="opacity-0 group-hover:opacity-100 text-slate-300 cursor-grab active:cursor-grabbing">
|
||||
<GripVertical size={16}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="font-bold text-slate-800 text-sm mb-3 line-clamp-2">{task.title}</h4>
|
||||
|
||||
{/* Optional: Show Value if present */}
|
||||
{task.value && task.value > 0 && (
|
||||
<div className="mb-3 text-xs font-semibold text-slate-600 bg-slate-50 px-2 py-1 rounded inline-block">
|
||||
R$ {task.value.toLocaleString('pt-BR')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show Linked Client Badge */}
|
||||
{task.clientId && (
|
||||
<div className="mb-3 flex items-center gap-1 text-[10px] text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded w-fit">
|
||||
<Building2 size={10} /> {companies.find(c => c.id === task.clientId)?.fantasyName || 'Cliente'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center pt-3 border-t border-slate-50">
|
||||
<div className="flex -space-x-2">
|
||||
<div className="w-6 h-6 rounded-full bg-slate-200 border-2 border-white flex items-center justify-center text-[10px] font-bold text-slate-500">
|
||||
<UserCircle size={16} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-slate-400">
|
||||
<Clock size={12} /> {task.dueDate ? new Date(task.dueDate).toLocaleDateString('pt-BR', {day:'2-digit', month:'short'}) : 'Sem data'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button onClick={openNewTaskModal} className="w-full py-2 text-slate-400 text-sm font-medium hover:bg-slate-200/50 rounded-xl transition-colors flex items-center justify-center gap-2 border border-transparent hover:border-slate-200/50">
|
||||
<Plus size={16} /> Adicionar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add Column Button */}
|
||||
<div className="flex-shrink-0 w-[50px] pt-10">
|
||||
<button
|
||||
onClick={() => setIsColumnModalOpen(true)}
|
||||
className="w-full h-[50px] bg-white border border-dashed border-slate-300 rounded-2xl flex items-center justify-center text-slate-400 hover:text-primary-500 hover:border-primary-300 hover:bg-primary-50 transition-all group tooltip-container"
|
||||
title="Nova Coluna"
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user