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.
467 lines
22 KiB
TypeScript
467 lines
22 KiB
TypeScript
|
|
import React, { useState, useMemo } from 'react';
|
|
import { ChevronLeft, ChevronRight, Clock, CheckCircle2, AlertCircle, Plus, X, Calendar as CalendarIcon, Trash2, List, Grid, DollarSign, ChevronDown } from 'lucide-react';
|
|
import { CalendarEvent, Expense, Receivable } from '../types';
|
|
import { useToast } from '../contexts/ToastContext';
|
|
import { CustomSelect } from './CustomSelect';
|
|
|
|
interface CalendarViewProps {
|
|
expenses?: Expense[];
|
|
receivables?: Receivable[];
|
|
}
|
|
|
|
const initialManualEvents: CalendarEvent[] = [
|
|
{ id: '1', title: 'Reunião Uda Studios', date: new Date().toISOString().split('T')[0], type: 'meeting', completed: false, description: 'Alinhamento mensal sobre o progresso do projeto de redesign.' },
|
|
];
|
|
|
|
export const CalendarView: React.FC<CalendarViewProps> = ({ expenses = [], receivables = [] }) => {
|
|
const { addToast } = useToast();
|
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
const [manualEvents, setManualEvents] = useState<CalendarEvent[]>(initialManualEvents);
|
|
|
|
// Combine Manual Events with Financial Data
|
|
const events = useMemo(() => {
|
|
const expenseEvents: CalendarEvent[] = expenses.map(e => ({
|
|
id: `exp-${e.id}`,
|
|
title: `Pagar: ${e.title}`,
|
|
date: e.dueDate,
|
|
type: 'payment',
|
|
description: `Valor: R$ ${e.amount.toLocaleString('pt-BR')} - Categoria: ${e.category} - Status: ${e.status === 'paid' ? 'Pago' : 'Pendente'}`,
|
|
completed: e.status === 'paid'
|
|
}));
|
|
|
|
const receivableEvents: CalendarEvent[] = receivables.map(r => ({
|
|
id: `rec-${r.id}`,
|
|
title: `Receber: ${r.description}`,
|
|
date: r.dueDate,
|
|
type: 'deadline', // Usaremos deadline logicamente, mas com cor especial visualmente
|
|
description: `Valor: R$ ${r.value.toLocaleString('pt-BR')} - Cliente: ${r.companyName} - Status: ${r.status === 'paid' ? 'Recebido' : 'Pendente'}`,
|
|
completed: r.status === 'paid'
|
|
}));
|
|
|
|
return [...manualEvents, ...expenseEvents, ...receivableEvents];
|
|
}, [manualEvents, expenses, receivables]);
|
|
|
|
// View State
|
|
const [viewMode, setViewMode] = useState<'month' | 'agenda'>('month');
|
|
|
|
// Create Modal State
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [newEvent, setNewEvent] = useState<Partial<CalendarEvent>>({ type: 'meeting', date: new Date().toISOString().split('T')[0] });
|
|
|
|
// Detail Modal State
|
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
|
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
|
|
|
const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate();
|
|
const getFirstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay();
|
|
|
|
const daysInMonth = getDaysInMonth(currentDate.getFullYear(), currentDate.getMonth());
|
|
const firstDay = getFirstDayOfMonth(currentDate.getFullYear(), currentDate.getMonth());
|
|
|
|
// --- Actions ---
|
|
|
|
const handleCreateEvent = () => {
|
|
if (!newEvent.title || !newEvent.date) return;
|
|
const event: CalendarEvent = {
|
|
...newEvent,
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
completed: false
|
|
} as CalendarEvent;
|
|
setManualEvents([...manualEvents, event]);
|
|
setIsModalOpen(false);
|
|
setNewEvent({ type: 'meeting', date: new Date().toISOString().split('T')[0] });
|
|
addToast({ type: 'success', title: 'Evento Criado', message: 'Agendamento salvo com sucesso.' });
|
|
};
|
|
|
|
const handleEventClick = (event: CalendarEvent) => {
|
|
setSelectedEvent(event);
|
|
setIsDetailModalOpen(true);
|
|
};
|
|
|
|
const handleDeleteEvent = () => {
|
|
if (!selectedEvent) return;
|
|
// Prevent deleting financial data from calendar
|
|
if (selectedEvent.id.startsWith('exp-') || selectedEvent.id.startsWith('rec-')) {
|
|
addToast({ type: 'warning', title: 'Ação Bloqueada', message: "Para excluir este registro, acesse o módulo Financeiro." });
|
|
return;
|
|
}
|
|
|
|
if (window.confirm('Deseja excluir este evento?')) {
|
|
setManualEvents(manualEvents.filter(e => e.id !== selectedEvent.id));
|
|
setIsDetailModalOpen(false);
|
|
setSelectedEvent(null);
|
|
addToast({ type: 'info', title: 'Evento Removido' });
|
|
}
|
|
};
|
|
|
|
const handleToggleComplete = () => {
|
|
if (!selectedEvent) return;
|
|
|
|
// Prevent modifying financial data status from calendar (simplification for now)
|
|
if (selectedEvent.id.startsWith('exp-') || selectedEvent.id.startsWith('rec-')) {
|
|
addToast({ type: 'warning', title: 'Ação Bloqueada', message: "Para baixar este pagamento, vá ao módulo Financeiro." });
|
|
return;
|
|
}
|
|
|
|
const updatedEvents = manualEvents.map(e =>
|
|
e.id === selectedEvent.id ? { ...e, completed: !e.completed } : e
|
|
);
|
|
setManualEvents(updatedEvents);
|
|
setSelectedEvent({ ...selectedEvent, completed: !selectedEvent.completed });
|
|
addToast({ type: 'success', title: selectedEvent.completed ? 'Reaberto' : 'Concluído', message: 'Status do evento atualizado.' });
|
|
};
|
|
|
|
const getEventStyle = (event: CalendarEvent) => {
|
|
if (event.id.startsWith('rec-')) {
|
|
return 'bg-green-50 border-green-400 text-green-700';
|
|
}
|
|
if (event.type === 'payment' || event.id.startsWith('exp-')) {
|
|
return 'bg-red-50 border-red-400 text-red-700';
|
|
}
|
|
if (event.type === 'deadline') {
|
|
return 'bg-amber-50 border-amber-400 text-amber-700';
|
|
}
|
|
return 'bg-blue-50 border-blue-400 text-blue-700';
|
|
};
|
|
|
|
const getEventTypeLabel = (event: CalendarEvent) => {
|
|
if (event.id.startsWith('rec-')) return 'Recebimento';
|
|
if (event.type === 'payment' || event.id.startsWith('exp-')) return 'Pagamento';
|
|
if (event.type === 'deadline') return 'Prazo';
|
|
return 'Reunião';
|
|
};
|
|
|
|
// --- Render Helpers ---
|
|
|
|
const handlePrevMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
|
|
const handleNextMonth = () => setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
|
|
|
|
const days = [];
|
|
// Empty slots
|
|
for (let i = 0; i < firstDay; i++) {
|
|
days.push(<div key={`empty-${i}`} className="h-32 bg-slate-50/50 border border-slate-100" />);
|
|
}
|
|
|
|
const todayStr = new Date().toISOString().split('T')[0];
|
|
|
|
// Days
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
const dayEvents = events.filter(e => e.date === dateStr);
|
|
const isToday = dateStr === todayStr;
|
|
|
|
days.push(
|
|
<div key={d} className={`h-32 border border-slate-100 p-2 transition-colors hover:bg-slate-50 ${isToday ? 'bg-orange-50/30' : 'bg-white'}`}>
|
|
<div className="flex justify-between items-start mb-2">
|
|
<span className={`text-sm font-bold w-7 h-7 flex items-center justify-center rounded-full ${isToday ? 'bg-primary-500 text-white' : 'text-slate-700'}`}>
|
|
{d}
|
|
</span>
|
|
{dayEvents.length > 0 && <span className="text-[10px] bg-slate-100 px-1.5 rounded text-slate-500">{dayEvents.length}</span>}
|
|
</div>
|
|
<div className="space-y-1 overflow-y-auto max-h-[80px] scrollbar-thin">
|
|
{dayEvents.map(ev => (
|
|
<div
|
|
key={ev.id}
|
|
onClick={(e) => { e.stopPropagation(); handleEventClick(ev); }}
|
|
className={`text-[10px] px-2 py-1 rounded border-l-2 truncate cursor-pointer transition-all hover:brightness-95 ${getEventStyle(ev)} ${ev.completed ? 'opacity-50 line-through' : ''}`}>
|
|
{ev.title}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Upcoming Deadlines Logic: Filter events from "today" onwards
|
|
const upcomingEvents = events
|
|
.filter(e => e.date >= todayStr && !e.completed)
|
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
|
|
const sidebarEvents = upcomingEvents.slice(0, 5);
|
|
|
|
// Styles for Inputs
|
|
const labelClass = "block text-sm font-bold text-slate-800 mb-1";
|
|
const inputClass = "w-full p-3 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="flex flex-col xl:flex-row gap-6 h-[calc(100vh-140px)] animate-fade-in relative">
|
|
|
|
{/* Create Event Modal */}
|
|
{isModalOpen && (
|
|
<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={() => setIsModalOpen(false)}></div>
|
|
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-md p-6 animate-scale-up flex flex-col max-h-[90vh]">
|
|
<div className="flex justify-between items-center mb-6 flex-shrink-0">
|
|
<h3 className="font-bold text-slate-800 text-lg">Novo Evento</h3>
|
|
<button onClick={() => setIsModalOpen(false)}><X size={20} className="text-slate-400 hover:text-slate-600"/></button>
|
|
</div>
|
|
<div className="space-y-4 overflow-y-auto">
|
|
<div>
|
|
<label className={labelClass}>Título</label>
|
|
<input
|
|
type="text"
|
|
className={inputClass}
|
|
value={newEvent.title || ''}
|
|
onChange={e => setNewEvent({...newEvent, title: e.target.value})}
|
|
placeholder="Ex: Reunião com Cliente"
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className={labelClass}>Data</label>
|
|
<input
|
|
type="date"
|
|
className={inputClass}
|
|
value={newEvent.date}
|
|
onChange={e => setNewEvent({...newEvent, date: e.target.value})}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Tipo</label>
|
|
<CustomSelect
|
|
value={newEvent.type || 'meeting'}
|
|
onChange={(val) => setNewEvent({...newEvent, type: val})}
|
|
options={[
|
|
{ value: 'meeting', label: 'Reunião' },
|
|
{ value: 'deadline', label: 'Prazo / Tarefa' }
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Descrição</label>
|
|
<textarea
|
|
className={inputClass}
|
|
rows={3}
|
|
value={newEvent.description || ''}
|
|
onChange={e => setNewEvent({...newEvent, description: e.target.value})}
|
|
placeholder="Detalhes adicionais..."
|
|
/>
|
|
</div>
|
|
<button onClick={handleCreateEvent} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl mt-2 hover:bg-primary-600 transition-colors shadow-lg shadow-primary-200/50">
|
|
Salvar Evento
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Detail Event Modal */}
|
|
{isDetailModalOpen && selectedEvent && (
|
|
<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={() => setIsDetailModalOpen(false)}></div>
|
|
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-scale-up">
|
|
<div className={`h-2 w-full ${selectedEvent.id.startsWith('rec-') ? 'bg-green-500' : selectedEvent.type === 'payment' ? 'bg-red-500' : selectedEvent.type === 'deadline' ? 'bg-amber-500' : 'bg-blue-500'}`} />
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div>
|
|
<span className={`inline-block px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider mb-2 ${
|
|
selectedEvent.id.startsWith('rec-') ? 'bg-green-50 text-green-600' :
|
|
(selectedEvent.type === 'payment' || selectedEvent.id.startsWith('exp-')) ? 'bg-red-50 text-red-600' :
|
|
selectedEvent.type === 'deadline' ? 'bg-amber-50 text-amber-600' :
|
|
'bg-blue-50 text-blue-600'
|
|
}`}>
|
|
{getEventTypeLabel(selectedEvent)}
|
|
</span>
|
|
<h3 className={`font-bold text-xl text-slate-800 ${selectedEvent.completed ? 'line-through opacity-50' : ''}`}>
|
|
{selectedEvent.title}
|
|
</h3>
|
|
</div>
|
|
<button onClick={() => setIsDetailModalOpen(false)}><X size={20} className="text-slate-400 hover:text-slate-600"/></button>
|
|
</div>
|
|
|
|
<div className="space-y-4 mb-8">
|
|
<div className="flex items-center gap-3 text-slate-600">
|
|
<CalendarIcon size={18} className="text-slate-400"/>
|
|
<span className="font-medium">
|
|
{new Date(selectedEvent.date + 'T12:00:00').toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}
|
|
</span>
|
|
</div>
|
|
{selectedEvent.description ? (
|
|
<div className="bg-slate-50 p-4 rounded-xl text-sm text-slate-600 border border-slate-100 whitespace-pre-line">
|
|
{selectedEvent.description}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-slate-400 italic">Sem descrição.</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={handleToggleComplete}
|
|
className={`flex-1 py-3 rounded-xl font-bold text-sm flex items-center justify-center gap-2 transition-colors ${
|
|
selectedEvent.completed
|
|
? 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
|
: 'bg-green-500 text-white hover:bg-green-600 shadow-lg shadow-green-200'
|
|
}`}
|
|
>
|
|
<CheckCircle2 size={18} />
|
|
{selectedEvent.completed ? 'Reabrir' : 'Concluir'}
|
|
</button>
|
|
<button
|
|
onClick={handleDeleteEvent}
|
|
className="p-3 bg-red-50 text-red-500 rounded-xl hover:bg-red-100 transition-colors"
|
|
title="Excluir"
|
|
>
|
|
<Trash2 size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Area */}
|
|
<div className="flex-1 flex flex-col bg-white rounded-[2rem] shadow-sm border border-slate-50 overflow-hidden">
|
|
{/* Calendar Header */}
|
|
<div className="p-6 border-b border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4">
|
|
<div className="flex gap-4 items-center">
|
|
{viewMode === 'month' ? (
|
|
<>
|
|
<h2 className="text-xl font-bold text-slate-800 capitalize">
|
|
{currentDate.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })}
|
|
</h2>
|
|
<div className="flex gap-1">
|
|
<button onClick={handlePrevMonth} className="p-1 hover:bg-slate-100 rounded-lg text-slate-500"><ChevronLeft size={20}/></button>
|
|
<button onClick={handleNextMonth} className="p-1 hover:bg-slate-100 rounded-lg text-slate-500"><ChevronRight size={20}/></button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<h2 className="text-xl font-bold text-slate-800">Agenda Completa</h2>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex bg-slate-100 p-1 rounded-xl">
|
|
<button
|
|
onClick={() => setViewMode('month')}
|
|
className={`p-2 rounded-lg transition-all ${viewMode === 'month' ? 'bg-white shadow text-primary-500' : 'text-slate-400 hover:text-slate-600'}`}
|
|
title="Visão Mensal"
|
|
>
|
|
<Grid size={18} />
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('agenda')}
|
|
className={`p-2 rounded-lg transition-all ${viewMode === 'agenda' ? 'bg-white shadow text-primary-500' : 'text-slate-400 hover:text-slate-600'}`}
|
|
title="Lista (Agenda)"
|
|
>
|
|
<List size={18} />
|
|
</button>
|
|
</div>
|
|
<button onClick={() => setIsModalOpen(true)} className="flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-xl text-sm font-bold shadow-lg shadow-primary-200/50 hover:bg-primary-600">
|
|
<Plus size={16} /> <span className="hidden sm:inline">Novo Evento</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{viewMode === 'month' ? (
|
|
<>
|
|
<div className="grid grid-cols-7 border-b border-slate-100 bg-slate-50/50">
|
|
{['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'].map(d => (
|
|
<div key={d} className="py-3 text-center text-xs font-bold text-slate-400 uppercase tracking-wider">{d}</div>
|
|
))}
|
|
</div>
|
|
<div className="grid grid-cols-7 flex-1 overflow-y-auto">
|
|
{days}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
{upcomingEvents.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-full text-slate-400">
|
|
<CheckCircle2 size={48} className="mb-4 opacity-20" />
|
|
<p>Nenhum evento futuro encontrado.</p>
|
|
</div>
|
|
) : (
|
|
upcomingEvents.map(event => (
|
|
<div
|
|
key={event.id}
|
|
onClick={() => handleEventClick(event)}
|
|
className="flex items-center gap-6 p-4 bg-slate-50 border border-slate-100 rounded-2xl hover:bg-white hover:shadow-md transition-all cursor-pointer group"
|
|
>
|
|
<div className="flex flex-col items-center justify-center w-16 h-16 bg-white rounded-xl border border-slate-200 shrink-0">
|
|
<span className="text-xs font-bold text-slate-400 uppercase">
|
|
{new Date(event.date + 'T12:00:00').toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '')}
|
|
</span>
|
|
<span className="text-xl font-bold text-slate-800">
|
|
{new Date(event.date + 'T12:00:00').getDate()}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<h4 className={`font-bold text-lg text-slate-800 ${event.completed ? 'line-through opacity-50' : ''}`}>{event.title}</h4>
|
|
<p className="text-sm text-slate-500 line-clamp-1">{event.description || 'Sem descrição'}</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-end gap-2">
|
|
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider ${
|
|
event.id.startsWith('rec-') ? 'bg-green-50 text-green-600' :
|
|
(event.type === 'payment' || event.id.startsWith('exp-')) ? 'bg-red-50 text-red-600' :
|
|
event.type === 'deadline' ? 'bg-amber-50 text-amber-600' :
|
|
'bg-blue-50 text-blue-600'
|
|
}`}>
|
|
{getEventTypeLabel(event)}
|
|
</span>
|
|
<span className="text-xs text-slate-400">
|
|
{new Date(event.date + 'T12:00:00').toLocaleDateString('pt-BR', { weekday: 'long' })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sidebar: Upcoming & Deadlines */}
|
|
<div className="w-full xl:w-80 flex flex-col gap-6">
|
|
<div className="bg-white p-6 rounded-[2rem] shadow-sm border border-slate-50 flex-1 overflow-y-auto min-h-[300px]">
|
|
<h3 className="font-bold text-slate-800 text-lg mb-4 flex items-center gap-2">
|
|
<Clock size={20} className="text-primary-500"/>
|
|
Próximos Vencimentos
|
|
</h3>
|
|
<p className="text-xs text-slate-400 mb-6">Próximos eventos e pagamentos.</p>
|
|
|
|
<div className="space-y-4">
|
|
{sidebarEvents.map(event => (
|
|
<div key={event.id} onClick={() => handleEventClick(event)} className="flex gap-4 items-start p-3 hover:bg-slate-50 rounded-2xl transition-colors border border-transparent hover:border-slate-100 group cursor-pointer">
|
|
<div className={`mt-1 w-3 h-3 rounded-full shrink-0 ${
|
|
event.id.startsWith('rec-') ? 'bg-green-500 shadow-sm shadow-green-200' :
|
|
(event.type === 'payment' || event.id.startsWith('exp-')) ? 'bg-red-500 shadow-sm shadow-red-200' :
|
|
event.type === 'deadline' ? 'bg-amber-500 shadow-sm shadow-amber-200' :
|
|
'bg-blue-500 shadow-sm shadow-blue-200'
|
|
}`} />
|
|
<div>
|
|
<h4 className="font-bold text-slate-800 text-sm group-hover:text-primary-500 transition-colors">{event.title}</h4>
|
|
<p className="text-xs text-slate-500 font-medium mt-0.5">
|
|
{new Date(event.date + 'T12:00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: 'long' })}
|
|
</p>
|
|
|
|
<div className="mt-2 flex gap-2">
|
|
<span className={`text-[10px] px-2 py-0.5 rounded border uppercase font-bold tracking-wider ${
|
|
event.id.startsWith('rec-') ? 'bg-green-50 border-green-100 text-green-600' :
|
|
(event.type === 'payment' || event.id.startsWith('exp-')) ? 'bg-red-50 border-red-100 text-red-600' :
|
|
event.type === 'deadline' ? 'bg-amber-50 border-amber-100 text-amber-600' :
|
|
'bg-blue-50 border-blue-100 text-blue-600'
|
|
}`}>
|
|
{getEventTypeLabel(event)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{sidebarEvents.length === 0 && (
|
|
<div className="text-center py-10 text-slate-400 text-sm">
|
|
<CheckCircle2 size={32} className="mx-auto mb-2 opacity-20"/>
|
|
Tudo tranquilo por aqui.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|