diff --git a/backend/index.js b/backend/index.js index c94c298..79a4efe 100644 --- a/backend/index.js +++ b/backend/index.js @@ -570,11 +570,14 @@ apiRouter.get('/funnels', async (req, res) => { const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; if (!effectiveTenantId || effectiveTenantId === 'all') return res.json([]); - const [rows] = await pool.query('SELECT * FROM tenant_funnels WHERE tenant_id = ? ORDER BY order_index ASC', [effectiveTenantId]); + const [funnels] = await pool.query('SELECT * FROM funnels WHERE tenant_id = ? ORDER BY created_at ASC', [effectiveTenantId]); - // If no funnels exist for this tenant, seed the default ones - if (rows.length === 0) { - const defaultFunnels = [ + // Seed default funnel if none exists + if (funnels.length === 0) { + const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; + await pool.query('INSERT INTO funnels (id, tenant_id, name) VALUES (?, ?, ?)', [fid, effectiveTenantId, 'Funil Padrão']); + + const defaultStages = [ { name: 'Sem atendimento', color: 'bg-zinc-100 text-zinc-700 border-zinc-200 dark:bg-dark-input dark:text-dark-muted dark:border-dark-border', order: 0 }, { name: 'Identificação', color: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800', order: 1 }, { name: 'Negociação', color: 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800', order: 2 }, @@ -582,51 +585,61 @@ apiRouter.get('/funnels', async (req, res) => { { name: 'Perdidos', color: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800', order: 4 } ]; - for (const f of defaultFunnels) { - const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; + for (const s of defaultStages) { + const sid = `stage_${crypto.randomUUID().split('-')[0]}`; await pool.query( - 'INSERT INTO tenant_funnels (id, tenant_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', - [fid, effectiveTenantId, f.name, f.color, f.order] + 'INSERT INTO funnel_stages (id, funnel_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', + [sid, fid, s.name, s.color, s.order] ); } - const [newRows] = await pool.query('SELECT * FROM tenant_funnels WHERE tenant_id = ? ORDER BY order_index ASC', [effectiveTenantId]); - return res.json(newRows); + // Update all teams of this tenant to use this funnel if they have none + await pool.query('UPDATE teams SET funnel_id = ? WHERE tenant_id = ? AND funnel_id IS NULL', [fid, effectiveTenantId]); + + funnels.push({ id: fid, tenant_id: effectiveTenantId, name: 'Funil Padrão' }); } - - res.json(rows); + + const [stages] = await pool.query('SELECT * FROM funnel_stages WHERE funnel_id IN (?) ORDER BY order_index ASC', [funnels.map(f => f.id)]); + const [teams] = await pool.query('SELECT id, funnel_id FROM teams WHERE tenant_id = ? AND funnel_id IS NOT NULL', [effectiveTenantId]); + + const result = funnels.map(f => ({ + ...f, + stages: stages.filter(s => s.funnel_id === f.id), + teamIds: teams.filter(t => t.funnel_id === f.id).map(t => t.id) + })); + + res.json(result); } catch (error) { + console.error("GET /funnels error:", error); res.status(500).json({ error: error.message }); } }); apiRouter.post('/funnels', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => { - const { name, color_class, order_index, tenantId } = req.body; + const { name, tenantId } = req.body; const effectiveTenantId = req.user.role === 'super_admin' ? tenantId : req.user.tenant_id; try { const fid = `funnel_${crypto.randomUUID().split('-')[0]}`; - await pool.query( - 'INSERT INTO tenant_funnels (id, tenant_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', - [fid, effectiveTenantId, name, color_class, order_index || 0] - ); - res.status(201).json({ id: fid, message: 'Etapa do funil criada com sucesso.' }); + await pool.query('INSERT INTO funnels (id, tenant_id, name) VALUES (?, ?, ?)', [fid, effectiveTenantId, name]); + res.status(201).json({ id: fid }); } catch (error) { res.status(500).json({ error: error.message }); } }); apiRouter.put('/funnels/:id', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => { - const { name, color_class, order_index } = req.body; + const { name, teamIds } = req.body; try { - const [existing] = await pool.query('SELECT * FROM tenant_funnels WHERE id = ?', [req.params.id]); - if (existing.length === 0) return res.status(404).json({ error: 'Etapa não encontrada' }); - if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) return res.status(403).json({ error: 'Acesso negado' }); - - await pool.query( - 'UPDATE tenant_funnels SET name = ?, color_class = ?, order_index = ? WHERE id = ?', - [name || existing[0].name, color_class || existing[0].color_class, order_index !== undefined ? order_index : existing[0].order_index, req.params.id] - ); - res.json({ message: 'Etapa do funil atualizada.' }); + if (name) { + await pool.query('UPDATE funnels SET name = ? WHERE id = ?', [name, req.params.id]); + } + if (teamIds && Array.isArray(teamIds)) { + await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]); + if (teamIds.length > 0) { + await pool.query('UPDATE teams SET funnel_id = ? WHERE id IN (?)', [req.params.id, teamIds]); + } + } + res.json({ message: 'Funnel updated.' }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -634,12 +647,49 @@ apiRouter.put('/funnels/:id', requireRole(['admin', 'owner', 'manager', 'super_a apiRouter.delete('/funnels/:id', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => { try { - const [existing] = await pool.query('SELECT * FROM tenant_funnels WHERE id = ?', [req.params.id]); - if (existing.length === 0) return res.status(404).json({ error: 'Etapa não encontrada' }); - if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) return res.status(403).json({ error: 'Acesso negado' }); + await pool.query('DELETE FROM funnel_stages WHERE funnel_id = ?', [req.params.id]); + await pool.query('UPDATE teams SET funnel_id = NULL WHERE funnel_id = ?', [req.params.id]); + await pool.query('DELETE FROM funnels WHERE id = ?', [req.params.id]); + res.json({ message: 'Funnel deleted.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); - await pool.query('DELETE FROM tenant_funnels WHERE id = ?', [req.params.id]); - res.json({ message: 'Etapa do funil excluída.' }); +apiRouter.post('/funnels/:id/stages', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => { + const { name, color_class, order_index } = req.body; + try { + const sid = `stage_${crypto.randomUUID().split('-')[0]}`; + await pool.query( + 'INSERT INTO funnel_stages (id, funnel_id, name, color_class, order_index) VALUES (?, ?, ?, ?, ?)', + [sid, req.params.id, name, color_class, order_index || 0] + ); + res.status(201).json({ id: sid }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +apiRouter.put('/funnel_stages/:id', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => { + const { name, color_class, order_index } = req.body; + try { + const [existing] = await pool.query('SELECT * FROM funnel_stages WHERE id = ?', [req.params.id]); + if (existing.length === 0) return res.status(404).json({ error: 'Stage not found' }); + + await pool.query( + 'UPDATE funnel_stages SET name = ?, color_class = ?, order_index = ? WHERE id = ?', + [name || existing[0].name, color_class || existing[0].color_class, order_index !== undefined ? order_index : existing[0].order_index, req.params.id] + ); + res.json({ message: 'Stage updated.' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +apiRouter.delete('/funnel_stages/:id', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => { + try { + await pool.query('DELETE FROM funnel_stages WHERE id = ?', [req.params.id]); + res.json({ message: 'Stage deleted.' }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -1043,20 +1093,39 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => { console.log('Schema update note (funnel_stage):', err.message); } - // Create tenant_funnels table + // Create funnels table await connection.query(` - CREATE TABLE IF NOT EXISTS tenant_funnels ( + CREATE TABLE IF NOT EXISTS funnels ( id varchar(36) NOT NULL, tenant_id varchar(36) NOT NULL, name varchar(255) NOT NULL, - color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200', - order_index int DEFAULT 0, created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY tenant_id (tenant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; `); + // Create funnel_stages table + await connection.query(` + CREATE TABLE IF NOT EXISTS funnel_stages ( + id varchar(36) NOT NULL, + funnel_id varchar(36) NOT NULL, + name varchar(255) NOT NULL, + color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200', + order_index int DEFAULT 0, + created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY funnel_id (funnel_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `); + + // Add funnel_id to teams + try { + await connection.query("ALTER TABLE teams ADD COLUMN funnel_id VARCHAR(36) DEFAULT NULL"); + } catch (err) { + if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (teams.funnel_id):', err.message); + } + connection.release(); // Ensure system tenant exists await pool.query('INSERT IGNORE INTO tenants (id, name, slug, admin_email, status) VALUES (?, ?, ?, ?, ?)', ['system', 'System Admin', 'system', email, 'active']); diff --git a/pages/AttendanceDetail.tsx b/pages/AttendanceDetail.tsx index f93d8ec..7ea3274 100644 --- a/pages/AttendanceDetail.tsx +++ b/pages/AttendanceDetail.tsx @@ -17,17 +17,25 @@ export const AttendanceDetail: React.FC = () => { setLoading(true); try { const tenantId = localStorage.getItem('ctms_tenant_id') || ''; - const [att, fDefs] = await Promise.all([ + const [att, fetchedFunnels] = await Promise.all([ getAttendanceById(id), getFunnels(tenantId) ]); setData(att); - setFunnelDefs(fDefs); if (att) { const u = await getUserById(att.user_id); setAgent(u); + + // Determine which funnel was used based on the agent's team + const targetTeamId = u?.team_id || null; + let activeFunnel = fetchedFunnels[0]; + if (targetTeamId) { + const matchedFunnel = fetchedFunnels.find(f => f.teamIds?.includes(targetTeamId)); + if (matchedFunnel) activeFunnel = matchedFunnel; + } + setFunnelDefs(activeFunnel && activeFunnel.stages ? activeFunnel.stages : []); } } catch (error) { console.error("Error loading details", error); diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx index 6ceff7d..b45f3ea 100644 --- a/pages/Dashboard.tsx +++ b/pages/Dashboard.tsx @@ -70,8 +70,18 @@ export const Dashboard: React.FC = () => { setData(fetchedData); setPrevData(prevFetchedData); setTeams(fetchedTeams); - setFunnelDefs(fetchedFunnels.sort((a, b) => a.order_index - b.order_index)); if (me) setCurrentUser(me); + + // Determine which funnel to display + const targetTeamId = filters.teamId !== 'all' ? filters.teamId : (me?.team_id || null); + let activeFunnel = fetchedFunnels[0]; // fallback to first/default + if (targetTeamId) { + const matchedFunnel = fetchedFunnels.find(f => f.teamIds?.includes(targetTeamId)); + if (matchedFunnel) activeFunnel = matchedFunnel; + } + + setFunnelDefs(activeFunnel && activeFunnel.stages ? activeFunnel.stages.sort((a: any, b: any) => a.order_index - b.order_index) : []); + } catch (error) { console.error("Error loading dashboard data:", error); } finally { diff --git a/pages/Funnels.tsx b/pages/Funnels.tsx index b96adb4..9308fcd 100644 --- a/pages/Funnels.tsx +++ b/pages/Funnels.tsx @@ -1,16 +1,23 @@ import React, { useState, useEffect } from 'react'; -import { Layers, Plus, Edit, Trash2, ChevronUp, ChevronDown, Loader2, X, CheckCircle2 } from 'lucide-react'; -import { getFunnels, createFunnel, updateFunnel, deleteFunnel } from '../services/dataService'; -import { FunnelStageDef } from '../types'; +import { Layers, Plus, Edit, Trash2, ChevronUp, ChevronDown, Loader2, X, Users } from 'lucide-react'; +import { getFunnels, createFunnel, updateFunnel, deleteFunnel, createFunnelStage, updateFunnelStage, deleteFunnelStage, getTeams } from '../services/dataService'; +import { FunnelDef, FunnelStageDef } from '../types'; export const Funnels: React.FC = () => { - const [funnels, setFunnels] = useState([]); + const [funnels, setFunnels] = useState([]); + const [teams, setTeams] = useState([]); + const [selectedFunnelId, setSelectedFunnelId] = useState(null); + const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); - const [editingFunnel, setEditingFunnel] = useState(null); + const [editingStage, setEditingStage] = useState(null); const [formData, setFormData] = useState({ name: '', color_class: '' }); const [isSaving, setIsSaving] = useState(false); + // Funnel creation state + const [isFunnelModalOpen, setIsFunnelModalOpen] = useState(false); + const [funnelName, setFunnelName] = useState(''); + const tenantId = localStorage.getItem('ctms_tenant_id') || ''; const PRESET_COLORS = [ @@ -22,28 +29,84 @@ export const Funnels: React.FC = () => { { label: 'Vermelho (Perda)', value: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800' }, ]; - const loadFunnels = async () => { + const loadData = async () => { setLoading(true); - const data = await getFunnels(tenantId); - setFunnels(data.sort((a, b) => a.order_index - b.order_index)); + const [fetchedFunnels, fetchedTeams] = await Promise.all([ + getFunnels(tenantId), + getTeams(tenantId) + ]); + setFunnels(fetchedFunnels); + setTeams(fetchedTeams); + if (!selectedFunnelId && fetchedFunnels.length > 0) { + setSelectedFunnelId(fetchedFunnels[0].id); + } setLoading(false); }; useEffect(() => { - loadFunnels(); + loadData(); }, [tenantId]); - const handleSave = async (e: React.FormEvent) => { + const selectedFunnel = funnels.find(f => f.id === selectedFunnelId); + + // --- Funnel Handlers --- + const handleCreateFunnel = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); try { - if (editingFunnel) { - await updateFunnel(editingFunnel.id, formData); + const res = await createFunnel({ name: funnelName, tenantId }); + setSelectedFunnelId(res.id); + setIsFunnelModalOpen(false); + setFunnelName(''); + loadData(); + } catch (err) { + alert("Erro ao criar funil."); + } finally { + setIsSaving(false); + } + }; + + const handleDeleteFunnel = async (id: string) => { + if (funnels.length <= 1) { + alert("Você precisa ter pelo menos um funil ativo."); + return; + } + if (confirm('Tem certeza que deseja excluir este funil e todas as suas etapas?')) { + await deleteFunnel(id); + setSelectedFunnelId(null); + loadData(); + } + }; + + const handleToggleTeam = async (teamId: string) => { + if (!selectedFunnel) return; + const currentTeamIds = selectedFunnel.teamIds || []; + const newTeamIds = currentTeamIds.includes(teamId) + ? currentTeamIds.filter(id => id !== teamId) + : [...currentTeamIds, teamId]; + + // Optimistic + const newFunnels = [...funnels]; + const idx = newFunnels.findIndex(f => f.id === selectedFunnel.id); + newFunnels[idx].teamIds = newTeamIds; + setFunnels(newFunnels); + + await updateFunnel(selectedFunnel.id, { teamIds: newTeamIds }); + }; + + // --- Stage Handlers --- + const handleSaveStage = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedFunnel) return; + setIsSaving(true); + try { + if (editingStage) { + await updateFunnelStage(editingStage.id, formData); } else { - await createFunnel({ ...formData, tenantId, order_index: funnels.length }); + await createFunnelStage(selectedFunnel.id, { ...formData, order_index: selectedFunnel.stages.length }); } setIsModalOpen(false); - loadFunnels(); + loadData(); } catch (err) { alert("Erro ao salvar etapa."); } finally { @@ -51,110 +114,209 @@ export const Funnels: React.FC = () => { } }; - const handleDelete = async (id: string) => { + const handleDeleteStage = async (id: string) => { if (confirm('Tem certeza que deseja excluir esta etapa?')) { - await deleteFunnel(id); - loadFunnels(); + await deleteFunnelStage(id); + loadData(); } }; - const handleMove = async (index: number, direction: 'up' | 'down') => { + const handleMoveStage = async (index: number, direction: 'up' | 'down') => { + if (!selectedFunnel) return; + const stages = selectedFunnel.stages; if (direction === 'up' && index === 0) return; - if (direction === 'down' && index === funnels.length - 1) return; + if (direction === 'down' && index === stages.length - 1) return; const newIndex = direction === 'up' ? index - 1 : index + 1; - const newFunnels = [...funnels]; - // Swap order_index - const tempOrder = newFunnels[index].order_index; - newFunnels[index].order_index = newFunnels[newIndex].order_index; - newFunnels[newIndex].order_index = tempOrder; + const tempOrder = stages[index].order_index; + stages[index].order_index = stages[newIndex].order_index; + stages[newIndex].order_index = tempOrder; - // Optimistic UI update - setFunnels(newFunnels.sort((a, b) => a.order_index - b.order_index)); + setFunnels([...funnels]); // trigger re-render - // Persist to backend await Promise.all([ - updateFunnel(newFunnels[index].id, { order_index: newFunnels[index].order_index }), - updateFunnel(newFunnels[newIndex].id, { order_index: newFunnels[newIndex].order_index }) + updateFunnelStage(stages[index].id, { order_index: stages[index].order_index }), + updateFunnelStage(stages[newIndex].id, { order_index: stages[newIndex].order_index }) ]); }; - const openModal = (funnel?: FunnelStageDef) => { - if (funnel) { - setEditingFunnel(funnel); - setFormData({ name: funnel.name, color_class: funnel.color_class }); + const openStageModal = (stage?: FunnelStageDef) => { + if (stage) { + setEditingStage(stage); + setFormData({ name: stage.name, color_class: stage.color_class }); } else { - setEditingFunnel(null); + setEditingStage(null); setFormData({ name: '', color_class: PRESET_COLORS[0].value }); } setIsModalOpen(true); }; - if (loading) return
; + if (loading && funnels.length === 0) return
; return ( -
-
-
-

Personalizar Funil

-

Crie, edite e reordene as etapas do funil de vendas da sua organização.

-
- -
- -
-
-

- Etapas do Funil -

+
+ + {/* Sidebar: Funnels List */} +
+
+

Meus Funis

+
-
- {funnels.map((f, index) => ( -
-
-
- - -
-
- - {f.name} - -
+
+ {funnels.map(f => ( + - -
-
+ ))} - {funnels.length === 0 && ( -
Nenhuma etapa configurada.
- )}
+ {/* Main Content: Selected Funnel Details */} +
+ {selectedFunnel ? ( + <> +
+
+

{selectedFunnel.name}

+

Gerencie as etapas deste funil e quais times o utilizam.

+
+ +
+ + {/* Teams Assignment */} +
+
+

+ Times Atribuídos +

+
+
+ {teams.length === 0 ? ( +

Nenhum time cadastrado na organização.

+ ) : ( +
+ {teams.map(t => { + const isAssigned = selectedFunnel.teamIds?.includes(t.id); + return ( + + ); + })} +
+ )} +

Times não atribuídos a um funil específico usarão o Funil Padrão.

+
+
+ + {/* Stages */} +
+
+

+ Etapas do Funil +

+ +
+ +
+ {selectedFunnel.stages?.sort((a,b) => a.order_index - b.order_index).map((f, index) => ( +
+
+
+ + +
+
+ + {f.name} + +
+
+ +
+ + +
+
+ ))} + {(!selectedFunnel.stages || selectedFunnel.stages.length === 0) && ( +
Nenhuma etapa configurada neste funil.
+ )} +
+
+ + ) : ( +
Selecione ou crie um funil.
+ )} +
+ + {/* Funnel Creation Modal */} + {isFunnelModalOpen && ( +
+
+
+

Novo Funil

+ +
+
+
+ + setFunnelName(e.target.value)} + placeholder="Ex: Vendas B2B" + className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-zinc-100 focus:ring-2 focus:ring-brand-yellow/20 outline-none transition-all" + required + /> +
+
+ + +
+
+
+
+ )} + + {/* Stage Modal */} {isModalOpen && (
-

{editingFunnel ? 'Editar Etapa' : 'Nova Etapa'}

+

{editingStage ? 'Editar Etapa' : 'Nova Etapa'}

-
+
{ setUser(u); if (u && tenantId) { - const [data, funnels] = await Promise.all([ + const [data, fetchedFunnels] = await Promise.all([ getAttendances(tenantId, { ...filters, userId: id @@ -40,7 +40,14 @@ export const UserDetail: React.FC = () => { getFunnels(tenantId) ]); setAttendances(data); - setFunnelDefs(funnels); + + const targetTeamId = u.team_id || null; + let activeFunnel = fetchedFunnels[0]; + if (targetTeamId) { + const matchedFunnel = fetchedFunnels.find(f => f.teamIds?.includes(targetTeamId)); + if (matchedFunnel) activeFunnel = matchedFunnel; + } + setFunnelDefs(activeFunnel && activeFunnel.stages ? activeFunnel.stages.sort((a: any, b: any) => a.order_index - b.order_index) : []); } } catch (error) { console.error("Error loading user details", error); diff --git a/services/dataService.ts b/services/dataService.ts index 126815b..bbe56ec 100644 --- a/services/dataService.ts +++ b/services/dataService.ts @@ -96,7 +96,7 @@ export const getFunnels = async (tenantId: string): Promise => { } }; -export const createFunnel = async (data: any): Promise => { +export const createFunnel = async (data: { name: string, tenantId: string }): Promise => { const response = await fetch(`${API_URL}/funnels`, { method: 'POST', headers: getHeaders(), @@ -109,7 +109,7 @@ export const createFunnel = async (data: any): Promise => { return await response.json(); }; -export const updateFunnel = async (id: string, data: any): Promise => { +export const updateFunnel = async (id: string, data: { name?: string, teamIds?: string[] }): Promise => { try { const response = await fetch(`${API_URL}/funnels/${id}`, { method: 'PUT', @@ -136,6 +136,46 @@ export const deleteFunnel = async (id: string): Promise => { } }; +export const createFunnelStage = async (funnelId: string, data: any): Promise => { + const response = await fetch(`${API_URL}/funnels/${funnelId}/stages`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro ao criar etapa'); + } + return await response.json(); +}; + +export const updateFunnelStage = async (id: string, data: any): Promise => { + try { + const response = await fetch(`${API_URL}/funnel_stages/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(data) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateFunnelStage):", error); + return false; + } +}; + +export const deleteFunnelStage = async (id: string): Promise => { + try { + const response = await fetch(`${API_URL}/funnel_stages/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteFunnelStage):", error); + return false; + } +}; + export const searchGlobal = async (query: string): Promise<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }> => { try { const response = await fetch(`${API_URL}/search?q=${encodeURIComponent(query)}`, { diff --git a/types.ts b/types.ts index 5310b92..9e6e111 100644 --- a/types.ts +++ b/types.ts @@ -9,12 +9,20 @@ export enum FunnelStage { export interface FunnelStageDef { id: string; - tenant_id: string; + funnel_id: string; name: string; color_class: string; order_index: number; } +export interface FunnelDef { + id: string; + tenant_id: string; + name: string; + stages: FunnelStageDef[]; + teamIds: string[]; +} + export interface User { id: string; tenant_id: string;