/* Configurações — Funcionários, Praias, Empresa, Parâmetros */ function Configuracoes({ ctx }) { const [sub, setSub] = useState("funcionarios"); return ( <>

Configurações

Equipe, praias, dados da empresa e parâmetros do sistema

{sub === "funcionarios" && } {sub === "produtos" && } {sub === "armazens" && } {sub === "praias" && } {sub === "empresa" && } {sub === "parametros" && } ); } const ROLE_COLORS = { "Admin Super": { bg:"#FDECE6", fg:"#B3270A" }, "Admin": { bg:"#FEF3E2", fg:"#B45309" }, "Gestor": { bg:"#E6EEFB", fg:"#1E40AF" }, "Operador": { bg:"#E8F5EC", fg:"#15803D" }, "Visualizador":{ bg:"#F4F4F5", fg:"#6B6B70" }, }; /* ====== FUNCIONÁRIOS ====== */ function FuncionariosTab({ ctx }) { const [editing, setEditing] = useState(null); // null | "new" | {item} const staff = ctx.state.staff; const onDelete = (s) => ctx.askConfirm({ title: "Excluir usuário?", message: `Tem certeza que deseja excluir ${s.name}?`, detail: "Este usuário perderá acesso ao sistema imediatamente.", danger: true, onConfirm: () => ctx.actions.deleteStaff(s.id), }); return ( <>
} label="Total de usuários" value={staff.length+""} sub="Cadastrados no sistema" /> } label="Conectados hoje" value="3" sub="Última hora: 2" /> } label="Pendentes de senha" value="0" sub="Todas configuradas" />
} onClick={() => setEditing("new")}>Novo usuário} > {staff.length === 0 ? (

Nenhum usuário cadastrado

Crie o primeiro usuário para começar.

) : ( {staff.map(s => { const rc = ROLE_COLORS[s.role] || ROLE_COLORS["Operador"]; return ( ); })}
Nome E-mail WhatsApp Perfil Último acesso Status
{s.name.split(" ").map(w => w[0]).slice(0,2).join("")}
{s.name}
{s.email} {s.phone} {s.role} {s.lastLogin ? fmtDate(s.lastLogin) : "—"} ativo
)}
{[ { name:"Dashboard", perms:[1,1,1,1,1] }, { name:"Fabricação", perms:[1,1,1,1,2] }, { name:"PDV Distribuidores",perms:[1,1,1,1,2] }, { name:"PDV Picolezeiros", perms:[1,1,1,1,2] }, { name:"Clientes", perms:[1,1,1,1,2] }, { name:"Estoque", perms:[1,1,1,1,2] }, { name:"Movimentações", perms:[1,1,1,2,2] }, { name:"Relatórios", perms:[1,1,1,2,2] }, { name:"Configurações", perms:[1,1,0,0,0] }, { name:"Criar/editar Admin",perms:[1,0,0,0,0] }, ].map((r, i) => ( {r.perms.map((v, j) => ( ))} ))}
Aba Admin Super Admin Gestor Operador Visualizador
{r.name} {v === 1 && } {v === 2 && ler} {v === 0 && }
{editing && ( setEditing(null)} onSave={(values) => { if (editing === "new") ctx.actions.addStaff(values); else ctx.actions.updateStaff(editing.id, values); setEditing(null); }} /> )} ); } function StaffModal({ initial, onClose, onSave }) { const [name, setName] = useState(initial?.name || ""); const [email, setEmail] = useState(initial?.email || ""); const [phone, setPhone] = useState(initial?.phone || ""); const [role, setRole] = useState(initial?.role || "Operador"); const [password, setPassword] = useState(initial?.password || ""); const [showPwd, setShowPwd] = useState(false); const [permissions, setPermissions] = useState(initial?.permissions || DEFAULT_PERMS_BY_ROLE[initial?.role || "Operador"] || ["dashboard"]); const [err, setErr] = useState({}); const isSuperAdmin = role === "Admin Super" || (Array.isArray(permissions) && permissions.includes("*")); const togglePerm = (id) => { if (isSuperAdmin) return; setPermissions(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); }; const setAll = (all) => setPermissions(all ? TAB_PERMS.map(t => t.id) : []); /* quando o perfil muda, sugere as permissões padrão do perfil */ const onRoleChange = (newRole) => { setRole(newRole); if (newRole === "Admin Super") setPermissions(["*"]); else if (DEFAULT_PERMS_BY_ROLE[newRole]) { const defs = DEFAULT_PERMS_BY_ROLE[newRole]; setPermissions(defs.includes("*") ? TAB_PERMS.map(t => t.id) : defs); } }; const genPassword = () => { const chars = "abcdefghijkmnpqrstuvwxyz23456789"; let p = ""; for (let i = 0; i < 8; i++) p += chars[Math.floor(Math.random()*chars.length)]; setPassword(p); setShowPwd(true); }; const submit = () => { const e = {}; if (!name.trim()) e.name = "Informe o nome"; if (!email.trim()) e.email = "Informe o e-mail"; if (!role) e.role = "Selecione um perfil"; if (!initial && (!password || password.length < 4)) e.password = "Mínimo de 4 caracteres"; if (initial && password && password.length < 4) e.password = "Mínimo de 4 caracteres"; if (!isSuperAdmin && permissions.length === 0) e.perms = "Selecione ao menos uma aba"; setErr(e); if (Object.keys(e).length) return; const payload = { name: name.trim(), email: email.trim(), phone, role, permissions: isSuperAdmin ? ["*"] : permissions, lastLogin: initial?.lastLogin || new Date().toISOString().slice(0,10) }; if (password) payload.password = password; onSave(payload); }; return ( }>
setName(e.target.value)} placeholder="Ex.: João Silva" autoFocus /> setEmail(e.target.value)} placeholder="usuario@otalpicole.com.br" /> setPhone(maskPhone(e.target.value))} placeholder="(00) 00000-0000" />
setPassword(e.target.value)} placeholder="••••••" />

