fix: allow origin colors to be edited and display correctly in dashboard

- Fixed database initialization where default origins were seeded without color_classes.

- Added a visual color picker to the Origens admin page to allow users to assign colors to origin tags.

- Updated Dashboard Pie Chart to read the color classes correctly and display them.
This commit is contained in:
Cauê Faleiros
2026-03-18 13:43:43 -03:00
parent 1d3315a1d0
commit f11db95a2f
5 changed files with 79 additions and 19 deletions

View File

@@ -699,12 +699,12 @@ apiRouter.delete('/origins/:id', requireRole(['admin', 'owner', 'manager', 'supe
});
apiRouter.post('/origins/:id/items', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => {
const { name } = req.body;
const { name, color_class } = req.body;
try {
const oid = `oriitm_${crypto.randomUUID().split('-')[0]}`;
await pool.query(
'INSERT INTO origin_items (id, origin_group_id, name) VALUES (?, ?, ?)',
[oid, req.params.id, name]
'INSERT INTO origin_items (id, origin_group_id, name, color_class) VALUES (?, ?, ?, ?)',
[oid, req.params.id, name, color_class || 'bg-zinc-100 text-zinc-800 border-zinc-200']
);
res.status(201).json({ id: oid });
} catch (error) {
@@ -713,12 +713,12 @@ apiRouter.post('/origins/:id/items', requireRole(['admin', 'owner', 'manager', '
});
apiRouter.put('/origin_items/:id', requireRole(['admin', 'owner', 'manager', 'super_admin']), async (req, res) => {
const { name } = req.body;
const { name, color_class } = req.body;
try {
const [existing] = await pool.query('SELECT * FROM origin_items WHERE id = ?', [req.params.id]);
if (existing.length === 0) return res.status(404).json({ error: 'Origin item not found' });
await pool.query('UPDATE origin_items SET name = ? WHERE id = ?', [name || existing[0].name, req.params.id]);
await pool.query('UPDATE origin_items SET name = ?, color_class = ? WHERE id = ?', [name || existing[0].name, color_class || existing[0].color_class, req.params.id]);
res.json({ message: 'Origin item updated.' });
} catch (error) {
res.status(500).json({ error: error.message });
@@ -1480,12 +1480,20 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
id varchar(36) NOT NULL,
origin_group_id varchar(36) NOT NULL,
name varchar(255) NOT NULL,
color_class varchar(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200',
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY origin_group_id (origin_group_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// Attempt to add color_class if table already existed without it
try {
await connection.query("ALTER TABLE origin_items ADD COLUMN color_class VARCHAR(255) DEFAULT 'bg-zinc-100 text-zinc-800 border-zinc-200'");
} catch (err) {
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (origin_items.color_class):', err.message);
}
// Add origin_group_id to teams
try {
await connection.query("ALTER TABLE teams ADD COLUMN origin_group_id VARCHAR(36) DEFAULT NULL");

View File

@@ -179,6 +179,17 @@ export const Dashboard: React.FC = () => {
}));
}, [data, funnelDefs]);
const tailwindToHex: Record<string, string> = {
'zinc': '#71717a',
'blue': '#3b82f6',
'purple': '#a855f7',
'green': '#22c55e',
'red': '#ef4444',
'pink': '#ec4899',
'orange': '#f97316',
'yellow': '#eab308'
};
// --- Chart Data: Origin ---
const originData = useMemo(() => {
const origins = data.reduce((acc, curr) => {
@@ -186,11 +197,21 @@ export const Dashboard: React.FC = () => {
return acc;
}, {} as Record<string, number>);
// Ensure type safety for value in sort
return (Object.entries(origins) as [string, number][])
.map(([name, value]) => ({ name, value }))
.map(([name, value]) => {
const def = originDefs.find(o => o.name === name);
// Extract base color (e.g. "red" from "bg-red-100 text-red-700...")
let hexColor = '';
if (def && def.color_class) {
const match = def.color_class.match(/bg-([a-z]+)-\d+/);
if (match && tailwindToHex[match[1]]) {
hexColor = tailwindToHex[match[1]];
}
}
return { name, value, hexColor };
})
.sort((a, b) => b.value - a.value);
}, [data]);
}, [data, originDefs]);
// --- Table Data: Sellers Ranking ---
const sellersRanking = useMemo(() => {
@@ -429,10 +450,9 @@ export const Dashboard: React.FC = () => {
{originData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS.origins[entry.name as keyof typeof COLORS.origins] || COLORS.charts[index % COLORS.charts.length]}
fill={entry.hexColor || COLORS.charts[index % COLORS.charts.length]}
/>
))}
</Pie>
))} </Pie>
<Tooltip
formatter={(value: any) => [value, 'Leads']}
contentStyle={{

View File

@@ -11,7 +11,7 @@ export const Origins: React.FC = () => {
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState<OriginItemDef | null>(null);
const [formData, setFormData] = useState({ name: '' });
const [formData, setFormData] = useState({ name: '', color_class: '' });
const [isSaving, setIsSaving] = useState(false);
// Group creation state
@@ -20,6 +20,15 @@ export const Origins: React.FC = () => {
const tenantId = localStorage.getItem('ctms_tenant_id') || '';
const PRESET_COLORS = [
{ label: 'Cinza', value: 'bg-zinc-100 text-zinc-700 border-zinc-200 dark:bg-dark-input dark:text-dark-muted dark:border-dark-border' },
{ label: 'Azul', value: 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800' },
{ label: 'Roxo', value: 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800' },
{ label: 'Verde', value: 'bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800' },
{ label: 'Vermelho', value: 'bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800' },
{ label: 'Rosa', value: 'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-400 dark:border-pink-800' },
];
const loadData = async () => {
setLoading(true);
const [fetchedGroups, fetchedTeams] = await Promise.all([
@@ -115,10 +124,10 @@ export const Origins: React.FC = () => {
const openItemModal = (item?: OriginItemDef) => {
if (item) {
setEditingItem(item);
setFormData({ name: item.name });
setFormData({ name: item.name, color_class: item.color_class || PRESET_COLORS[0].value });
} else {
setEditingItem(null);
setFormData({ name: '' });
setFormData({ name: '', color_class: PRESET_COLORS[0].value });
}
setIsModalOpen(true);
};
@@ -211,7 +220,9 @@ export const Origins: React.FC = () => {
<div className="divide-y divide-zinc-100 dark:divide-dark-border">
{selectedGroup.items?.map((o) => (
<div key={o.id} className="p-4 px-6 flex items-center justify-between group hover:bg-zinc-50 dark:hover:bg-dark-bg transition-colors">
<div className="font-medium text-zinc-800 dark:text-zinc-200">{o.name}</div>
<span className={`px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide border ${o.color_class || 'bg-zinc-100 text-zinc-700 border-zinc-200'}`}>
{o.name}
</span>
<div className="flex gap-2">
<button onClick={() => openItemModal(o)} className="p-2 text-zinc-400 hover:text-brand-yellow hover:bg-zinc-100 dark:hover:bg-dark-input rounded-lg transition-colors">
@@ -286,6 +297,26 @@ export const Origins: React.FC = () => {
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 dark:text-dark-muted uppercase mb-2 block">Cor Visual</label>
<div className="grid grid-cols-2 gap-2">
{PRESET_COLORS.map((color, i) => (
<label key={i} className={`flex items-center gap-2 p-2 border rounded-lg cursor-pointer transition-all ${formData.color_class === color.value ? 'border-brand-yellow bg-yellow-50/50 dark:bg-yellow-900/10' : 'border-zinc-200 dark:border-dark-border hover:bg-zinc-50 dark:hover:bg-dark-input'}`}>
<input
type="radio"
name="color"
value={color.value}
checked={formData.color_class === color.value}
onChange={(e) => setFormData({...formData, color_class: e.target.value})}
className="sr-only"
/>
<span className={`w-3 h-3 rounded-full border ${color.value.split(' ')[0]} ${color.value.split(' ')[2]}`}></span>
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">{color.label}</span>
</label>
))}
</div>
</div>
<div className="pt-4 flex justify-end gap-3 mt-6 border-t border-zinc-100 dark:border-dark-border pt-6">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-dark-border rounded-lg text-sm font-medium transition-colors">Cancelar</button>
<button type="submit" disabled={isSaving} className="px-6 py-2 bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 rounded-lg text-sm font-bold flex items-center gap-2 hover:opacity-90 transition-all shadow-sm disabled:opacity-70">

View File

@@ -230,7 +230,7 @@ export const deleteOriginGroup = async (id: string): Promise<boolean> => {
}
};
export const createOriginItem = async (groupId: string, data: { name: string }): Promise<any> => {
export const createOriginItem = async (groupId: string, data: { name: string, color_class?: string }): Promise<any> => {
const response = await fetch(`${API_URL}/origins/${groupId}/items`, {
method: 'POST',
headers: getHeaders(),
@@ -243,7 +243,7 @@ export const createOriginItem = async (groupId: string, data: { name: string }):
return await response.json();
};
export const updateOriginItem = async (id: string, data: { name: string }): Promise<boolean> => {
export const updateOriginItem = async (id: string, data: { name: string, color_class?: string }): Promise<boolean> => {
try {
const response = await fetch(`${API_URL}/origin_items/${id}`, {
method: 'PUT',

View File

@@ -27,6 +27,7 @@ export interface OriginItemDef {
id: string;
origin_group_id: string;
name: string;
color_class: string;
}
export interface OriginGroupDef {