// Freelancers Module
function Freelancers() {
const [freelancers, setFreelancers] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [selected, setSelected] = React.useState(null);
const [addModal, setAddModal] = React.useState(false);
const [editModal, setEditModal] = React.useState(false);
const [delConfirm, setDelConfirm] = React.useState(false);
const [payModal, setPayModal] = React.useState(false);
const [saving, setSaving] = React.useState(false);
const [savingPay, setSavingPay] = React.useState(false);
const emptyForm = { name: '', role: '', city: '', phone: '', email: '', rate: '', skills: '', status: 'Disponível' };
const [form, setForm] = React.useState(emptyForm);
const [editForm, setEditForm] = React.useState(emptyForm);
const emptyPay = { tipo: 'total', valor: '', metodo: 'PIX', descricao: '' };
const [payForm, setPayForm] = React.useState(emptyPay);
const [payments, setPayments] = React.useState([]);
const [toast, setToast] = React.useState(null);
const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); };
const handleCreate = async () => {
if (!form.name.trim() || !form.role.trim()) {
showToast('Preencha nome e especialidade.', 'danger'); return;
}
setSaving(true);
const skills = form.skills.split(',').map(s => s.trim()).filter(Boolean);
const { data, error } = await sb.from('freelancers').insert({
name: form.name,
role: form.role,
city: form.city,
phone: form.phone,
email: form.email,
rate: parseFloat(form.rate) || 0,
skills,
status: form.status,
rating: 5.0,
events_count: 0,
}).select().single();
setSaving(false);
if (error) { showToast('Erro ao adicionar freelancer.', 'danger'); return; }
setFreelancers(prev => [...prev, data].sort((a, b) => a.name.localeCompare(b.name)));
setAddModal(false);
setForm(emptyForm);
showToast(`${data.name} adicionado com sucesso!`);
};
const openEdit = (f) => {
setEditForm({ name: f.name, role: f.role || '', city: f.city || '', phone: f.phone || '', email: f.email || '', rate: String(f.rate || ''), skills: (f.skills || []).join(', '), status: f.status || 'Disponível' });
setEditModal(true);
};
const handleEditSave = async () => {
if (!editForm.name.trim()) { showToast('Preencha o nome.', 'danger'); return; }
setSaving(true);
const skills = editForm.skills.split(',').map(s => s.trim()).filter(Boolean);
const { data, error } = await sb.from('freelancers').update({
name: editForm.name, role: editForm.role, city: editForm.city,
phone: editForm.phone, email: editForm.email,
rate: parseFloat(editForm.rate) || 0, skills, status: editForm.status,
}).eq('id', selected.id).select().single();
setSaving(false);
if (error) { showToast('Erro ao salvar.', 'danger'); return; }
setFreelancers(prev => prev.map(f => f.id === data.id ? data : f).sort((a, b) => a.name.localeCompare(b.name)));
setSelected(data);
setEditModal(false);
showToast('Freelancer atualizado!');
};
const handleDelete = async () => {
const id = selected.id;
const nome = selected.name;
await sb.from('freelancers').delete().eq('id', id);
setFreelancers(prev => prev.filter(f => f.id !== id));
setDelConfirm(false);
setSelected(null);
showToast(`${nome} excluído.`);
};
React.useEffect(() => {
sb.from('freelancers').select('*').order('name').then(({ data }) => {
if (data) setFreelancers(data);
setLoading(false);
});
}, []);
React.useEffect(() => {
setPayments(selected?.payments || []);
}, [selected?.id]);
React.useEffect(() => {
if (payModal && payForm.tipo === 'total' && selected) {
setPayForm(f => ({ ...f, valor: String(selected.rate || '') }));
}
if (payModal && payForm.tipo === 'parcial') {
setPayForm(f => ({ ...f, valor: '' }));
}
}, [payForm.tipo, payModal]);
const openPayModal = () => {
setPayForm({ tipo: 'total', valor: String(selected?.rate || ''), metodo: 'PIX', descricao: '' });
setPayModal(true);
};
const handlePayment = async () => {
const valor = parseFloat(String(payForm.valor).replace(',', '.')) || 0;
if (valor <= 0) { showToast('Informe um valor válido.', 'danger'); return; }
setSavingPay(true);
const novoPag = {
id: Date.now(),
data: new Date().toLocaleDateString('pt-BR'),
valor,
metodo: payForm.metodo,
tipo: payForm.tipo,
descricao: payForm.descricao,
};
const updated = [...payments, novoPag];
const { error } = await sb.from('freelancers').update({ payments: updated }).eq('id', selected.id);
setSavingPay(false);
if (!error) {
setPayments(updated);
setFreelancers(prev => prev.map(f => f.id === selected.id ? { ...f, payments: updated } : f));
setSelected(prev => ({ ...prev, payments: updated }));
}
setPayModal(false);
setPayForm(emptyPay);
showToast(`R$ ${valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} registrado via ${novoPag.metodo}!`);
};
const statusColor = { 'Disponível': 'success', 'Escalado': 'primary', 'Indisponível': 'danger' };
const StarRating = ({ rating }) => (
{rating}
);
if (selected) {
const f = selected;
return (
setSelected(null)} style={{ transform: 'rotate(180deg)' }}>
Perfil do Freelancer
{f.name}
{f.role} · {f.city}
{f.events_count} eventos
{f.status}
{(f.skills || []).map(s => {s})}
Mensagem
showToast('Freelancer convidado!')}>Convidar
openEdit(f)}>Editar
setDelConfirm(true)}>Excluir
{[
{ label: 'Cachê/diária', value: `R$ ${(f.rate || 0).toLocaleString('pt-BR')}` },
{ label: 'E-mail', value: f.email },
{ label: 'Telefone', value: f.phone },
].map(i => (
))}
Histórico de eventos
Próximos eventos
Nenhum evento agendado
Pagamentos
{payments.length > 0 && (
R$ {payments.reduce((s, p) => s + p.valor, 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
)}
{/* Histórico */}
{payments.length === 0 ? (
Nenhum pagamento registrado.
) : (
{[...payments].reverse().map(p => {
const metodoColor = { PIX: '#10B981', Cartão: '#3B82F6', Dinheiro: '#F59E0B' };
return (
R$ {p.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
{p.metodo}
{p.tipo === 'parcial' && Parcial}
{p.data}
{p.descricao &&
{p.descricao}
}
);
})}
)}
Registrar pagamento
{/* Modal registrar pagamento */}
{ setPayModal(false); setPayForm(emptyPay); }} title="Registrar Pagamento" width={460}>
{/* Info do freelancer */}
{selected?.name}
Cachê: R$ {(selected?.rate || 0).toLocaleString('pt-BR')}
{/* Tipo: Total / Parcial */}
Tipo de pagamento
{['total', 'parcial'].map(t => (
))}
{payForm.tipo === 'parcial' && (
Informe o valor que será pago agora.
)}
{/* Valor */}
setPayForm(f => ({ ...f, valor: v }))}
type="number"
placeholder="Ex: 1800" />
{/* Forma de pagamento */}
Forma de pagamento
{[
{ id: 'PIX', emoji: '⚡', color: '#10B981' },
{ id: 'Cartão', emoji: '💳', color: '#3B82F6' },
{ id: 'Dinheiro', emoji: '💵', color: '#F59E0B' },
].map(m => (
))}
{/* Descrição */}
setPayForm(f => ({ ...f, descricao: v }))}
placeholder="Ex: Evento Congresso Tech — 28/04" />
{ setPayModal(false); setPayForm(emptyPay); }}>Cancelar
{savingPay ? 'Salvando…' : 'Confirmar pagamento'}
{/* Modal editar */}
setEditModal(false)} title="Editar Freelancer" width={560}>
setEditForm(f => ({ ...f, name: v }))} />
setEditForm(f => ({ ...f, role: v }))} />
setEditForm(f => ({ ...f, email: v }))} />
setEditForm(f => ({ ...f, phone: v }))} />
setEditForm(f => ({ ...f, city: v }))} />
setEditForm(f => ({ ...f, rate: v }))} type="number" />
setEditForm(f => ({ ...f, skills: v }))} />
{/* Confirmação exclusão */}
setDelConfirm(false)} title="Excluir freelancer" width={400}>
Tem certeza que deseja excluir {selected?.name}? Esta ação não pode ser desfeita.
setDelConfirm(false)}>Cancelar
Excluir
{toast &&
setToast(null)} />}
);
}
return (
Freelancers
Gerencie sua rede de profissionais parceiros
setAddModal(true)}>Adicionar freelancer
{loading ?
: (
{freelancers.length === 0 ? (
) : freelancers.map(f => (
setSelected(f)} style={{ padding: 20 }}>
R${f.rate >= 1000 ? (f.rate / 1000).toFixed(1) + 'k' : f.rate}
Cachê
{(f.skills || []).slice(0, 3).map(s => {s})}
))}
)}
{ setAddModal(false); setForm(emptyForm); }} title="Adicionar Freelancer" width={560}>
setForm(f => ({ ...f, name: v }))} placeholder="Ex: Carlos Melo" />
setForm(f => ({ ...f, role: v }))} placeholder="Ex: DJ / Sonorização" />
setForm(f => ({ ...f, email: v }))} placeholder="Ex: carlos@email.com" />
setForm(f => ({ ...f, phone: v }))} placeholder="Ex: (11) 99999-0000" />
setForm(f => ({ ...f, city: v }))} placeholder="Ex: São Paulo, SP" />
setForm(f => ({ ...f, rate: v }))} placeholder="Ex: 1800" type="number" />
setForm(f => ({ ...f, skills: v }))} placeholder="Ex: DJ, Sonorização, Iluminação" />
{toast &&
setToast(null)} />}
);
}
Object.assign(window, { Freelancers });