import React, { useState, useEffect, createContext, useContext, useMemo, useCallback, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, addDoc, onSnapshot, doc, updateDoc, deleteDoc, query, orderBy, serverTimestamp, arrayUnion, arrayRemove, setDoc, deleteField } from 'firebase/firestore'; // Import deleteField // Context for Firebase services and user ID const FirebaseContext = createContext(null); // Custom hook to use Firebase context const useFirebase = () => useContext(FirebaseContext); // Helper function to format phone number to Brazilian standard (XX) XX XXXXX-XXXX or (XX) XX XXXX-XXXX const formatPhoneNumber = (phoneNumberString) => { if (!phoneNumberString) return ''; const cleaned = ('' + phoneNumberString).replace(/\D/g, ''); // Remove all non-digit characters const match = cleaned.match(/^(\d{2})(\d{2})(\d{4,5})(\d{4})$/); // Matches (DD) DD DDDD-DDDD or (DD) DD DDDDD-DDDD if (match) { return `(${match[1]}) ${match[2]} ${match[3]}-${match[4]}`; } return phoneNumberString; // Return original if no match }; // Helper function to unformat phone number (remove non-digits) const unformatPhoneNumber = (formattedPhoneNumberString) => { if (!formattedPhoneNumberString) return ''; return formattedPhoneNumberString.replace(/\D/g, ''); }; // Notification Component function Notification({ message, type, id, onDismiss }) { useEffect(() => { const timer = setTimeout(() => { onDismiss(id); }, 3000); // Notification disappears after 3 seconds return () => clearTimeout(timer); }, [id, onDismiss]); let bgColor = 'bg-blue-600'; // Default for info if (type === 'success') bgColor = 'bg-green-600'; if (type === 'error') bgColor = 'bg-red-600'; return (
{message}
); } function App() { const [db, setDb] = useState(null); const [auth, setAuth] = useState(null); const [userId, setUserId] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [view, setView] = useState('kanban'); // Default to kanban view const [selectedLead, setSelectedLead] = useState(null); // Used for LeadDetail modal const [leads, setLeads] = useState([]); const [kanbanColumns, setKanbanColumns] = useState([]); // State for dynamic Kanban columns const [availableTags, setAvailableTags] = useState([]); // State for available tags const [availableSources, setAvailableSources] = useState([]); // State for available sources const [customFieldDefinitions, setCustomFieldDefinitions] = useState([]); // State for custom field definitions const [notifications, setNotifications] = useState([]); // State for notifications const [showConfirmModal, setShowConfirmModal] = useState(false); const [leadToDelete, setLeadToDelete] = useState(null); const [showColumnManagement, setShowColumnManagement] = useState(false); // State to toggle column management view const [showTagManagementModal, setShowTagManagementModal] = useState(false); // State for tag management modal const [showSourceManagementModal, setShowSourceManagementModal] = useState(false); // State for source management modal // Filter states const [filterStatus, setFilterStatus] = useState('Todos'); const [filterSource, setFilterSource] = useState(''); const [filterName, setFilterName] = useState(''); // New: Filter by name const [filterStartDate, setFilterStartDate] = useState(''); // New: Filter by date range const [filterEndDate, setFilterEndDate] = useState(''); // New: Filter by date range const [filterMinValue, setFilterMinValue] = useState(''); // New: Filter by value range const [filterMaxValue, setFilterMaxValue] = useState(''); // New: Filter by value range const [filterTags, setFilterTags] = useState([]); // New: Filter by tags const [showFilters, setShowFilters] = useState(false); // State to toggle filter visibility, now default to false // Function to add a notification const addNotification = useCallback((message, type = 'info') => { const id = Date.now(); setNotifications(prev => [...prev, { id, message, type }]); }, []); // Function to dismiss a notification const dismissNotification = useCallback((id) => { setNotifications(prev => prev.filter(notif => notif.id !== id)); }, []); // Initialize Firebase and set up auth listener useEffect(() => { try { const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; if (Object.keys(firebaseConfig).length === 0) { console.error("Firebase config is missing. Please ensure __firebase_config is provided."); addNotification("Erro: Configuração do Firebase ausente.", "error"); return; } const app = initializeApp(firebaseConfig); const firestore = getFirestore(app); const authentication = getAuth(app); setDb(firestore); setAuth(authentication); const unsubscribe = onAuthStateChanged(authentication, async (user) => { if (user) { setUserId(user.uid); } else { // Sign in anonymously if no user is authenticated try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(authentication, __initial_auth_token); setUserId(authentication.currentUser.uid); } else { await signInAnonymously(authentication); setUserId(authentication.currentUser.uid); } } catch (error) { console.error("Erro ao autenticar no Firebase:", error); addNotification(`Erro de autenticação: ${error.message}`, "error"); } } setIsAuthReady(true); }); return () => unsubscribe(); } catch (error) { console.error("Erro na inicialização do Firebase:", error); addNotification(`Erro de inicialização: ${error.message}`, "error"); } }, [addNotification]); // Fetch leads when Firebase is ready and userId is available useEffect(() => { if (db && userId && isAuthReady) { const leadsCollectionRef = collection(db, `artifacts/${__app_id}/users/${userId}/leads`); const q = query(leadsCollectionRef); const unsubscribe = onSnapshot(q, async (snapshot) => { const fetchedLeads = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); if (fetchedLeads.length === 0) { // Add example lead if no leads exist const exampleLead = { name: 'Lead de Exemplo', email: '[email protected]', phone: '5562983315518', // Example phone number in raw format status: 'Novo', source: 'Website', notes: 'Este é um lead de exemplo para demonstrar o CRM.', billingValue: 1000.00, // Now using billingValue consistently tags: ['demo', 'novo'], // Example tags createdAt: serverTimestamp(), // Explicitly set creation timestamp customFieldValues: {} // Initialize custom field values }; await addDoc(leadsCollectionRef, exampleLead); } // Sort in memory by createdAt descending fetchedLeads.sort((a, b) => (b.createdAt?.toDate() || 0) - (a.createdAt?.toDate() || 0)); setLeads(fetchedLeads); }, (error) => { console.error("Erro ao buscar leads:", error); addNotification(`Erro ao carregar leads: ${error.message}`, "error"); }); return () => unsubscribe(); } }, [db, userId, isAuthReady, addNotification]); // Fetch Kanban columns when Firebase is ready and userId is available useEffect(() => { if (db && userId && isAuthReady) { const columnsCollectionRef = collection(db, `artifacts/${__app_id}/users/${userId}/kanbanColumns`); const q = query(columnsCollectionRef, orderBy('order')); const unsubscribe = onSnapshot(q, async (snapshot) => { const fetchedColumns = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); if (fetchedColumns.length === 0) { // If no columns exist, create default ones const defaultColumns = [ { name: 'Novo', order: 0 }, { name: 'Em atendimento', order: 1 }, { name: 'Qualificado', order: 2 }, { name: 'Proposta', order: 3 }, { name: 'Fechado', order: 4 }, // Changed from 'Fechado Ganho' { name: 'Perdido', order: 5 } // Changed from 'Fechado Perdido' ]; for (const col of defaultColumns) { await addDoc(columnsCollectionRef, col); } } else { setKanbanColumns(fetchedColumns); } }, (error) => { console.error("Erro ao buscar colunas Kanban:", error); addNotification(`Erro ao carregar colunas: ${e.message}`, "error"); }); return () => unsubscribe(); } }, [db, userId, isAuthReady, addNotification]); // Fetch available tags useEffect(() => { if (db && userId && isAuthReady) { const tagsCollectionRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/tags`); const unsubscribe = onSnapshot(tagsCollectionRef, (docSnap) => { if (docSnap.exists()) { setAvailableTags(docSnap.data().allTags || []); } else { // Set default tags if none exist setDoc(tagsCollectionRef, { allTags: ['demo', 'novo'] }, { merge: true }); setAvailableTags(['demo', 'novo']); } }, (error) => { console.error("Erro ao buscar tags:", error); addNotification(`Erro ao carregar tags: ${e.message}`, "error"); }); return () => unsubscribe(); } }, [db, userId, isAuthReady, addNotification]); // Fetch available sources useEffect(() => { if (db && userId && isAuthReady) { const sourcesCollectionRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/sources`); const unsubscribe = onSnapshot(sourcesCollectionRef, (docSnap) => { if (docSnap.exists()) { setAvailableSources(docSnap.data().allSources || []); } else { // Set default sources if none exist setDoc(sourcesCollectionRef, { allSources: ['Anúncios Google', 'Anúncios Meta', 'Indicação', 'Orgânico', 'Outro'] }, { merge: true }); setAvailableSources(['Anúncios Google', 'Anúncios Meta', 'Indicação', 'Orgânico', 'Outro']); } }, (error) => { console.error("Erro ao buscar origens:", error); addNotification(`Erro ao carregar origens: ${e.message}`, "error"); }); return () => unsubscribe(); } }, [db, userId, isAuthReady, addNotification]); // Fetch custom field definitions useEffect(() => { if (db && userId && isAuthReady) { const customFieldsCollectionRef = collection(db, `artifacts/${__app_id}/users/${userId}/customFieldDefinitions`); const unsubscribe = onSnapshot(customFieldsCollectionRef, (snapshot) => { const fetchedDefinitions = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setCustomFieldDefinitions(fetchedDefinitions); }, (error) => { console.error("Erro ao buscar definições de campos personalizados:", error); addNotification(`Erro ao carregar definições de campos: ${e.message}`, "error"); }); return () => unsubscribe(); } }, [db, userId, isAuthReady, addNotification]); // Filtered leads based on current filters const filteredLeads = useMemo(() => { return leads.filter(lead => { const matchesName = filterName === '' || lead.name.toLowerCase().includes(filterName.toLowerCase()); const matchesStatus = filterStatus === 'Todos' || lead.status === filterStatus; const matchesSource = filterSource === '' || (lead.source && lead.source.toLowerCase().includes(filterSource.toLowerCase())); const leadDate = lead.createdAt?.toDate ? lead.createdAt.toDate() : null; // Use createdAt for filtering const start = filterStartDate ? new Date(filterStartDate) : null; const end = filterEndDate ? new Date(filterEndDate) : null; const matchesDate = (!start || (leadDate && leadDate >= start)) && (!end || (leadDate && leadDate <= end)); const leadBillingValue = lead.billingValue || 0; const minVal = filterMinValue === '' ? -Infinity : parseFloat(filterMinValue); const maxVal = filterMaxValue === '' ? Infinity : parseFloat(filterMaxValue); const matchesValue = leadBillingValue >= minVal && leadBillingValue <= maxVal; const matchesTags = filterTags.length === 0 || (lead.tags && filterTags.every(tag => lead.tags.includes(tag))); return matchesName && matchesStatus && matchesSource && matchesDate && matchesValue && matchesTags; }); }, [leads, filterStatus, filterSource, filterName, filterStartDate, filterEndDate, filterMinValue, filterMaxValue, filterTags]); const handleAddLead = async (leadData) => { if (!db || !userId) { addNotification("Serviço de banco de dados não disponível.", "error"); return; } try { await addDoc(collection(db, `artifacts/${__app_id}/users/${userId}/leads`), { ...leadData, createdAt: serverTimestamp(), // Explicitly set creation timestamp customFieldValues: leadData.customFieldValues || {} // Ensure customFieldValues is initialized }); addNotification('Lead adicionado com sucesso!', "success"); setSelectedLead(null); // Close the form modal setView('kanban'); // Go back to kanban after adding } catch (e) { console.error("Erro ao adicionar lead: ", e); addNotification(`Erro ao adicionar lead: ${e.message}`, "error"); } }; const handleUpdateLead = async (id, leadData) => { if (!db || !userId) { addNotification("Serviço de banco de dados não disponível.", "error"); return; } try { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, id); const currentLead = leads.find(l => l.id === id); // Check if status is changing to 'Em atendimento' for the first time if (leadData.status === 'Em atendimento' && (!currentLead || !currentLead.firstResponseAt)) { leadData.firstResponseAt = serverTimestamp(); } await updateDoc(leadRef, { ...leadData, }); addNotification('Lead atualizado com sucesso!', "success"); // If the update was from the detail modal, close it. if (selectedLead && selectedLead.id === id) { setSelectedLead(null); } setView('kanban'); // Go back to kanban after editing } catch (e) { console.error("Erro ao atualizar lead: ", e); addNotification(`Erro ao atualizar lead: ${e.message}`, "error"); } }; const handleDeleteLead = async () => { if (!db || !userId || !leadToDelete) { addNotification("Serviço de banco de dados não disponível ou lead não selecionado.", "error"); return; } try { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, leadToDelete.id); await deleteDoc(leadRef); addNotification('Lead excluído com sucesso!', "success"); setShowConfirmModal(false); setLeadToDelete(null); setView('kanban'); // Go back to kanban after deletion setSelectedLead(null); // Close detail modal if open } catch (e) { console.error("Erro ao excluir lead: ", e); addNotification(`Erro ao excluir lead: ${e.message}`, "error"); } }; const confirmDelete = (lead) => { setLeadToDelete(lead); setShowConfirmModal(true); }; const cancelDelete = () => { setShowConfirmModal(false); setLeadToDelete(null); }; const navigateTo = (newView, lead = null) => { setView(newView); setSelectedLead(lead); // Set selectedLead to open the modal // No need to clear global message, as it's replaced by notifications }; const handleStatusChange = async (leadId, newStatus) => { if (!db || !userId) { addNotification("Serviço de banco de dados não disponível.", "error"); return; } try { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, leadId); const currentLead = leads.find(l => l.id === leadId); const updateData = { status: newStatus }; // Check if status is changing to 'Em atendimento' for the first time if (newStatus === 'Em atendimento' && (!currentLead || !currentLead.firstResponseAt)) { updateData.firstResponseAt = serverTimestamp(); } await updateDoc(leadRef, updateData); addNotification(`Status do lead atualizado para ${newStatus}!`, "success"); } catch (e) { console.error("Erro ao atualizar status do lead: ", e); addNotification(`Erro ao atualizar status: ${e.message}`, "error"); } }; const handleDownloadLeads = () => { // Placeholder for download functionality addNotification('Funcionalidade de Download de Leads em desenvolvimento!', "info"); console.log('Download Leads clicked. Leads data:', leads); }; // Column Management functions const handleAddColumn = async (columnName) => { if (!db || !userId) return; try { const newOrder = kanbanColumns.length > 0 ? Math.max(...kanbanColumns.map(c => c.order)) + 1 : 0; await addDoc(collection(db, `artifacts/${__app_id}/users/${userId}/kanbanColumns`), { name: columnName, order: newOrder }); addNotification('Coluna adicionada com sucesso!', "success"); } catch (e) { console.error("Erro ao adicionar coluna: ", e); addNotification(`Erro ao adicionar coluna: ${e.message}`, "error"); } }; const handleUpdateColumn = async (columnId, newName) => { if (!db || !userId) return; try { const columnRef = doc(db, `artifacts/${__app_id}/users/${userId}/kanbanColumns`, columnId); await updateDoc(columnRef, { name: newName }); addNotification('Coluna atualizada com sucesso!', "success"); } catch (e) { console.error("Erro ao atualizar coluna: ", e); addNotification(`Erro ao atualizar coluna: ${e.message}`, "error"); } }; const handleReorderColumn = async (columnId, direction) => { if (!db || !userId) return; const currentColumnIndex = kanbanColumns.findIndex(col => col.id === columnId); if (currentColumnIndex === -1) return; const currentColumn = kanbanColumns[currentColumnIndex]; let targetColumnIndex = -1; if (direction === 'up' && currentColumnIndex > 0) { targetColumnIndex = currentColumnIndex - 1; } else if (direction === 'down' && currentColumnIndex < kanbanColumns.length - 1) { targetColumnIndex = currentColumnIndex + 1; } if (targetColumnIndex !== -1) { const targetColumn = kanbanColumns[targetColumnIndex]; try { // Swap orders in Firestore await updateDoc(doc(db, `artifacts/${__app_id}/users/${userId}/kanbanColumns`, currentColumn.id), { order: targetColumn.order }); await updateDoc(doc(db, `artifacts/${__app_id}/users/${userId}/kanbanColumns`, targetColumn.id), { order: currentColumn.order }); addNotification('Ordem da coluna atualizada!', "success"); } catch (e) { console.error("Erro ao reordenar coluna: ", e); addNotification(`Erro ao reordenar coluna: ${e.message}`, "error"); } } }; const handleDeleteColumn = async (columnId) => { if (!db || !userId) return; const columnToDelete = kanbanColumns.find(col => col.id === columnId); if (!columnToDelete) return; // Using a custom modal instead of window.confirm const confirmAction = async () => { try { // Move leads from deleted status to 'Novo' const leadsToUpdate = leads.filter(lead => lead.status === columnToDelete.name); const batch = db.batch(); // Firebase batch for multiple updates for (const lead of leadsToUpdate) { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, lead.id); batch.update(leadRef, { status: 'Novo' }); } await batch.commit(); // Delete the column await deleteDoc(doc(db, `artifacts/${__app_id}/users/${userId}/kanbanColumns`, columnId)); addNotification('Coluna excluída e leads movidos com sucesso!', "success"); setShowConfirmModal(false); // Close the confirm modal } catch (e) { console.error("Erro ao excluir coluna: ", e); addNotification(`Erro ao excluir coluna: ${e.message}`, "error"); } }; setLeadToDelete({ id: columnId, name: columnToDelete.name, type: 'column', confirmAction }); setShowConfirmModal(true); }; // Tag management functions const handleAddTag = async (tagName) => { if (!db || !userId) return; const tagsDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/tags`); try { // Use setDoc with merge: true to create the document if it doesn't exist, or update it if it does await setDoc(tagsDocRef, { allTags: arrayUnion(tagName) }, { merge: true }); // No global message here, TagEditor handles local message } catch (e) { console.error("Erro ao adicionar tag:", e); // No global message here, TagEditor handles local message } }; const handleRemoveTag = async (tagName) => { if (!db || !userId) return; const tagsDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/tags`); try { await updateDoc(tagsDocRef, { allTags: arrayRemove(tagName) }); // No global message here, TagEditor handles local message } catch (e) { console.error("Erro ao remover tag:", e); // No global message here, TagEditor handles local message } }; // Source management functions const handleAddSource = async (sourceName) => { if (!db || !userId) return; const sourcesDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/sources`); try { await setDoc(sourcesDocRef, { allSources: arrayUnion(sourceName) }, { merge: true }); } catch (e) { console.error("Erro ao adicionar origem:", e); } }; const handleRemoveSource = async (sourceName) => { if (!db || !userId) return; const sourcesDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/sources`); try { await updateDoc(sourcesDocRef, { allSources: arrayRemove(sourceName) }); } catch (e) { console.error("Erro ao remover origem:", e); } }; const handleUpdateSource = async (oldName, newName) => { if (!db || !userId) return; const sourcesDocRef = doc(db, `artifacts/${__app_id}/users/${userId}/settings/sources`); try { await updateDoc(sourcesDocRef, { allSources: arrayRemove(oldName) }); await updateDoc(sourcesDocRef, { allSources: arrayUnion(newName) }); // Also update leads that use this source const leadsToUpdate = leads.filter(lead => lead.source === oldName); const batch = db.batch(); for (const lead of leadsToUpdate) { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, lead.id); batch.update(leadRef, { source: newName }); } await batch.commit(); } catch (e) { console.error("Erro ao atualizar origem: ", e); } }; // Custom Field Management functions const handleAddCustomFieldDefinition = async (fieldDefinition) => { if (!db || !userId) return; try { await addDoc(collection(db, `artifacts/${__app_id}/users/${userId}/customFieldDefinitions`), fieldDefinition); addNotification('Campo personalizado adicionado!', "success"); } catch (e) { console.error("Erro ao adicionar definição de campo personalizado:", e); addNotification(`Erro ao adicionar campo personalizado: ${e.message}`, "error"); } }; const handleUpdateLeadCustomFieldValue = async (leadId, fieldName, value) => { if (!db || !userId) return; try { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, leadId); await updateDoc(leadRef, { [`customFieldValues.${fieldName}`]: value }); // No notification here, as it's a frequent interaction } catch (e) { console.error("Erro ao atualizar valor do campo personalizado:", e); addNotification(`Erro ao atualizar campo: ${e.message}`, "error"); } }; const handleDeleteCustomFieldDefinition = async (fieldId) => { if (!db || !userId) return; try { await deleteDoc(doc(db, `artifacts/${__app_id}/users/${userId}/customFieldDefinitions`, fieldId)); addNotification('Definição de campo personalizado excluída!', "success"); // Optionally, also remove the field from all leads' customFieldValues const batch = db.batch(); leads.forEach(lead => { if (lead.customFieldValues && lead.customFieldValues[fieldId]) { const leadRef = doc(db, `artifacts/${__app_id}/users/${userId}/leads`, lead.id); batch.update(leadRef, { [`customFieldValues.${fieldId}`]: deleteField() }); // Use deleteField to remove the field } }); await batch.commit(); } catch (e) { console.error("Erro ao excluir definição de campo personalizado:", e); addNotification(`Erro ao excluir definição de campo: ${e.message}`, "error"); } }; if (!isAuthReady) { return (

Carregando CRM...

); } return ( {/* Custom scrollbar styles and global background */}
{/* Reduced mb and pb */}

{/* Reduced mb */} Rise CRM

{/* User ID moved to a less prominent spot */} {userId && (
ID do Usuário: {userId}
)}
{/* Summary Dashboard - Always visible, moved closer to top */} {/* Filter Toggle Button */} {(view === 'list' || view === 'kanban') && (
)} {/* Filters - Conditionally rendered */} {(view === 'list' || view === 'kanban') && showFilters && (

Filtros

setFilterName(e.target.value)} className="w-full p-2 bg-gray-700 text-gray-100 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
setFilterStartDate(e.target.value)} className="w-full p-2 bg-gray-700 text-gray-100 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
setFilterEndDate(e.target.value)} className="w-full p-2 bg-gray-700 text-gray-100 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
setFilterMinValue(e.target.value)} className="w-full p-2 bg-gray-700 text-gray-100 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
setFilterMaxValue(e.target.value)} className="w-full p-2 bg-gray-700 text-gray-100 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
)} {view === 'list' && ( setSelectedLead(lead)} // Row click opens detail modal setView={setView} // Pass setView to LeadList setShowColumnManagement={setShowColumnManagement} // Pass for column management button setSelectedLead={setSelectedLead} // Pass for new lead button handleDownloadLeads={handleDownloadLeads} // Pass for download button currentView={view} // Pass the current view state setShowTagManagementModal={setShowTagManagementModal} // Pass for tag management button setShowSourceManagementModal={setShowSourceManagementModal} // Pass for source management button /> )} {view === 'kanban' && ( setSelectedLead(lead)} // Card click opens detail modal onStatusChange={handleStatusChange} kanbanColumns={kanbanColumns} // Pass dynamic columns setView={setView} // Pass setView for toggle button setShowColumnManagement={setShowColumnManagement} // Pass for column management button setSelectedLead={setSelectedLead} // Pass for new lead button handleDownloadLeads={handleDownloadLeads} // Pass for download button currentView={view} // Pass the current view state setShowTagManagementModal={setShowTagManagementModal} // Pass for tag management button setShowSourceManagementModal={setShowSourceManagementModal} // Pass for source management button /> )} {/* LeadForm is now a modal, so it's rendered when selectedLead is 'new' */} {selectedLead && selectedLead.id === 'new' && ( setSelectedLead(null)} // Close modal kanbanColumns={kanbanColumns} // Pass columns to form for status dropdown availableSources={availableSources} // Pass available sources to form availableTags={availableTags} // Pass available tags to form addNotification={addNotification} // Pass notification function /> )} {/* LeadDetail is now a modal that handles its own edit state */} {selectedLead && selectedLead.id !== 'new' && ( setSelectedLead(null)} // Close modal onUpdateLead={handleUpdateLead} // Pass update function onDeleteLead={handleDeleteLead} // Pass delete function kanbanColumns={kanbanColumns} availableTags={availableTags} availableSources={availableSources} // Pass available sources to detail customFieldDefinitions={customFieldDefinitions} // Pass custom field definitions onAddCustomFieldDefinition={handleAddCustomFieldDefinition} // Pass function to add custom field definition onUpdateLeadCustomFieldValue={handleUpdateLeadCustomFieldValue} // Pass function to update custom field value confirmDelete={confirmDelete} // Pass confirmDelete for modal's delete button addNotification={addNotification} // Pass addNotification for custom field button /> )} {showConfirmModal && ( )} {showColumnManagement && ( setShowColumnManagement(false)} onAddColumn={handleAddColumn} onUpdateColumn={handleUpdateColumn} onReorderColumn={handleReorderColumn} onDeleteColumn={handleDeleteColumn} /> )} {showTagManagementModal && ( setShowTagManagementModal(false)} /> )} {showSourceManagementModal && ( setShowSourceManagementModal(false)} /> )} {/* Notifications container */}
{notifications.map(notif => ( ))}
); } function SummaryDashboard({ leads, kanbanColumns }) { const totalLeads = leads.length; const totalSales = leads.filter(lead => lead.status === 'Fechado').length; // Updated to 'Fechado' const totalLosses = leads.filter(lead => lead.status === 'Perdido').length; // Updated to 'Perdido' const awaitingContact = leads.filter(lead => lead.status === kanbanColumns.find(col => col.order === 0)?.name || 'Novo').length; // "Novo" is typically the first column // Calculate average response time const leadsWithResponseTime = leads.filter(lead => lead.createdAt && lead.firstResponseAt); const totalResponseTimeMs = leadsWithResponseTime.reduce((sum, lead) => { const created = lead.createdAt.toDate(); const responded = lead.firstResponseAt.toDate(); return sum + (responded.getTime() - created.getTime()); }, 0); const averageResponseTimeMs = leadsWithResponseTime.length > 0 ? totalResponseTimeMs / leadsWithResponseTime.length : 0; // Convert milliseconds to a human-readable format (e.g., days, hours, minutes) const formatTime = (ms) => { if (ms === 0) return 'N/A'; const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ${hours % 24}h`; if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes}m ${seconds % 60}s`; return `${seconds}s`; }; // Calculate total billing value for 'Fechado' leads const totalWonBillingValue = leads .filter(lead => lead.status === 'Fechado') .reduce((sum, lead) => sum + (lead.billingValue || 0), 0); return (
{/* Reduced mb */}

Resumo do CRM

Leads Totais

{totalLeads.toLocaleString('pt-BR')}

Vendas

{totalSales.toLocaleString('pt-BR')}

Perdas

{totalLosses.toLocaleString('pt-BR')}

Aguardando Contato

{awaitingContact.toLocaleString('pt-BR')}

Tempo Médio de Resposta

{formatTime(averageResponseTimeMs)}

Faturamento de Vendas (Fechado)

{totalWonBillingValue.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}

); } function LeadList({ leads, onView, setView, setShowColumnManagement, setSelectedLead, handleDownloadLeads, currentView, setShowTagManagementModal, setShowSourceManagementModal }) { const [searchTerm, setSearchTerm] = useState(''); const filteredLeads = leads.filter(lead => lead.name.toLowerCase().includes(searchTerm.toLowerCase()) || (lead.email && lead.email.toLowerCase().includes(searchTerm.toLowerCase())) || (lead.phone && unformatPhoneNumber(lead.phone).includes(unformatPhoneNumber(searchTerm))) || // Search unformatted phone (lead.source && lead.source.toLowerCase().includes(searchTerm.toLowerCase())) || (lead.tags && lead.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))) ); return (

Lista de Leads

{/* Toggle between Kanban and List View */} {/* Button to open Column Management */} {/* Button to open Tag Management Modal */}
setSearchTerm(e.target.value)} />
{filteredLeads.length === 0 ? (

Nenhum lead encontrado com os filtros e busca atuais.

) : ( {/* New column header */} {filteredLeads.map(lead => ( onView(lead)} > {/* Formatted phone number */} ))}
Nome Email Telefone Status Origem Faturamento (R$) Tags Data de Criação
{lead.name} {lead.email} {formatPhoneNumber(lead.phone) || 'N/A'} {lead.status} {lead.source || 'N/A'} {(lead.billingValue || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
{lead.tags && lead.tags.map(tag => ( {tag} ))}
{lead.createdAt?.toDate ? lead.createdAt.toDate().toLocaleDateString('pt-BR') : 'N/A'}
)}
); } function KanbanBoard({ leads, onView, onStatusChange, kanbanColumns, setView, setShowColumnManagement, setSelectedLead, handleDownloadLeads, currentView, setShowTagManagementModal, setShowSourceManagementModal }) { const sortedColumns = useMemo(() => { return [...kanbanColumns].sort((a, b) => a.order - b.order); }, [kanbanColumns]); const statusNames = sortedColumns.map(col => col.name); const handleDragStart = (e, leadId, currentStatus) => { e.dataTransfer.setData('leadId', leadId); e.dataTransfer.setData('currentStatus', currentStatus); }; const handleDragOver = (e) => { e.preventDefault(); // Necessary to allow dropping }; const handleDrop = (e, newStatus) => { e.preventDefault(); const leadId = e.dataTransfer.getData('leadId'); const currentStatus = e.dataTransfer.getData('currentStatus'); if (leadId && currentStatus !== newStatus) { onStatusChange(leadId, newStatus); } }; return (
{/* Main Kanban Board container */} {/* Fixed Header and Buttons */}
{/* Added sticky, top-0, bg and z-index */}

Kanban de Leads

{/* Toggle between Kanban and List View */} {/* Button to open Column Management */} {/* Button to open Tag Management Modal */}
{/* Scrollable Columns Container */}
{/* Added negative margin and padding to extend to edges */}
{/* Inner flex container for columns */} {statusNames.map(status => (
handleDrop(e, status)} >

{status} ({leads.filter(lead => lead.status === status).length.toLocaleString('pt-BR')})

{leads.filter(lead => lead.status === status).length === 0 ? (

Nenhum lead neste status.

) : ( leads.filter(lead => lead.status === status).map(lead => ( )) )}
))}
); } function LeadCard({ lead, onView, onStatusChange, allStatuses, onDragStart }) { return (
onView(lead)} draggable="true" // Make the card draggable onDragStart={(e) => onDragStart(e, lead.id, lead.status)} // Handle drag start >

{lead.name}

Email: {lead.email}

Telefone: {formatPhoneNumber(lead.phone) || 'N/A'}

{/* Formatted phone number */}

Origem: {lead.source || 'N/A'}

Faturamento: {(lead.billingValue || 0).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}

Criado em: {lead.createdAt?.toDate ? lead.createdAt.toDate().toLocaleDateString('pt-BR') : 'N/A'}

{lead.tags && lead.tags.map(tag => ( {tag} ))}
); } function LeadForm({ lead, onSave, onCancel, kanbanColumns, availableSources, availableTags, addNotification }) { const statusOptions = useMemo(() => { return kanbanColumns.map(col => col.name); }, [kanbanColumns]); const initialFormData = useRef(null); const isMouseDownOnOverlay = useRef(false); // New ref to track mouse down on overlay const [formData, setFormData] = useState({ name: '', phone: '', email: '', source: availableSources.length > 0 ? availableSources[0] : '', // Default to first available source notes: '', billingValue: '', tags: [], status: statusOptions.length > 0 ? statusOptions[0] : 'Novo', customFieldValues: {} // Initialize custom field values }); useEffect(() => { const currentData = { name: lead?.name || '', phone: unformatPhoneNumber(lead?.phone || ''), // Unformat phone for internal state email: lead?.email || '', source: lead?.source || (availableSources.length > 0 ? availableSources[0] : ''), notes: lead?.notes || '', billingValue: lead?.billingValue || '', tags: lead?.tags || [], status: lead?.status || (statusOptions.length > 0 ? statusOptions[0] : 'Novo'), customFieldValues: lead?.customFieldValues || {} }; setFormData(currentData); initialFormData.current = currentData; // Store initial data for comparison }, [lead, statusOptions, availableSources]); const hasUnsavedChanges = useMemo(() => { return JSON.stringify(formData) !== JSON.stringify(initialFormData.current); }, [formData]); const handleChange = (e) => { const { name, value } = e.target; if (name === 'phone') { const cleaned = value.replace(/\D/g, ''); // Only digits setFormData(prev => ({ ...prev, [name]: cleaned })); // Store unformatted number } else { setFormData(prev => ({ ...prev, [name]: value })); } }; const handlePhoneBlur = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: unformatPhoneNumber(value) })); // Ensure it's unformatted on blur for storage }; const handleTagChange = (newTags) => { setFormData(prev => ({ ...prev, tags: newTags })); }; const handleSaveAndClose = () => { const dataToSave = { ...formData, billingValue: formData.billingValue !== '' ? parseFloat(formData.billingValue) : 0, phone: unformatPhoneNumber(formData.phone) // Ensure phone is unformatted for saving }; if (lead && lead.id !== 'new') { onSave(lead.id, dataToSave); } else { onSave(dataToSave); } initialFormData.current = formData; // Update initial data after saving onCancel(); // Close modal after saving }; const handleDiscardAndClose = () => { onCancel(); // Just close, discarding changes }; const handleOverlayMouseDown = (e) => { if (e.target === e.currentTarget) { // Check if click started directly on the overlay isMouseDownOnOverlay.current = true; } }; const handleOverlayMouseUp = (e) => { if (isMouseDownOnOverlay.current && e.target === e.currentTarget) { // Check if click ended directly on the overlay handleCloseAttempt(); } isMouseDownOnOverlay.current = false; // Reset flag }; const handleCloseAttempt = () => { if (hasUnsavedChanges) { addNotification('Você tem alterações não salvas. Deseja salvar?', 'info'); setShowUnsavedChangesConfirmModal(true); } else { onCancel(); } }; const [showUnsavedChangesConfirmModal, setShowUnsavedChangesConfirmModal] = useState(false); return (
e.stopPropagation()}> {/* Prevent click from bubbling */}

