feat: implement relational lead origins with team assignments
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m51s

- Dropped simple origins table in favor of origin_groups and origin_items to match the Funnels architecture.

- Added origin_group_id to teams table to assign specific origins to specific teams.

- Updated /admin/origins page to support creating origin groups, adding origin items to them, and assigning teams to groups.

- Updated Dashboard and UserDetail pages to dynamically load the exact origin items belonging to the active team/user.
This commit is contained in:
Cauê Faleiros
2026-03-18 11:18:30 -03:00
parent 64c4ca8fb5
commit 1d3315a1d0
8 changed files with 641 additions and 27 deletions

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useParams, Link } from 'react-router-dom';
import { getAttendances, getUserById, getFunnels } from '../services/dataService';
import { Attendance, User, FunnelStage, DashboardFilter, FunnelStageDef } from '../types';
import { getAttendances, getUserById, getFunnels, getOrigins } from '../services/dataService';
import { Attendance, User, FunnelStage, DashboardFilter, FunnelStageDef, OriginItemDef } from '../types';
import { ArrowLeft, Mail, Phone, Clock, MessageSquare, ChevronLeft, ChevronRight, Eye, Filter } from 'lucide-react';
import { DateRangePicker } from '../components/DateRangePicker';
@@ -12,6 +12,7 @@ export const UserDetail: React.FC = () => {
const [user, setUser] = useState<User | undefined>();
const [attendances, setAttendances] = useState<Attendance[]>([]);
const [funnelDefs, setFunnelDefs] = useState<FunnelStageDef[]>([]);
const [originDefs, setOriginDefs] = useState<OriginItemDef[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<DashboardFilter>({
@@ -32,12 +33,13 @@ export const UserDetail: React.FC = () => {
setUser(u);
if (u && tenantId) {
const [data, fetchedFunnels] = await Promise.all([
const [data, fetchedFunnels, fetchedOrigins] = await Promise.all([
getAttendances(tenantId, {
...filters,
userId: id
}),
getFunnels(tenantId)
getFunnels(tenantId),
getOrigins(tenantId)
]);
setAttendances(data);
@@ -48,6 +50,13 @@ export const UserDetail: React.FC = () => {
if (matchedFunnel) activeFunnel = matchedFunnel;
}
setFunnelDefs(activeFunnel && activeFunnel.stages ? activeFunnel.stages.sort((a: any, b: any) => a.order_index - b.order_index) : []);
let activeOriginGroup = fetchedOrigins[0];
if (targetTeamId) {
const matchedOrigin = fetchedOrigins.find(o => o.teamIds?.includes(targetTeamId));
if (matchedOrigin) activeOriginGroup = matchedOrigin;
}
setOriginDefs(activeOriginGroup && activeOriginGroup.items ? activeOriginGroup.items : []);
}
} catch (error) {
console.error("Error loading user details", error);
@@ -154,19 +163,18 @@ export const UserDetail: React.FC = () => {
<option key={stage} value={stage}>{stage}</option>
))}
</select>
<select
<select
className="bg-zinc-50 dark:bg-dark-bg border border-zinc-200 dark:border-dark-border px-3 py-2 rounded-lg text-sm text-zinc-700 dark:text-zinc-200 outline-none focus:ring-2 focus:ring-brand-yellow/20 cursor-pointer hover:border-zinc-300 dark:hover:border-zinc-700 transition-all"
value={filters.origin}
onChange={(e) => handleFilterChange('origin', e.target.value)}
>
<option value="all">Todas Origens</option>
<option value="WhatsApp">WhatsApp</option>
<option value="Instagram">Instagram</option>
<option value="Website">Website</option>
<option value="LinkedIn">LinkedIn</option>
<option value="Indicação">Indicação</option>
</select>
</div>
{originDefs.length > 0 ? originDefs.map(o => (
<option key={o.id} value={o.name}>{o.name}</option>
)) : ['WhatsApp', 'Instagram', 'Website', 'LinkedIn', 'Indicação'].map(o => (
<option key={o} value={o}>{o}</option>
))}
</select> </div>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">