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:
@@ -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");
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user