{lead && lead.id !== 'new' ? 'Editar Lead' : 'Adicionar Novo Lead'}

{ e.preventDefault(); handleSaveAndClose(); }} className="space-y-4"> {/* Top section: Nome, telefone, email, origem e observação */}
{/* Separated section: Valor de faturamento, tags e status */}
{/* Separator */}
{/* Status takes full width on medium screens */}
{showUnsavedChangesConfirmModal && ( setShowUnsavedChangesConfirmModal(false)} /> )}
); } function LeadDetail({ lead, onClose, onUpdateLead, onDeleteLead, kanbanColumns, availableTags, availableSources, customFieldDefinitions, onAddCustomFieldDefinition, onUpdateLeadCustomFieldValue, confirmDelete, addNotification }) { const statusOptions = useMemo(() => { return kanbanColumns.map(col => col.name); }, [kanbanColumns]); const initialFormData = useRef(null); const isMouseDownOnOverlay = useRef(false); // New ref to track mouse down on overlay const [formData, setFormData] = useState({ name: lead.name || '', email: lead.email || '', phone: lead.phone || '', status: lead.status || (statusOptions.length > 0 ? statusOptions[0] : 'Novo'), source: lead.source || (availableSources.length > 0 ? availableSources[0] : ''), notes: lead.notes || '', billingValue: lead.billingValue || '', tags: lead.tags || [], customFieldValues: lead.customFieldValues || {} // Ensure customFieldValues is initialized }); const [showCustomFieldForm, setShowCustomFieldForm] = useState(false); const [showUnsavedChangesConfirmModal, setShowUnsavedChangesConfirmModal] = useState(false); useEffect(() => { if (lead) { const currentData = { name: lead.name || '', email: lead.email || '', phone: unformatPhoneNumber(lead.phone || ''), // Unformat phone for internal state status: lead.status || (statusOptions.length > 0 ? statusOptions[0] : 'Novo'), source: lead.source || (availableSources.length > 0 ? availableSources[0] : ''), notes: lead.notes || '', billingValue: lead.billingValue || '', tags: lead.tags || [], customFieldValues: lead.customFieldValues || {} }; setFormData(currentData); initialFormData.current = currentData; // Store initial data for comparison } }, [lead, statusOptions, availableSources]); const hasUnsavedChanges = useMemo(() => { return JSON.stringify(formData) !== JSON.stringify(initialFormData.current); }, [formData]); const handleChange = (e) => { const { name, value } = e.target; if (name === 'phone') { const cleaned = value.replace(/\D/g, ''); // Only digits setFormData(prev => ({ ...prev, [name]: cleaned })); // Store unformatted number } else { setFormData(prev => ({ ...prev, [name]: value })); } }; const handlePhoneBlur = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: unformatPhoneNumber(value) })); // Ensure it's unformatted on blur for storage }; const handleTagChange = (newTags) => { setFormData(prev => ({ ...prev, tags: newTags })); }; const handleCustomFieldChange = (fieldId, value) => { setFormData(prev => ({ ...prev, customFieldValues: { ...prev.customFieldValues, [fieldId]: value } })); onUpdateLeadCustomFieldValue(lead.id, fieldId, value); }; const handleSaveAndClose = () => { const dataToSave = { ...formData, billingValue: formData.billingValue !== '' ? parseFloat(formData.billingValue) : 0, phone: unformatPhoneNumber(formData.phone) // Ensure phone is unformatted for saving }; onUpdateLead(lead.id, dataToSave); initialFormData.current = formData; // Update initial data after saving onClose(); // Close modal after saving }; const handleDiscardAndClose = () => { onClose(); // Just close, discarding changes }; const handleOverlayMouseDown = (e) => { if (e.target === e.currentTarget) { // Check if click started directly on the overlay isMouseDownOnOverlay.current = true; } }; const handleOverlayMouseUp = (e) => { if (isMouseDownOnOverlay.current && e.target === e.currentTarget) { // Check if click ended directly on the overlay handleCloseAttempt(); } isMouseDownOnOverlay.current = false; // Reset flag }; const handleCloseAttempt = () => { if (hasUnsavedChanges) { addNotification('Você tem alterações não salvas. Deseja salvar?', 'info'); setShowUnsavedChangesConfirmModal(true); } else { onClose(); } }; return (
e.stopPropagation()}> {/* Prevent click from bubbling */} {/* Close button at top right */}

