// GuinchoFlow — Shared Components
// ── Icons (inline SVG via lucide-style paths) ──────────────────────────────
const Icon = ({ name, size = 20, color = 'currentColor', className = '' }) => {
const paths = {
home: 'M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z M9 22V12h6v10',
truck: 'M1 3h15v13H1zM16 8h4l3 3v5h-7V8z M5.5 21a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM18.5 21a1.5 1.5 0 100-3 1.5 1.5 0 000 3z',
alert: 'M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z M12 9v4 M12 17h.01',
search: 'M11 19a8 8 0 100-16 8 8 0 000 16z M21 21l-4.35-4.35',
calendar: 'M3 4h18v18H3z M16 2v4 M8 2v4 M3 10h18',
check: 'M22 11.08V12a10 10 0 11-5.93-9.14 M22 4L12 14.01l-3-3',
x: 'M18 6L6 18 M6 6l12 12',
bell: 'M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9 M13.73 21a2 2 0 01-3.46 0',
user: 'M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2 M12 11a4 4 0 100-8 4 4 0 000 8z',
users: 'M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2 M23 21v-2a4 4 0 00-3-3.87 M16 3.13a4 4 0 010 7.75',
dollar: 'M12 1v22 M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6',
chart: 'M18 20V10 M12 20V4 M6 20v-6',
settings: 'M12 15a3 3 0 100-6 3 3 0 000 6z M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z',
eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 12a3 3 0 100-6 3 3 0 000 6',
edit: 'M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7 M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z',
trash: 'M3 6h18 M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6 M10 11v6 M14 11v6 M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2',
plus: 'M12 5v14 M5 12h14',
menu: 'M3 12h18 M3 6h18 M3 18h18',
close: 'M18 6L6 18 M6 6l12 12',
chevronRight: 'M9 18l6-6-6-6',
chevronDown: 'M6 9l6 6 6-6',
logout: 'M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4 M16 17l5-5-5-5 M21 12H9',
clipboard: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2 M9 5a2 2 0 002 2h2a2 2 0 002-2 M9 5a2 2 0 012-2h2a2 2 0 012 2',
map: 'M1 6v16l7-4 8 4 7-4V2l-7 4-8-4-7 4 M8 2v16 M16 6v16',
camera: 'M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z M12 17a4 4 0 100-8 4 4 0 000 8z',
fileText: 'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8',
wrench: 'M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z',
trendUp: 'M23 6l-9.5 9.5-5-5L1 18 M17 6h6v6',
trendDown: 'M23 18l-9.5-9.5-5 5L1 6 M17 18h6v-6',
building: 'M6 2h12a2 2 0 012 2v18H4V4a2 2 0 012-2z M9 22V12h6v10 M9 6h.01 M15 6h.01 M9 10h.01 M15 10h.01',
download: 'M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4 M7 10l5 5 5-5 M12 15V3',
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
key: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4',
battery: 'M1 9h18v6H1z M23 13v-2',
zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z',
phone: 'M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z',
mail: 'M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2z M22 6l-10 7L2 6',
};
const d = paths[name] || paths.alert;
return (
);
};
// ── Status Badge ──────────────────────────────────────────────────────────
const StatusBadge = ({ status }) => {
const map = {
aberta: { label: 'Aberta', bg: 'rgba(59,130,246,0.12)', color: '#2563EB' },
atendimento: { label: 'Em Atendimento', bg: 'rgba(245,158,11,0.15)', color: '#B45309' },
agendada: { label: 'Agendada', bg: 'rgba(139,92,246,0.12)', color: '#7C3AED' },
finalizada: { label: 'Finalizada', bg: 'rgba(16,185,129,0.12)', color: '#047857' },
cancelada: { label: 'Cancelada', bg: 'rgba(239,68,68,0.12)', color: '#DC2626' },
disponivel: { label: 'Disponível', bg: 'rgba(16,185,129,0.12)', color: '#047857' },
ativo: { label: 'Ativo', bg: 'rgba(16,185,129,0.12)', color: '#047857' },
inativo: { label: 'Inativo', bg: 'rgba(148,163,184,0.15)',color: '#64748B' },
manutencao: { label: 'Manutenção', bg: 'rgba(245,158,11,0.15)', color: '#B45309' },
entrada: { label: 'Entrada', bg: 'rgba(16,185,129,0.12)', color: '#047857' },
saida: { label: 'Saída', bg: 'rgba(239,68,68,0.12)', color: '#DC2626' },
};
const s = map[status] || map.inativo;
return (
{s.label}
);
};
// ── Avatar ────────────────────────────────────────────────────────────────
const AVATAR_COLORS = ['#3B82F6','#8B5CF6','#F59E0B','#10B981','#EF4444','#EC4899','#14B8A6'];
const Avatar = ({ initials, size = 32 }) => {
const idx = (initials.charCodeAt(0) + (initials.charCodeAt(1) || 0)) % AVATAR_COLORS.length;
return (
{initials}
);
};
// ── Card ──────────────────────────────────────────────────────────────────
const Card = ({ children, style = {}, className = '', ...rest }) => (
{children}
);
// ── Modal ─────────────────────────────────────────────────────────────────
const Modal = ({ open, onClose, title, children, width = 680, noPad = false }) => {
if (!open) return null;
return (
e.target === e.currentTarget && onClose()}>
{title}
{children}
);
};
// ── Toast ─────────────────────────────────────────────────────────────────
let _toastFn = null;
const Toast = () => {
const [toasts, setToasts] = React.useState([]);
_toastFn = (msg, type='success') => {
const id = Date.now();
setToasts(t => [...t, { id, msg, type }]);
setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200);
};
const colors = { success: '#10B981', error: '#EF4444', warning: '#F59E0B', info: '#3B82F6' };
return (
{toasts.map(t => (
{t.msg}
))}
);
};
const showToast = (msg, type='success') => _toastFn && _toastFn(msg, type);
// ── SignaturePad (assinatura digital responsiva) ──────────────────────────
const SignaturePad = ({ label, onSign, height = 160 }) => {
const canvasRef = React.useRef(null);
const containerRef = React.useRef(null);
const [drawing, setDrawing] = React.useState(false);
const [hasSign, setHasSign] = React.useState(false);
// Ajuste de DPI e largura responsiva
React.useEffect(() => {
const setupCanvas = () => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const dpr = window.devicePixelRatio || 1;
const w = container.offsetWidth;
canvas.width = w * dpr;
canvas.height = height * dpr;
canvas.style.width = w + 'px';
canvas.style.height = height + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.strokeStyle = '#0F172A';
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
};
setupCanvas();
window.addEventListener('resize', setupCanvas);
return () => window.removeEventListener('resize', setupCanvas);
}, [height]);
const getPos = (e) => {
const canvas = canvasRef.current;
const r = canvas.getBoundingClientRect();
const src = e.touches && e.touches[0] ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
};
const start = (e) => {
e.preventDefault();
setDrawing(true);
const pos = getPos(e);
const ctx = canvasRef.current.getContext('2d');
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
};
const move = (e) => {
if (!drawing) return;
e.preventDefault();
const pos = getPos(e);
const ctx = canvasRef.current.getContext('2d');
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
if (!hasSign) setHasSign(true);
};
const end = (e) => {
if (!drawing) return;
e && e.preventDefault();
setDrawing(false);
if (hasSign && onSign) onSign(canvasRef.current.toDataURL('image/png'));
};
const clear = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
setHasSign(false);
if (onSign) onSign(null);
};
return (
{!hasSign &&
Assine acima usando o mouse ou o dedo}
);
};
// ── Input ─────────────────────────────────────────────────────────────────
const Input = ({ label, placeholder, value, onChange, type = 'text', style = {} }) => {
const [focused, setFocused] = React.useState(false);
return (
{label && }
setFocused(true)} onBlur={() => setFocused(false)}
style={{
padding: '9px 12px', borderRadius: 8, fontSize: 14, color: '#0F172A',
border: `1px solid ${focused ? '#3B82F6' : '#E2E8F0'}`,
outline: 'none', background: '#fff', fontFamily: 'Inter, sans-serif',
boxShadow: focused ? '0 0 0 3px rgba(59,130,246,0.12)' : 'none',
transition: 'all 0.15s'
}}
/>
);
};
// ── Select ────────────────────────────────────────────────────────────────
const Select = ({ label, value, onChange, options = [], style = {} }) => (
{label && }
);
// ── Btn ───────────────────────────────────────────────────────────────────
const Btn = ({ children, onClick, variant = 'primary', size = 'md', icon, disabled = false, style = {} }) => {
const [hov, setHov] = React.useState(false);
const base = {
display: 'inline-flex', alignItems: 'center', gap: 6, cursor: disabled ? 'not-allowed' : 'pointer',
border: 'none', borderRadius: 8, fontWeight: 600, fontFamily: 'DM Sans, sans-serif',
transition: 'all 0.15s', opacity: disabled ? 0.6 : 1, ...style
};
const sizes = { sm: { padding: '6px 12px', fontSize: 13 }, md: { padding: '9px 16px', fontSize: 14 }, lg: { padding: '12px 20px', fontSize: 15 } };
const variants = {
primary: { background: hov ? '#E08E00' : '#F59E0B', color: '#0F172A', boxShadow: hov ? '0 4px 12px rgba(245,158,11,0.35)' : '0 1px 4px rgba(245,158,11,0.25)' },
secondary: { background: hov ? '#F1F5F9' : '#F8FAFC', color: '#374151', border: '1px solid #E2E8F0' },
danger: { background: hov ? '#DC2626' : '#EF4444', color: '#fff' },
ghost: { background: hov ? '#F1F5F9' : 'transparent', color: '#64748B' },
blue: { background: hov ? '#2563EB' : '#3B82F6', color: '#fff', boxShadow: hov ? '0 4px 12px rgba(59,130,246,0.35)' : 'none' },
};
return (
);
};
// ── Pagination ────────────────────────────────────────────────────────────
const Pagination = ({ page, total, perPage, onChange }) => {
const pages = Math.ceil(total / perPage);
return (
onChange(Math.max(1, page-1))} disabled={page===1}>← Anterior
{Array.from({length: Math.min(pages, 5)}, (_, i) => {
const p = pages <= 5 ? i+1 : page <= 3 ? i+1 : page >= pages-2 ? pages-4+i : page-2+i;
return (
);
})}
onChange(Math.min(pages, page+1))} disabled={page===pages}>Próximo →
);
};
// ── Empty State ───────────────────────────────────────────────────────────
const EmptyState = ({ icon = 'clipboard', title = 'Nenhum dado encontrado', subtitle = 'Tente ajustar os filtros ou adicione um novo registro.' }) => (
);
// ── Skeleton ──────────────────────────────────────────────────────────────
const Skeleton = ({ w = '100%', h = 16, rounded = 4, style = {} }) => (
);
// Export
Object.assign(window, { Icon, StatusBadge, Avatar, Card, Modal, Toast, showToast, Input, Select, Btn, Pagination, EmptyState, Skeleton, AVATAR_COLORS });