feat(areas): Fase 2c — UI multi-área no perfil e nas configurações da clínica
- perfil profissional: chips de áreas de atuação (getMyAreas/setMyAreas) - configurações da clínica: chips de áreas da unidade (getClinicaAreas/setClinicaAreas) - fix: handleSavePerfil agora envia logradouro/numero (rua/número não salvavam) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -738,6 +738,15 @@ export const HybridBackend = {
|
||||
return r.ok;
|
||||
} catch { return false; }
|
||||
},
|
||||
getClinicaAreas: async (clinicaId: string): Promise<string[]> => {
|
||||
try { const r = await apiFetch(`${API_URL}/clinicas/${clinicaId}/areas`); const d = r.ok ? await r.json() : {}; return Array.isArray(d?.areas) ? d.areas : []; } catch { return []; }
|
||||
},
|
||||
setClinicaAreas: async (clinicaId: string, areas: string[]): Promise<boolean> => {
|
||||
try {
|
||||
const r = await apiFetch(`${API_URL}/clinicas/${clinicaId}/areas`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ areas }) });
|
||||
return r.ok;
|
||||
} catch { return false; }
|
||||
},
|
||||
|
||||
// Semeia o catálogo de biomedicina estética no workspace ativo (idempotente).
|
||||
seedBiomedicina: async (): Promise<boolean> => {
|
||||
|
||||
@@ -95,11 +95,17 @@ const UnidadePerfilCard: React.FC<{ workspaceId?: string; clinColor: string }> =
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [f, setF] = useState<any>({});
|
||||
const [allAreas, setAllAreas] = useState<{ slug: string; nome: string; cor: string }[]>([]);
|
||||
const [clinAreas, setClinAreas] = useState<string[]>([]);
|
||||
useEffect(() => { HybridBackend.getAreas().then(setAllAreas); }, []);
|
||||
|
||||
const load = async () => {
|
||||
if (!workspaceId) { setLoading(false); return; }
|
||||
setLoading(true);
|
||||
try { const r = await HybridBackend.getClinicaProfile(workspaceId); if (r.success) setF(r.clinica || {}); }
|
||||
try {
|
||||
const r = await HybridBackend.getClinicaProfile(workspaceId); if (r.success) setF(r.clinica || {});
|
||||
setClinAreas(await HybridBackend.getClinicaAreas(workspaceId));
|
||||
}
|
||||
catch { /* ignore */ } finally { setLoading(false); }
|
||||
};
|
||||
useEffect(() => { load(); }, [workspaceId]);
|
||||
@@ -126,6 +132,7 @@ const UnidadePerfilCard: React.FC<{ workspaceId?: string; clinColor: string }> =
|
||||
logradouro: f.logradouro, numero: f.numero, bairro: f.bairro, cidade: f.cidade, estado: f.estado, cep: f.cep,
|
||||
});
|
||||
if (!r.success) { toast.error(r.message || 'ERRO AO SALVAR.'); return; }
|
||||
await HybridBackend.setClinicaAreas(workspaceId, clinAreas);
|
||||
toast.success('DADOS DA UNIDADE ATUALIZADOS!'); setEditing(false);
|
||||
} catch { toast.error('ERRO AO SALVAR.'); } finally { setSaving(false); }
|
||||
};
|
||||
@@ -172,6 +179,23 @@ const UnidadePerfilCard: React.FC<{ workspaceId?: string; clinColor: string }> =
|
||||
<div><label className={LABEL_CLS}>Estado</label><input maxLength={2} className={`${FIELD_CLS} uppercase`} value={f.estado || ''} onChange={e => set('estado', e.target.value.toUpperCase())} /></div>
|
||||
<div><label className={LABEL_CLS}>Cor</label><input type="color" className="w-full h-10 border border-gray-200 rounded-xl px-1" value={f.cor || clinColor} onChange={e => set('cor', e.target.value)} /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={LABEL_CLS}>Áreas de atuação da unidade</label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{allAreas.map(a => {
|
||||
const on = clinAreas.includes(a.slug);
|
||||
return (
|
||||
<button key={a.slug} type="button"
|
||||
onClick={() => setClinAreas(prev => on ? prev.filter(s => s !== a.slug) : [...prev, a.slug])}
|
||||
className={`text-[11px] font-black uppercase px-3 py-1.5 rounded-full border transition-all ${on ? 'text-white border-transparent' : 'text-gray-500 border-gray-200 bg-white hover:bg-gray-50'}`}
|
||||
style={on ? { backgroundColor: a.cor } : undefined}>
|
||||
{a.nome}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-1">Uma unidade pode ter mais de uma área (ex.: odontologia + biomedicina).</p>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-1">
|
||||
<button onClick={() => { setEditing(false); load(); }} className="px-4 py-2 border border-gray-200 rounded-xl text-[11px] font-black uppercase hover:bg-gray-50">Cancelar</button>
|
||||
<button onClick={save} disabled={saving} className="flex-1 py-2 bg-blue-600 text-white rounded-xl text-[11px] font-black uppercase disabled:opacity-50 flex items-center justify-center gap-2">{saving ? <Loader2 size={13} className="animate-spin" /> : <CheckCircle2 size={13} />} Salvar</button>
|
||||
|
||||
@@ -156,7 +156,7 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
|
||||
// buscar
|
||||
const [lista, setLista] = useState<Profissional[]>([]);
|
||||
const [areas, setAreas] = useState<{ slug: string; nome: string }[]>([]);
|
||||
const [areas, setAreas] = useState<{ slug: string; nome: string; cor: string }[]>([]);
|
||||
const [fArea, setFArea] = useState('');
|
||||
const [fTipo, setFTipo] = useState('');
|
||||
const [fEstado, setFEstado] = useState('');
|
||||
@@ -166,6 +166,7 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
const [fRaio, setFRaio] = useState('');
|
||||
// perfil
|
||||
const [perfil, setPerfil] = useState<any>(null);
|
||||
const [myAreas, setMyAreas] = useState<string[]>([]);
|
||||
const [savingPerfil, setSavingPerfil] = useState(false);
|
||||
// propostas
|
||||
const [propostas, setPropostas] = useState<{ enviadas: Proposta[]; recebidas: Proposta[] }>({ enviadas: [], recebidas: [] });
|
||||
@@ -214,7 +215,10 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
|
||||
const fetchPerfil = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try { const res = await apiFetch('/api/profissionais/perfil'); if (res.ok) setPerfil(await res.json()); }
|
||||
try {
|
||||
const res = await apiFetch('/api/profissionais/perfil'); if (res.ok) setPerfil(await res.json());
|
||||
setMyAreas(await HybridBackend.getMyAreas());
|
||||
}
|
||||
catch { /* silently fail */ }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
@@ -253,13 +257,15 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
const res = await apiFetch('/api/profissionais/perfil', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
estado: perfil.estado, cidade: perfil.cidade, bairro: perfil.bairro, cep: perfil.cep,
|
||||
estado: perfil.estado, cidade: perfil.cidade, bairro: perfil.bairro,
|
||||
logradouro: perfil.logradouro, numero: perfil.numero, cep: perfil.cep,
|
||||
especialidade: perfil.especialidade_dir, raio_atuacao_km: perfil.raio_atuacao_km,
|
||||
bio_profissional: perfil.bio_profissional, disponivel_diretorio: perfil.disponivel_diretorio === true,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Erro ao salvar.');
|
||||
await HybridBackend.setMyAreas(myAreas);
|
||||
toast.success('PERFIL PROFISSIONAL ATUALIZADO!');
|
||||
} catch (e: any) { toast.error(e.message.toUpperCase()); }
|
||||
finally { setSavingPerfil(false); }
|
||||
@@ -480,6 +486,23 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
<input type="number" min="0" max="2000" className={inputCls} value={perfil.raio_atuacao_km ?? ''} onChange={e => setP('raio_atuacao_km', e.target.value === '' ? null : +e.target.value)} placeholder="EX: 100" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Áreas de atuação</label>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{areas.map(a => {
|
||||
const on = myAreas.includes(a.slug);
|
||||
return (
|
||||
<button key={a.slug} type="button"
|
||||
onClick={() => setMyAreas(prev => on ? prev.filter(s => s !== a.slug) : [...prev, a.slug])}
|
||||
className={`text-[11px] font-black uppercase px-3 py-1.5 rounded-full border transition-all ${on ? 'text-white border-transparent' : 'text-gray-500 border-gray-200 bg-white hover:bg-gray-50'}`}
|
||||
style={on ? { backgroundColor: a.cor } : undefined}>
|
||||
{a.nome}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-1">Você pode atuar em mais de uma área (ex.: dentista que também faz harmonização).</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Apresentação / currículo resumido</label>
|
||||
<textarea className={inputCls} rows={3} value={perfil.bio_profissional || ''} onChange={e => setP('bio_profissional', e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user