{lead.name} ID: {lead.id}

{/* Scrollable content area */}
{/* Added pr-4 for scrollbar space */} {/* Editable Fields Section */}

Campos Editáveis

{/* New button for custom field creation */}
{/* Custom Fields Section */} {customFieldDefinitions.length > 0 && (

Campos Personalizados

{customFieldDefinitions.map(fieldDef => (
{fieldDef.type === 'texto' && ( handleCustomFieldChange(fieldDef.id, e.target.value)} className="w-full p-2 bg-gray-600 text-gray-100 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> )} {(fieldDef.type === 'numero' || fieldDef.type === 'valor') && ( handleCustomFieldChange(fieldDef.id, parseFloat(e.target.value))} className="w-full p-2 bg-gray-600 text-gray-100 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" step={fieldDef.type === 'valor' ? "0.01" : "any"} /> )} {fieldDef.type === 'data' && ( handleCustomFieldChange(fieldDef.id, e.target.value)} className="w-full p-2 bg-gray-600 text-gray-100 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> )} {fieldDef.type === 'multipla-escolha' && ( )} {fieldDef.type === 'caixa-selecao' && (
{(fieldDef.options || []).map(option => ( ))}
)}
))}
)} {/* Lead Information Section (Read-only) */}

Informações do Lead