Permissões de acesso
Marque as abas que este usuário pode acessar
{!isSuperAdmin && (
)}
{isSuperAdmin ? (
Admin Super tem acesso total
Todas as abas, inclusive criação de outros administradores.
) : ( <>
{TAB_PERMS.map(tab => { const on = permissions.includes(tab.id); return ( ); })}
{err.perms &&
{err.perms}
} )}
); } /* ====== PRODUTOS / TAMANHO DE CAIXA ====== */ function ProdutosTab({ ctx }) { const [drafts, setDrafts] = useState({}); const products = ctx.state.products; // agrupa por linha const lines = {}; products.forEach(p => { if (!lines[p.line]) lines[p.line] = []; lines[p.line].push(p); }); const set = (id, patch) => setDrafts(d => ({ ...d, [id]: { ...(d[id] || {}), ...patch } })); const current = (p, field) => { const d = drafts[p.id]; if (d && d[field] != null) return d[field]; return p[field]; }; const dirty = (p) => { const d = drafts[p.id]; if (!d) return false; return Object.keys(d).some(k => d[k] !== p[k]); }; const save = (p) => { const d = drafts[p.id]; if (!d) return; const patch = {}; if (d.boxSize != null) patch.boxSize = parseInt(d.boxSize, 10) || 1; if (d.price != null) patch.price = parseFloat(String(d.price).replace(",", ".")) || 0; ctx.actions.updateProduct(p.id, patch); setDrafts(prev => { const { [p.id]: _, ...rest } = prev; return rest; }); }; const saveAll = () => { Object.entries(drafts).forEach(([id, d]) => { const p = products.find(x => x.id === id); if (!p) return; if (!dirty(p)) return; save(p); }); }; const applyLineBoxSize = (line, size) => { const sz = parseInt(size, 10) || 60; products.filter(p => p.line === line).forEach(p => { set(p.id, { boxSize: sz }); }); ctx.pushToast(`${line}: ${sz} un/caixa aplicado a ${products.filter(p => p.line === line).length} produto(s)`); }; const dirtyCount = Object.keys(drafts).filter(id => { const p = products.find(x => x.id === id); return p && dirty(p); }).length; return ( <>
{Object.entries(lines).map(([line, list]) => { const sizes = [...new Set(list.map(p => current(p, "boxSize")))]; const sharedSize = sizes.length === 1 ? sizes[0] : null; return (
{line}
{list.length} sabor(es){sharedSize ? " · atual: " + sharedSize + " un/cx" : " · tamanhos mistos"}
Aplicar a toda linha: {[20, 30, 40, 50, 60, 100].map(sz => ( ))}
{list.map(p => { const boxSz = parseInt(current(p, "boxSize"), 10) || 1; const price = parseFloat(String(current(p, "price")).replace(",", ".")) || 0; const isDirty = dirty(p); return ( ); })}
Sabor Preço un. (R$) Un. por caixa Valor da caixa
{p.flavor}
set(p.id, { price: e.target.value.replace(/[^\d.,]/g,"") })} style={{ width:90, textAlign:"right", padding:"5px 8px", fontSize:12.5 }} />
set(p.id, { boxSize: e.target.value.replace(/\D/g, "") })} style={{ width:60, textAlign:"right", padding:"5px 8px", fontSize:12.5 }} /> un
{fmtBRL(boxSz * price * 0.55)} {isDirty ? : }
); })}
{dirtyCount > 0 && (
{dirtyCount} alteração(ões) pendente(s)
)}
  • O tamanho da caixa aparece em todos os comprovantes ("5 cx de 60 un") e no card do produto no PDV.
  • No PDV Distribuidores o distribuidor pode comprar caixas fechadas (botões +1/+5/+10) e/ou unidades avulsas (botão +un).
  • O estoque sempre é controlado em unidades — caixa é apenas uma forma de agrupar a venda.
  • O preço exibido por caixa é sempre preço un. × 0,55 × unidades/caixa (55% do preço de varejo).
); } /* ====== PRAIAS ====== */ function PraiasTab({ ctx }) { const [editing, setEditing] = useState(null); const beaches = ctx.state.beaches; const onDelete = (b) => ctx.askConfirm({ title: "Excluir praia?", message: `Excluir "${b.name}"?`, detail: "Picolezeiros vinculados a essa praia precisarão ser atualizados.", danger: true, onConfirm: () => ctx.actions.deleteBeach(b.id), }); return ( <> } onClick={() => setEditing("new")}>Nova praia} > {beaches.length === 0 ? (

Nenhuma praia cadastrada

Cadastre praias para que os picolezeiros possam operar.

) : ( {beaches.map(b => { const pics = ctx.state.picolezeiros.filter(p => p.beaches.includes(b.id)).length; return ( ); })}
Praia Município Observações Picolezeiros
{b.name}
{b.city} {b.note || "—"} {pics}
)}
{editing && ( setEditing(null)} onSave={(v) => { if (editing === "new") ctx.actions.addBeach(v); else ctx.actions.updateBeach(editing.id, v); setEditing(null); }} /> )} ); } function BeachModal({ initial, onClose, onSave }) { const [name, setName] = useState(initial?.name || ""); const [city, setCity] = useState(initial?.city || ""); const [note, setNote] = useState(initial?.note || ""); const [err, setErr] = useState({}); const submit = () => { const e = {}; if (!name.trim()) e.name = "Informe o nome"; if (!city.trim()) e.city = "Informe o município"; setErr(e); if (Object.keys(e).length) return; onSave({ name: name.trim(), city: city.trim(), note: note.trim() }); }; return ( }>
setName(e.target.value)} placeholder="Ex.: Porto da Barra" autoFocus /> setCity(e.target.value)} placeholder="Salvador" />