// Shared design system — Promoup 360°
const DS = {
colors: {
primary: '#EA6B00',
primaryDark: '#C45508',
primaryLight: '#FFF4E6',
success: '#10B981',
successLight: '#ECFDF5',
warning: '#F59E0B',
warningLight: '#FFFBEB',
danger: '#EF4444',
dangerLight: '#FEF2F2',
sidebar: '#12085C',
sidebarHover: '#1C1280',
sidebarActive: '#EA6B00',
bg: '#F8F5FF',
card: '#FFFFFF',
border: '#E2E8F0',
text: '#0F172A',
textSec: '#64748B',
textMuted: '#94A3B8',
purple: '#7B35C8',
},
radius: { sm: '8px', md: '12px', lg: '16px', xl: '20px', full: '9999px' },
shadow: {
sm: '0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
md: '0 4px 12px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.04)',
lg: '0 10px 30px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.04)',
}
};
// Icon library — simple SVG paths
const iconPaths = {
dashboard: 'M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M9 22V12h6v10',
calendar: 'M8 2v4 M16 2v4 M3 10h18 M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z',
budget: 'M12 2v20 M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
users: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75',
crm: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
tasks: 'M9 11l3 3L22 4 M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11',
portal: 'M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9z M3 9l9 6 9-6',
reports: 'M18 20V10 M12 20V4 M6 20v-6',
wallet: 'M21 12V7H5a2 2 0 0 1 0-4h14v4 M3 5v14a2 2 0 0 0 2 2h16v-5 M18 12a2 2 0 0 0 0 4h4v-4z',
bell: 'M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9 M13.73 21a2 2 0 0 1-3.46 0',
search: 'M21 21l-4.35-4.35 M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z',
plus: 'M12 5v14 M5 12h14',
x: 'M18 6L6 18 M6 6l12 12',
check: 'M20 6L9 17l-5-5',
arrow: 'M5 12h14 M12 5l7 7-7 7',
arrowDown: 'M12 5v14 M19 12l-7 7-7-7',
edit: 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7 M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z',
trash: 'M3 6h18 M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6 M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2',
mail: 'M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z M22 6l-10 7L2 6',
phone: 'M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.15 12.91a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.06 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 16.92z',
star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',
link: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71 M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 M7 10l5 5 5-5 M12 15V3',
eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
filter: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z',
grid: 'M3 3h7v7H3z M14 3h7v7h-7z M14 14h7v7h-7z M3 14h7v7H3z',
list: 'M8 6h13 M8 12h13 M8 18h13 M3 6h.01 M3 12h.01 M3 18h.01',
settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4 M16 17l5-5-5-5 M21 12H9',
message: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
trending: 'M23 6l-9.5 9.5-5-5L1 18 M17 6h6v6',
clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z M12 6v6l4 2',
alert: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z M12 9v4 M12 17h.01',
chevronRight: 'M9 18l6-6-6-6',
chevronLeft: 'M15 18l-6-6 6-6',
chevronDown: 'M6 9l6 6 6-6',
zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
award: 'M12 15a7 7 0 1 0 0-14 7 7 0 0 0 0 14z M8.21 13.89L7 23l5-3 5 3-1.21-9.12',
briefcase: 'M20 7H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2',
tag: 'M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z M7 7h.01',
photo: 'M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z M12 17a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
map: 'M1 6v16l7-4 8 4 7-4V2l-7 4-8-4-7 4z M8 2v16 M16 6v16',
cpu: 'M9 2H5a2 2 0 0 0-2 2v4 M15 2h4a2 2 0 0 0 2 2v4 M9 22H5a2 2 0 0 0-2-2v-4 M15 22h4a2 2 0 0 0 2-2v-4 M9 9h6v6H9z M2 9h2 M2 15h2 M20 9h2 M20 15h2 M9 2v2 M15 2v2 M9 20v2 M15 20v2',
};
function Icon({ name, size = 18, color = 'currentColor', strokeWidth = 1.8 }) {
const d = iconPaths[name];
if (!d) return null;
return (
);
}
// Badge component
function Badge({ children, variant = 'default', size = 'sm' }) {
const variants = {
default: { bg: '#F1F5F9', color: '#475569' },
primary: { bg: '#EFF6FF', color: '#2563EB' },
success: { bg: '#ECFDF5', color: '#059669' },
warning: { bg: '#FFFBEB', color: '#D97706' },
danger: { bg: '#FEF2F2', color: '#DC2626' },
purple: { bg: '#F5F3FF', color: '#7C3AED' },
};
const v = variants[variant] || variants.default;
const padding = size === 'sm' ? '2px 8px' : '4px 12px';
const fontSize = size === 'sm' ? '11px' : '12px';
return (
{children}
);
}
// Card component
function Card({ children, style = {}, onClick, hover = false }) {
const [hovered, setHovered] = React.useState(false);
return (
hover && setHovered(true)}
onMouseLeave={() => hover && setHovered(false)}
style={{
background: DS.colors.card, borderRadius: DS.radius.md,
boxShadow: hovered ? DS.shadow.md : DS.shadow.sm,
border: `1px solid ${DS.colors.border}`,
transition: 'all 0.18s ease',
cursor: onClick ? 'pointer' : 'default',
transform: hovered ? 'translateY(-1px)' : 'none',
...style,
}}
>{children}
);
}
// Button component
function Btn({ children, variant = 'primary', size = 'md', onClick, icon, style = {}, disabled = false }) {
const [hovered, setHovered] = React.useState(false);
const variants = {
primary: { bg: DS.colors.primary, hover: DS.colors.primaryDark, color: '#fff', border: 'none' },
secondary: { bg: '#fff', hover: '#F8FAFC', color: DS.colors.text, border: `1px solid ${DS.colors.border}` },
ghost: { bg: 'transparent', hover: '#F1F5F9', color: DS.colors.textSec, border: 'none' },
danger: { bg: '#FEF2F2', hover: '#FEE2E2', color: DS.colors.danger, border: 'none' },
success: { bg: DS.colors.success, hover: '#059669', color: '#fff', border: 'none' },
};
const v = variants[variant] || variants.primary;
const sizes = { sm: { padding: '6px 12px', fontSize: '12px' }, md: { padding: '8px 16px', fontSize: '13px' }, lg: { padding: '11px 22px', fontSize: '14px' } };
const s = sizes[size] || sizes.md;
return (
);
}
// Avatar
function Avatar({ name = '', size = 36, src, color }) {
const colors = ['#3B82F6','#8B5CF6','#10B981','#F59E0B','#EF4444','#06B6D4','#EC4899'];
const c = color || colors[name.charCodeAt(0) % colors.length];
const initials = name.split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase();
return (
{src ?

: initials}
);
}
// Progress bar
function ProgressBar({ value, max = 100, color = DS.colors.primary, height = 6 }) {
const pct = Math.min(100, (value / max) * 100);
return (
);
}
// Stat card
function StatCard({ label, value, delta, icon, color = DS.colors.primary, subtitle }) {
const positive = delta >= 0;
return (
{label}
{value}
{subtitle &&
{subtitle}
}
{delta !== undefined && (
{positive ? '+' : ''}{delta}%
vs last month
)}
);
}
// Empty state
function EmptyState({ icon = 'grid', title = 'Nothing here yet', desc = '', action }) {
return (
{title}
{desc}
{action &&
{action}
}
);
}
// Modal
function Modal({ open, onClose, title, children, width = 520, extra }) {
if (!open) return null;
return (
e.stopPropagation()}>
{children}
);
}
// Input
function Input({ label, value, onChange, placeholder, type = 'text', style = {}, list, onBlur }) {
return (
{label && }
onChange(e.target.value)} placeholder={placeholder}
list={list}
style={{
border: `1px solid ${DS.colors.border}`, borderRadius: DS.radius.sm,
padding: '9px 12px', fontSize: 13, color: DS.colors.text,
outline: 'none', transition: 'border 0.15s',
fontFamily: 'inherit',
}}
onFocus={e => e.target.style.borderColor = DS.colors.primary}
onBlur={e => { e.target.style.borderColor = DS.colors.border; if (onBlur) onBlur(e); }}
/>
);
}
// Select
function Select({ label, value, onChange, options, style = {} }) {
return (
{label && }
);
}
// Divider
function Divider({ style = {} }) {
return ;
}
// Section header
function SectionHeader({ title, subtitle, action }) {
return (
{title}
{subtitle &&
{subtitle}
}
{action}
);
}
// Mini chart (bar chart using divs)
function MiniBarChart({ data, color = DS.colors.primary, height = 80 }) {
const max = Math.max(...data.map(d => d.value));
return (
{data.map((d, i) => (
))}
);
}
// Donut chart (SVG)
function DonutChart({ segments, size = 120 }) {
const total = segments.reduce((s, seg) => s + seg.value, 0);
let angle = -90;
const r = 40, cx = 60, cy = 60;
const paths = segments.map(seg => {
const pct = seg.value / total;
const sweep = pct * 360;
const startAngle = angle;
angle += sweep;
const x1 = cx + r * Math.cos((startAngle * Math.PI) / 180);
const y1 = cy + r * Math.sin((startAngle * Math.PI) / 180);
const x2 = cx + r * Math.cos(((startAngle + sweep) * Math.PI) / 180);
const y2 = cy + r * Math.sin(((startAngle + sweep) * Math.PI) / 180);
const large = sweep > 180 ? 1 : 0;
return { d: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`, color: seg.color, label: seg.label, value: seg.value };
});
return (
);
}
// Toast notification
function Toast({ message, type = 'success', onClose }) {
React.useEffect(() => {
const t = setTimeout(onClose, 3000);
return () => clearTimeout(t);
}, []);
const colors = { success: DS.colors.success, danger: DS.colors.danger, warning: DS.colors.warning };
return (
);
}
// Loading spinner
function Spinner({ size = 32 }) {
return (
);
}
// ───────────────────────────────────────────────────────────
// Permissões por setor (department) + nível (role)
// ───────────────────────────────────────────────────────────
const DEPT_LABELS = {
admin: 'Administração',
comercial: 'Comercial',
financeiro: 'Financeiro',
producao: 'Produção',
sdr: 'SDR',
atendimento: 'Atendimento',
};
const DEPT_OPTIONS = [
{ value: '', label: '— Selecionar —' },
{ value: 'admin', label: 'Administração (vê tudo)' },
{ value: 'comercial', label: 'Comercial' },
{ value: 'financeiro', label: 'Financeiro' },
{ value: 'producao', label: 'Produção' },
{ value: 'sdr', label: 'SDR' },
{ value: 'atendimento', label: 'Atendimento' },
];
// Quais módulos do menu cada setor pode acessar.
// Regra do cliente: Admin e Financeiro veem TUDO. Os demais setores
// veem tudo EXCETO Financeiro (a página de transações). A restrição
// principal pros não-financeiros é não ver os VALORES R$ — isso é
// controlado por canSeeFinancials, não por canSeeModule.
// 'usuarios' fica separado — exige role='admin' independentemente do setor.
const ALL_MODULES_FULL = ['dashboard','budgets','events','freelancers','crm','fornecedores','servicos','financeiro','tasks','portal','reports','whatsapp'];
const ALL_MODULES_NO_FIN = ['dashboard','budgets','events','freelancers','crm','fornecedores','servicos','tasks','portal','reports','whatsapp'];
const DEPT_MODULES = {
admin: ALL_MODULES_FULL,
financeiro: ALL_MODULES_FULL,
comercial: ALL_MODULES_NO_FIN,
producao: ALL_MODULES_NO_FIN,
sdr: ALL_MODULES_NO_FIN,
atendimento: ALL_MODULES_NO_FIN,
};
// Setores que podem ver valores em R$ (Dashboard, colunas de preço no orçamento, etc.)
const FINANCIAL_DEPTS = new Set(['admin', 'comercial', 'financeiro']);
function canSeeModule(user, moduleId) {
if (!user) return false;
if (moduleId === 'usuarios') return user.role === 'admin';
const dept = user.department || 'atendimento';
return (DEPT_MODULES[dept] || []).includes(moduleId);
}
function canSeeFinancials(user) {
if (!user) return false;
return FINANCIAL_DEPTS.has(user.department || '');
}
function canEditBudgets(user) {
if (!user) return false;
if (user.role === 'visualizador') return false;
return FINANCIAL_DEPTS.has(user.department || '');
}
function isReadOnly(user) {
return !user || user.role === 'visualizador';
}
function deptLabel(dept) {
return DEPT_LABELS[dept] || '—';
}
// Tela de "sem permissão" reutilizável
function NoAccess({ moduleName }) {
return (
Acesso restrito
Seu setor não tem permissão para acessar {moduleName ? `o módulo de ${moduleName}` : 'esta área'}.
Fale com um administrador caso precise de acesso.
);
}
// Export all to window
Object.assign(window, {
DS, Icon, Badge, Card, Btn, Avatar, ProgressBar, StatCard,
EmptyState, Modal, Input, Select, Divider, SectionHeader,
MiniBarChart, DonutChart, Toast, Spinner,
DEPT_LABELS, DEPT_OPTIONS, DEPT_MODULES,
canSeeModule, canSeeFinancials, canEditBudgets, isReadOnly, deptLabel,
NoAccess,
});