Detalhes de Contato

{lead.email}

{formatPhoneNumber(lead.phone) || 'N/A'}

{/* Formatted phone number */}

{lead.source || 'N/A'}

Criado em: {lead.createdAt?.toDate ? lead.createdAt.toDate().toLocaleString('pt-BR') : 'N/A'}

Descrição do Projeto

{lead.notes || 'N/A'}

{/* Action Buttons */}
{showCustomFieldForm && ( setShowCustomFieldForm(false)} addNotification={addNotification} /> )} {showUnsavedChangesConfirmModal && ( setShowUnsavedChangesConfirmModal(false)} /> )}
); } function CustomFieldForm({ onSave, onClose, addNotification }) { const initialFormData = useRef(null); const isMouseDownOnOverlay = useRef(false); // New ref to track mouse down on overlay const [fieldName, setFieldName] = useState(''); const [fieldType, setFieldType] = useState('texto'); const [options, setOptions] = useState(''); // For multiple choice/checkbox const [localMessage, setLocalMessage] = useState(''); const [showUnsavedChangesConfirmModal, setShowUnsavedChangesConfirmModal] = useState(false); useEffect(() => { // Initialize initialFormData initialFormData.current = { fieldName: '', fieldType: 'texto', options: '' }; }, []); const hasUnsavedChanges = useMemo(() => { const currentData = { fieldName, fieldType, options }; return JSON.stringify(currentData) !== JSON.stringify(initialFormData.current); }, [fieldName, fieldType, options]); const handleSubmit = (e) => { e.preventDefault(); if (fieldName.trim() === '') { setLocalMessage('O nome do campo não pode estar vazio.'); return; } const newField = { name: fieldName.trim(), type: fieldType, }; if (fieldType === 'multipla-escolha' || fieldType === 'caixa-selecao') { if (options.trim() === '') { setLocalMessage('As opções não podem estar vazias para este tipo de campo.'); return; } newField.options = options.split(',').map(opt => opt.trim()).filter(opt => opt !== ''); } onSave(newField); initialFormData.current = { fieldName: '', fieldType: 'texto', options: '' }; // Reset initial data after saving onClose(); }; const handleSaveAndClose = () => { handleSubmit({ preventDefault: () => {} }); // Simulate form submission }; const handleDiscardAndClose = () => { onClose(); // Just close, discarding changes }; const handleOverlayMouseDown = (e) => { if (e.target === e.currentTarget) { // Check if click started directly on the overlay isMouseDownOnOverlay.current = true; } }; const handleOverlayMouseUp = (e) => { if (isMouseDownOnOverlay.current && e.target === e.currentTarget) { // Check if click ended directly on the overlay handleCloseAttempt(); } isMouseDownOnOverlay.current = false; // Reset flag }; const handleCloseAttempt = () => { if (hasUnsavedChanges) { addNotification('Você tem alterações não salvas. Deseja salvar?', 'info'); setShowUnsavedChangesConfirmModal(true); } else { onClose(); } }; return (
e.stopPropagation()}>

