feat: synchronize dashboard origins with management page and add integration endpoints
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m53s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m53s
- Updated Dashboard origin chart to strictly reflect only configured origins, grouping unmapped data into an 'Outros' category. - Added GET /api/integration/funnels and GET /api/integration/origins endpoints to allow external AIs to dynamically map stages and lead sources.
This commit is contained in:
@@ -626,12 +626,18 @@ apiRouter.get('/origins', async (req, res) => {
|
|||||||
const gid = `origrp_${crypto.randomUUID().split('-')[0]}`;
|
const gid = `origrp_${crypto.randomUUID().split('-')[0]}`;
|
||||||
await pool.query('INSERT INTO origin_groups (id, tenant_id, name) VALUES (?, ?, ?)', [gid, effectiveTenantId, 'Origens Padrão']);
|
await pool.query('INSERT INTO origin_groups (id, tenant_id, name) VALUES (?, ?, ?)', [gid, effectiveTenantId, 'Origens Padrão']);
|
||||||
|
|
||||||
const defaultOrigins = ['WhatsApp', 'Instagram', 'Website', 'LinkedIn', 'Indicação'];
|
const defaultOrigins = [
|
||||||
for (const name of defaultOrigins) {
|
{ name: 'WhatsApp', color: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800' },
|
||||||
|
{ name: 'Instagram', color: 'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-400 dark:border-pink-800' },
|
||||||
|
{ name: 'Website', color: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800' },
|
||||||
|
{ name: 'LinkedIn', color: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800' },
|
||||||
|
{ name: 'Indicação', color: 'bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800' }
|
||||||
|
];
|
||||||
|
for (const origin of defaultOrigins) {
|
||||||
const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`;
|
const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`;
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'INSERT INTO origin_items (id, origin_group_id, name) VALUES (?, ?, ?)',
|
'INSERT INTO origin_items (id, origin_group_id, name, color_class) VALUES (?, ?, ?, ?)',
|
||||||
[oid, gid, name]
|
[oid, gid, origin.name, origin.color]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,25 +192,45 @@ export const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
// --- Chart Data: Origin ---
|
// --- Chart Data: Origin ---
|
||||||
const originData = useMemo(() => {
|
const originData = useMemo(() => {
|
||||||
const origins = data.reduce((acc, curr) => {
|
const counts = data.reduce((acc, curr) => {
|
||||||
acc[curr.origin] = (acc[curr.origin] || 0) + 1;
|
acc[curr.origin] = (acc[curr.origin] || 0) + 1;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
return (Object.entries(origins) as [string, number][])
|
if (originDefs.length > 0) {
|
||||||
.map(([name, value]) => {
|
const activeOrigins = originDefs.map(def => {
|
||||||
const def = originDefs.find(o => o.name === name);
|
let hexColor = '#71717a'; // Default zinc
|
||||||
// Extract base color (e.g. "red" from "bg-red-100 text-red-700...")
|
if (def.color_class) {
|
||||||
let hexColor = '';
|
|
||||||
if (def && def.color_class) {
|
|
||||||
const match = def.color_class.match(/bg-([a-z]+)-\d+/);
|
const match = def.color_class.match(/bg-([a-z]+)-\d+/);
|
||||||
if (match && tailwindToHex[match[1]]) {
|
if (match && tailwindToHex[match[1]]) {
|
||||||
hexColor = tailwindToHex[match[1]];
|
hexColor = tailwindToHex[match[1]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { name, value, hexColor };
|
return {
|
||||||
})
|
name: def.name,
|
||||||
.sort((a, b) => b.value - a.value);
|
value: counts[def.name] || 0,
|
||||||
|
hexColor
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate "Outros" for data that doesn't match current active origins
|
||||||
|
const activeNames = new Set(originDefs.map(d => d.name));
|
||||||
|
const othersValue = (Object.entries(counts) as [string, number][])
|
||||||
|
.filter(([name]) => !activeNames.has(name))
|
||||||
|
.reduce((sum, [_, val]) => sum + val, 0);
|
||||||
|
|
||||||
|
if (othersValue > 0) {
|
||||||
|
activeOrigins.push({
|
||||||
|
name: 'Outros',
|
||||||
|
value: othersValue,
|
||||||
|
hexColor: '#94a3b8' // Gray-400
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeOrigins.sort((a, b) => b.value - a.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return []; // No definitions = No chart (matches funnel behavior)
|
||||||
}, [data, originDefs]);
|
}, [data, originDefs]);
|
||||||
|
|
||||||
// --- Table Data: Sellers Ranking ---
|
// --- Table Data: Sellers Ranking ---
|
||||||
|
|||||||
Reference in New Issue
Block a user