Definir Novo Campo Personalizado

{ setFieldName(e.target.value); setLocalMessage(''); }} className="w-full p-3 bg-gray-700 text-gray-100 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required />
{(fieldType === 'multipla-escolha' || fieldType === 'caixa-selecao') && (
)} {localMessage && (

{localMessage}

)}
{showUnsavedChangesConfirmModal && ( setShowUnsavedChangesConfirmModal(false)} /> )}
); } function ConfirmModal({ message, onConfirm, onCancel }) { return (
{/* Click outside to close */}
e.stopPropagation()}> {/* Prevent click from bubbling */}

{message}

); } function UnsavedChangesConfirmModal({ message, onSave, onDiscard, onCancel }) { return (
e.stopPropagation()}>

{message}

); } function ColumnManagement({ kanbanColumns, onClose, onAddColumn, onUpdateColumn, onReorderColumn, onDeleteColumn }) { const [newColumnName, setNewColumnName] = useState(''); const [editingColumnId, setEditingColumnId] = useState(null); const [editingColumnName, setEditingColumnName] = useState(''); const handleEditClick = (column) => { setEditingColumnId(column.id); setEditingColumnName(column.name); }; const handleSaveEdit = (columnId) => { if (editingColumnName.trim() === '') { // Using a custom modal instead of alert // alert('O nome da coluna não pode estar vazio.'); // For now, I'll keep the alert as per previous code, but recommend a custom modal here console.error('O nome da coluna não pode estar vazio.'); return; } onUpdateColumn(columnId, editingColumnName); setEditingColumnId(null); setEditingColumnName(''); }; const handleCancelEdit = () => { setEditingColumnId(null); setEditingColumnName(''); }; const handleAdd = () => { if (newColumnName.trim() === '') { // Using a custom modal instead of alert // alert('O nome da nova coluna não pode estar vazio.'); // For now, I'll keep the alert as per previous code, but recommend a custom modal here console.error('O nome da nova coluna não pode estar vazio.'); return; } onAddColumn(newColumnName); setNewColumnName(''); }; return (
{/* Click outside to close */}
e.stopPropagation()}> {/* Prevent click from bubbling */}

Gerenciar Colunas Kanban

{/* Add New Column */}

Adicionar Nova Coluna

setNewColumnName(e.target.value)} className="flex-grow p-2 bg-gray-600 text-gray-100 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* Existing Columns List */}

Colunas Existentes

{kanbanColumns.length === 0 ? (

Nenhuma coluna configurada.

) : (
    {kanbanColumns.map((column, index) => (
  • {editingColumnId === column.id ? ( setEditingColumnName(e.target.value)} className="flex-grow p-2 bg-gray-700 text-gray-100 border border-blue-500 rounded-md focus:outline-none" /> ) : ( {column.name} )}
    {editingColumnId === column.id ? ( <> ) : ( <> )}
  • ))}
)}
); } function TagManagementModal({ availableTags, onAddTag, onRemoveTag, onClose }) { return (
{/* Click outside to close */}
e.stopPropagation()}> {/* Prevent click from bubbling */}

Gerenciar Tags

); } function TagEditor({ availableTags, onAddTag, onRemoveTag }) { const [newTag, setNewTag] = useState(''); const [localMessage, setLocalMessage] = useState(''); const handleAdd = async () => { if (newTag.trim() === '') { setLocalMessage('O nome da tag não pode estar vazio.'); return; } if (availableTags.includes(newTag.trim())) { setLocalMessage(`A tag "${newTag.trim()}" já existe.`); return; } await onAddTag(newTag.trim()); setNewTag(''); setLocalMessage(`Tag "${newTag.trim()}" adicionada!`); setTimeout(() => setLocalMessage(''), 3000); }; const handleRemove = async (tagName) => { await onRemoveTag(tagName); setLocalMessage(`Tag "${tagName}" removida!`); setTimeout(() => setLocalMessage(''), 3000); }; return (

Adicionar/Remover Tags

{ setNewTag(e.target.value); setLocalMessage(''); }} className="flex-grow p-2 bg-gray-600 text-gray-100 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
{localMessage && (

{localMessage}

)}
{availableTags.map(tag => ( {tag} ))}
); } function TagInput({ selectedTags, availableTags, onTagChange, isFilter = false }) { const [inputValue, setInputValue] = useState(''); const [showSuggestions, setShowSuggestions] = useState(false); const filteredSuggestions = useMemo(() => { const lowerInput = inputValue.toLowerCase(); return availableTags.filter(tag => tag.toLowerCase().includes(lowerInput) && !selectedTags.includes(tag) ).slice(0, 5); // Limit suggestions to 5 }, [inputValue, availableTags, selectedTags]); const handleInputChange = (e) => { setInputValue(e.target.value); setShowSuggestions(true); }; const handleAddTag = (tagToAdd) => { if (!selectedTags.includes(tagToAdd)) { onTagChange([...selectedTags, tagToAdd]); } setInputValue(''); setShowSuggestions(false); }; const handleRemoveTag = (tagToRemove) => { onTagChange(selectedTags.filter(tag => tag !== tagToRemove)); }; const handleKeyDown = (e) => { if (e.key === 'Enter' && inputValue.trim() !== '') { e.preventDefault(); // Prevent form submission const tagToAdd = inputValue.trim(); if (!selectedTags.includes(tagToAdd) && availableTags.includes(tagToAdd)) { handleAddTag(tagToAdd); } else if (!selectedTags.includes(tagToAdd) && !availableTags.includes(tagToAdd)) { // Optionally allow creating new tags directly from here if desired // For now, only allow adding existing tags via Enter // You could call a prop like onCreateNewTag(tagToAdd) here } } else if (e.key === 'Backspace' && inputValue === '' && selectedTags.length > 0) { // Remove last tag if input is empty and backspace is pressed handleRemoveTag(selectedTags[selectedTags.length - 1]); } }; return (
document.getElementById('tag-input-field').focus()}> {selectedTags.map(tag => ( {tag} ))} setShowSuggestions(true)} onBlur={() => setTimeout(() => setShowSuggestions(false), 100)} // Delay to allow click on suggestion placeholder={selectedTags.length === 0 ? "Adicionar tags..." : ""} className="flex-grow bg-transparent text-gray-100 placeholder-gray-400 focus:outline-none min-w-[100px]" />
{showSuggestions && filteredSuggestions.length > 0 && (
    {filteredSuggestions.map(tag => (
  • handleAddTag(tag)} // Use onMouseDown to prevent blur before click className="p-2 cursor-pointer hover:bg-gray-600 text-gray-200" > {tag}
  • ))}
)}
); } function SourceManagementModal({ availableSources, onAddSource, onRemoveSource, onUpdateSource, onClose }) { const [newSourceName, setNewSourceName] = useState(''); const [editingSource, setEditingSource] = useState(null); const [editingSourceName, setEditingSourceName] = useState(''); const [localMessage, setLocalMessage] = useState(''); const handleAdd = async () => { if (newSourceName.trim() === '') { setLocalMessage('O nome da origem não pode estar vazio.'); return; } if (availableSources.includes(newSourceName.trim())) { setLocalMessage(`A origem "${newSourceName.trim()}" já existe.`); return; } await onAddSource(newSourceName.trim()); setNewSourceName(''); setLocalMessage(`Origem "${newSourceName.trim()}" adicionada!`); setTimeout(() => setLocalMessage(''), 3000); }; const handleRemove = async (sourceName) => { await onRemoveSource(sourceName); setLocalMessage(`Origem "${sourceName}" removida!`); setTimeout(() => setLocalMessage(''), 3000); }; const handleEditClick = (source) => { setEditingSource(source); setEditingSourceName(source); }; const handleSaveEdit = async () => { if (editingSourceName.trim() === '') { setLocalMessage('O nome da origem não pode estar vazio.'); return; } if (editingSource && editingSourceName !== editingSource) { if (availableSources.includes(editingSourceName.trim())) { setLocalMessage(`A origem "${editingSourceName.trim()}" já existe.`); return; } await onUpdateSource(editingSource, editingSourceName.trim()); setLocalMessage(`Origem "${editingSource}" atualizada para "${editingSourceName}"!`); setTimeout(() => setLocalMessage(''), 3000); } setEditingSource(null); setEditingSourceName(''); }; const handleCancelEdit = () => { setEditingSource(null); setEditingSourceName(''); }; return (
{/* Click outside to close */}
e.stopPropagation()}> {/* Prevent click from bubbling */}

Gerenciar Origens

Adicionar Nova Origem

{ setNewSourceName(e.target.value); setLocalMessage(''); }} className="flex-grow p-2 bg-gray-600 text-gray-100 border border-gray-500 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />

Origens Existentes

{availableSources.length === 0 ? (

Nenhuma origem configurada.

) : (
    {availableSources.map(source => (
  • {editingSource === source ? ( setEditingSourceName(e.target.value)} className="flex-grow p-2 bg-gray-700 text-gray-100 border border-blue-500 rounded-md focus:outline-none" /> ) : ( {source} )}
    {editingSource === source ? ( <> ) : ( <> )}
  • ))}
)}
{localMessage && (

{localMessage}

)}
); } export default App;