feat(salas): área por sala + preço por modalidade (fixo ou sob consulta)

- salas.area + filtro por área no marketplace + badge no card + seletor no cadastro
- preço por modalidade: hora/turno/diária/semanal/mensal podem ser FIXO (valor) ou
  SOB CONSULTA (flags *_consulta) — precos() mostra 'Sob consulta'
- backfill de área das salas pelo papel do dono
- POST/PUT salas aceitam area + flags; marketplace retorna/filtra por área

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-14 07:36:35 +02:00
parent 97783c1e34
commit 92a0ec3917
2 changed files with 81 additions and 18 deletions
+22 -6
View File
@@ -4065,6 +4065,15 @@ async function runMigrations() {
)`, )`,
// Coluna geográfica (ponto WGS84) p/ busca por raio em salas e profissionais. // Coluna geográfica (ponto WGS84) p/ busca por raio em salas e profissionais.
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`, `ALTER TABLE salas ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`,
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS area TEXT`,
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS hora_consulta BOOLEAN DEFAULT false`,
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS turno_consulta BOOLEAN DEFAULT false`,
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS diaria_consulta BOOLEAN DEFAULT false`,
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS semanal_consulta BOOLEAN DEFAULT false`,
`ALTER TABLE salas ADD COLUMN IF NOT EXISTS mensal_consulta BOOLEAN DEFAULT false`,
`UPDATE salas s SET area = CASE u.role WHEN 'biomedico' THEN 'biomedicina' WHEN 'protetico' THEN 'protese' ELSE 'odontologia' END
FROM usuarios u WHERE u.id = s.owner_usuario_id AND s.area IS NULL`,
`UPDATE salas SET area = 'odontologia' WHERE area IS NULL`,
`ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`, `ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`,
`ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS logradouro TEXT`, `ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS logradouro TEXT`,
`ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS numero TEXT`, `ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS numero TEXT`,
@@ -4426,15 +4435,19 @@ app.post('/api/salas', authGuard, async (req, res) => {
const b = req.body || {}; const b = req.body || {};
if (!b.nome || !b.cidade || !b.estado) return res.status(400).json({ error: 'Nome, cidade e estado são obrigatórios.' }); if (!b.nome || !b.cidade || !b.estado) return res.status(400).json({ error: 'Nome, cidade e estado são obrigatórios.' });
const id = `sala_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`; const id = `sala_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
let area = b.area;
if (!area) { const { rows: ur } = await pool.query('SELECT role FROM usuarios WHERE id=$1', [req.authUser.userId]); area = roleToArea(ur[0]?.role); }
await pool.query( await pool.query(
`INSERT INTO salas (id, owner_usuario_id, clinica_id, nome, tipo, descricao, equipamentos, endereco, bairro, cidade, estado, cep, `INSERT INTO salas (id, owner_usuario_id, clinica_id, nome, tipo, descricao, equipamentos, endereco, bairro, cidade, estado, cep,
valor_hora, valor_turno, valor_diaria, valor_semanal, valor_mensal, disponivel) valor_hora, valor_turno, valor_diaria, valor_semanal, valor_mensal, disponivel, area,
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)`, hora_consulta, turno_consulta, diaria_consulta, semanal_consulta, mensal_consulta)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)`,
[id, req.authUser.userId, b.clinicaId || null, b.nome, SALA_TIPOS.includes(b.tipo) ? b.tipo : 'independente', [id, req.authUser.userId, b.clinicaId || null, b.nome, SALA_TIPOS.includes(b.tipo) ? b.tipo : 'independente',
b.descricao || null, b.equipamentos || null, b.endereco || null, b.descricao || null, b.equipamentos || null, b.endereco || null,
b.bairro?.toUpperCase() || null, String(b.cidade).toUpperCase(), String(b.estado).toUpperCase(), b.cep || null, b.bairro?.toUpperCase() || null, String(b.cidade).toUpperCase(), String(b.estado).toUpperCase(), b.cep || null,
b.valorHora ?? null, b.valorTurno ?? null, b.valorDiaria ?? null, b.valorSemanal ?? null, b.valorMensal ?? null, b.valorHora ?? null, b.valorTurno ?? null, b.valorDiaria ?? null, b.valorSemanal ?? null, b.valorMensal ?? null,
b.disponivel !== false]); b.disponivel !== false, area,
b.horaConsulta === true, b.turnoConsulta === true, b.diariaConsulta === true, b.semanalConsulta === true, b.mensalConsulta === true]);
await setGeoByCep('salas', id, b.cep); await setGeoByCep('salas', id, b.cep);
await registrarAudit(pool, { clinicaId: b.clinicaId || null, entidade: 'sala', entidadeId: id, acao: 'criou', actorId: req.authUser.userId, actorNome: await actorNome(req.authUser.userId), detalhes: { nome: b.nome } }); await registrarAudit(pool, { clinicaId: b.clinicaId || null, entidade: 'sala', entidadeId: id, acao: 'criou', actorId: req.authUser.userId, actorNome: await actorNome(req.authUser.userId), detalhes: { nome: b.nome } });
res.json({ success: true, id }); res.json({ success: true, id });
@@ -4451,7 +4464,8 @@ app.put('/api/salas/:id', authGuard, async (req, res) => {
endereco: b.endereco, bairro: b.bairro?.toUpperCase(), cidade: b.cidade ? String(b.cidade).toUpperCase() : undefined, endereco: b.endereco, bairro: b.bairro?.toUpperCase(), cidade: b.cidade ? String(b.cidade).toUpperCase() : undefined,
estado: b.estado ? String(b.estado).toUpperCase() : undefined, cep: b.cep, estado: b.estado ? String(b.estado).toUpperCase() : undefined, cep: b.cep,
valor_hora: b.valorHora, valor_turno: b.valorTurno, valor_diaria: b.valorDiaria, valor_semanal: b.valorSemanal, valor_mensal: b.valorMensal, valor_hora: b.valorHora, valor_turno: b.valorTurno, valor_diaria: b.valorDiaria, valor_semanal: b.valorSemanal, valor_mensal: b.valorMensal,
disponivel: b.disponivel }; area: b.area, disponivel: b.disponivel,
hora_consulta: b.horaConsulta, turno_consulta: b.turnoConsulta, diaria_consulta: b.diariaConsulta, semanal_consulta: b.semanalConsulta, mensal_consulta: b.mensalConsulta };
const keys = Object.keys(COLS).filter(k => COLS[k] !== undefined); const keys = Object.keys(COLS).filter(k => COLS[k] !== undefined);
if (!keys.length) return res.json({ success: true }); if (!keys.length) return res.json({ success: true });
const set = keys.map((k, i) => `${k} = $${i + 1}`).join(', '); const set = keys.map((k, i) => `${k} = $${i + 1}`).join(', ');
@@ -4479,7 +4493,7 @@ app.delete('/api/salas/:id', authGuard, async (req, res) => {
// Marketplace de salas (qualquer usuário autenticado) // Marketplace de salas (qualquer usuário autenticado)
app.get('/api/salas/marketplace', authGuard, async (req, res) => { app.get('/api/salas/marketplace', authGuard, async (req, res) => {
try { try {
const { tipo, estado, cidade, cep, raio, clinicaId } = req.query; const { tipo, estado, cidade, cep, raio, clinicaId, area } = req.query;
const raioKm = Number(raio); const raioKm = Number(raio);
// Origem = CEP digitado → clínica ativa → próprio usuário (proximidade automática). // Origem = CEP digitado → clínica ativa → próprio usuário (proximidade automática).
const origin = await resolveOrigem(req, cep, clinicaId); const origin = await resolveOrigem(req, cep, clinicaId);
@@ -4489,6 +4503,7 @@ app.get('/api/salas/marketplace', authGuard, async (req, res) => {
if (tipo && SALA_TIPOS.includes(tipo)) { params.push(tipo); where += ` AND s.tipo = $${params.length}`; } if (tipo && SALA_TIPOS.includes(tipo)) { params.push(tipo); where += ` AND s.tipo = $${params.length}`; }
if (estado) { params.push(String(estado).toUpperCase()); where += ` AND s.estado = $${params.length}`; } if (estado) { params.push(String(estado).toUpperCase()); where += ` AND s.estado = $${params.length}`; }
if (cidade) { params.push(`%${String(cidade).toUpperCase()}%`); where += ` AND s.cidade ILIKE $${params.length}`; } if (cidade) { params.push(`%${String(cidade).toUpperCase()}%`); where += ` AND s.cidade ILIKE $${params.length}`; }
if (area) { params.push(String(area)); where += ` AND s.area = $${params.length}`; }
let distSelect = 'NULL::float AS dist_km'; let distSelect = 'NULL::float AS dist_km';
let order = 'ORDER BY s.estado, s.cidade, s.nome'; let order = 'ORDER BY s.estado, s.cidade, s.nome';
@@ -4501,8 +4516,9 @@ app.get('/api/salas/marketplace', authGuard, async (req, res) => {
if (Number.isFinite(raioKm) && raioKm > 0) { params.push(raioKm * 1000); where += ` AND s.geo IS NOT NULL AND ST_DWithin(s.geo, ${pt}, $${params.length})`; } if (Number.isFinite(raioKm) && raioKm > 0) { params.push(raioKm * 1000); where += ` AND s.geo IS NOT NULL AND ST_DWithin(s.geo, ${pt}, $${params.length})`; }
} }
const { rows } = await pool.query( const { rows } = await pool.query(
`SELECT s.id, s.nome, s.tipo, s.descricao, s.equipamentos, s.endereco, s.bairro, s.cidade, s.estado, s.cep, `SELECT s.id, s.nome, s.tipo, s.descricao, s.equipamentos, s.endereco, s.bairro, s.cidade, s.estado, s.cep, s.area,
s.valor_hora, s.valor_turno, s.valor_diaria, s.valor_semanal, s.valor_mensal, s.valor_hora, s.valor_turno, s.valor_diaria, s.valor_semanal, s.valor_mensal,
s.hora_consulta, s.turno_consulta, s.diaria_consulta, s.semanal_consulta, s.mensal_consulta,
s.owner_usuario_id, u.nome AS owner_nome, c.nome_fantasia AS clinica_nome, ${distSelect} s.owner_usuario_id, u.nome AS owner_nome, c.nome_fantasia AS clinica_nome, ${distSelect}
FROM salas s LEFT JOIN usuarios u ON u.id = s.owner_usuario_id LEFT JOIN clinicas c ON c.id = s.clinica_id FROM salas s LEFT JOIN usuarios u ON u.id = s.owner_usuario_id LEFT JOIN clinicas c ON c.id = s.clinica_id
${where} ${order} LIMIT 200`, params); ${where} ${order} LIMIT 200`, params);
+59 -12
View File
@@ -40,6 +40,12 @@ interface Sala {
valor_diaria: string | null; valor_diaria: string | null;
valor_semanal: string | null; valor_semanal: string | null;
valor_mensal: string | null; valor_mensal: string | null;
hora_consulta?: boolean;
turno_consulta?: boolean;
diaria_consulta?: boolean;
semanal_consulta?: boolean;
mensal_consulta?: boolean;
area?: string;
disponivel?: boolean; disponivel?: boolean;
owner_nome?: string; owner_nome?: string;
clinica_id?: string | null; clinica_id?: string | null;
@@ -71,6 +77,12 @@ const TIPO_META: Record<string, { label: string; icon: any }> = {
independente: { label: 'Sala Independente', icon: Home }, independente: { label: 'Sala Independente', icon: Home },
}; };
const AREA_META: Record<string, { nome: string; cor: string }> = {
odontologia: { nome: 'Odontologia', cor: '#2563eb' },
biomedicina: { nome: 'Biomedicina', cor: '#db2777' },
protese: { nome: 'Prótese', cor: '#7c3aed' },
};
const STATUS_STYLE: Record<string, string> = { const STATUS_STYLE: Record<string, string> = {
pendente: 'bg-amber-100 text-amber-700', pendente: 'bg-amber-100 text-amber-700',
confirmada: 'bg-emerald-100 text-emerald-700', confirmada: 'bg-emerald-100 text-emerald-700',
@@ -81,9 +93,16 @@ const STATUS_STYLE: Record<string, string> = {
const fmtValor = (v: string | null) => (v == null || v === '' ? null : Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })); const fmtValor = (v: string | null) => (v == null || v === '' ? null : Number(v).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }));
const fmtData = (iso: string) => new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }); const fmtData = (iso: string) => new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
const precos = (s: Sala) => [ const precos = (s: Sala): Array<[string, string]> => {
['HORA', s.valor_hora], ['TURNO', s.valor_turno], ['DIÁRIA', s.valor_diaria], ['SEMANA', s.valor_semanal], ['MÊS', s.valor_mensal], const mods: Array<[string, string | null, boolean | undefined]> = [
].filter(([, v]) => fmtValor(v as string | null)) as Array<[string, string]>; ['HORA', s.valor_hora, s.hora_consulta], ['TURNO', s.valor_turno, s.turno_consulta],
['DIÁRIA', s.valor_diaria, s.diaria_consulta], ['SEMANA', s.valor_semanal, s.semanal_consulta],
['MÊS', s.valor_mensal, s.mensal_consulta],
];
return mods
.filter(([, v, c]) => fmtValor(v as string | null) || c)
.map(([l, v]) => [l, fmtValor(v as string | null) || 'Sob consulta'] as [string, string]);
};
const inputCls = 'w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold text-gray-700 outline-none focus:border-teal-400'; const inputCls = 'w-full px-3 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold text-gray-700 outline-none focus:border-teal-400';
const labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1'; const labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1';
@@ -94,12 +113,17 @@ const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved:
const ws = HybridBackend.getActiveWorkspace(); const ws = HybridBackend.getActiveWorkspace();
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [vincularUnidade, setVincularUnidade] = useState(sala ? !!sala.clinica_id : !!ws?.id); const [vincularUnidade, setVincularUnidade] = useState(sala ? !!sala.clinica_id : !!ws?.id);
const [areas, setAreas] = useState<{ slug: string; nome: string }[]>([]);
useEffect(() => { HybridBackend.getAreas().then(setAreas); }, []);
const [f, setF] = useState({ const [f, setF] = useState({
nome: sala?.nome || '', tipo: sala?.tipo || 'independente', descricao: sala?.descricao || '', nome: sala?.nome || '', tipo: sala?.tipo || 'independente', descricao: sala?.descricao || '',
equipamentos: sala?.equipamentos || '', endereco: sala?.endereco || '', bairro: sala?.bairro || '', equipamentos: sala?.equipamentos || '', endereco: sala?.endereco || '', bairro: sala?.bairro || '',
cidade: sala?.cidade || '', estado: sala?.estado || '', cep: sala?.cep || '', cidade: sala?.cidade || '', estado: sala?.estado || '', cep: sala?.cep || '', area: sala?.area || '',
valorHora: sala?.valor_hora || '', valorTurno: sala?.valor_turno || '', valorDiaria: sala?.valor_diaria || '', valorHora: sala?.valor_hora || '', valorTurno: sala?.valor_turno || '', valorDiaria: sala?.valor_diaria || '',
valorSemanal: sala?.valor_semanal || '', valorMensal: sala?.valor_mensal || '', valorSemanal: sala?.valor_semanal || '', valorMensal: sala?.valor_mensal || '',
horaConsulta: sala?.hora_consulta === true, turnoConsulta: sala?.turno_consulta === true,
diariaConsulta: sala?.diaria_consulta === true, semanalConsulta: sala?.semanal_consulta === true,
mensalConsulta: sala?.mensal_consulta === true,
disponivel: sala?.disponivel !== false, disponivel: sala?.disponivel !== false,
}); });
const set = (k: string, v: any) => setF(p => ({ ...p, [k]: v })); const set = (k: string, v: any) => setF(p => ({ ...p, [k]: v }));
@@ -201,12 +225,23 @@ const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved:
</label> </label>
)} )}
<div> <div>
<p className={labelCls}>Valores de locação (R$) preencha os modelos oferecidos</p> <label className={labelCls}>Área da sala</label>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2"> <select className={inputCls} value={f.area} onChange={e => set('area', e.target.value)}>
{([['valorHora', 'HORA'], ['valorTurno', 'TURNO'], ['valorDiaria', 'DIÁRIA'], ['valorSemanal', 'SEMANAL'], ['valorMensal', 'MENSAL']] as const).map(([k, l]) => ( <option value=""> Área (padrão pelo seu perfil) </option>
<div key={k}> {areas.map(a => <option key={a.slug} value={a.slug}>{a.nome}</option>)}
<label className={labelCls}>{l}</label> </select>
<input type="number" min="0" className={inputCls} value={(f as any)[k]} onChange={e => set(k, e.target.value)} placeholder="—" /> </div>
<div>
<p className={labelCls}>Valores de locação fixo (R$) ou "sob consulta" por modalidade</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{([['valorHora', 'horaConsulta', 'HORA'], ['valorTurno', 'turnoConsulta', 'TURNO'], ['valorDiaria', 'diariaConsulta', 'DIÁRIA'], ['valorSemanal', 'semanalConsulta', 'SEMANAL'], ['valorMensal', 'mensalConsulta', 'MENSAL']] as const).map(([vk, ck, l]) => (
<div key={vk} className="flex items-center gap-2">
<label className={`${labelCls} w-16 mb-0 shrink-0`}>{l}</label>
<input type="number" min="0" disabled={(f as any)[ck]} value={(f as any)[ck] ? '' : (f as any)[vk]} onChange={e => set(vk, e.target.value)}
className={`${inputCls} flex-1 ${(f as any)[ck] ? 'opacity-40' : ''}`} placeholder={(f as any)[ck] ? 'sob consulta' : '—'} />
<label className="flex items-center gap-1 text-[9px] font-bold text-gray-500 uppercase cursor-pointer whitespace-nowrap">
<input type="checkbox" checked={(f as any)[ck]} onChange={e => set(ck, e.target.checked)} className="w-3.5 h-3.5 accent-teal-600" /> sob consulta
</label>
</div> </div>
))} ))}
</div> </div>
@@ -367,6 +402,7 @@ const SalaCard: React.FC<{ sala: Sala; mine?: boolean; onReserve?: () => void; o
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="font-black text-gray-900 text-sm uppercase truncate">{sala.nome}</span> <span className="font-black text-gray-900 text-sm uppercase truncate">{sala.nome}</span>
<span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase" style={{ backgroundColor: `${COLOR}15`, color: COLOR }}>{meta.label}</span> <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase" style={{ backgroundColor: `${COLOR}15`, color: COLOR }}>{meta.label}</span>
{sala.area && AREA_META[sala.area] && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase text-white" style={{ backgroundColor: AREA_META[sala.area].cor }}>{AREA_META[sala.area].nome}</span>}
{mine && sala.disponivel === false && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-gray-100 text-gray-500">OCULTA</span>} {mine && sala.disponivel === false && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-gray-100 text-gray-500">OCULTA</span>}
{mine && (sala.reservas_pendentes || 0) > 0 && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-amber-100 text-amber-700">{sala.reservas_pendentes} PENDENTE{sala.reservas_pendentes! > 1 ? 'S' : ''}</span>} {mine && (sala.reservas_pendentes || 0) > 0 && <span className="text-[9px] font-black px-2 py-0.5 rounded-full uppercase bg-amber-100 text-amber-700">{sala.reservas_pendentes} PENDENTE{sala.reservas_pendentes! > 1 ? 'S' : ''}</span>}
</div> </div>
@@ -472,6 +508,9 @@ export const SalasPlugin: React.FC = () => {
const [fCidade, setFCidade] = useState(''); const [fCidade, setFCidade] = useState('');
const [fCep, setFCep] = useState(''); const [fCep, setFCep] = useState('');
const [fRaio, setFRaio] = useState(''); const [fRaio, setFRaio] = useState('');
const [fArea, setFArea] = useState('');
const [areas, setAreas] = useState<{ slug: string; nome: string }[]>([]);
useEffect(() => { HybridBackend.getAreas().then(setAreas); }, []);
// minhas // minhas
const [minhas, setMinhas] = useState<Sala[]>([]); const [minhas, setMinhas] = useState<Sala[]>([]);
// reservas // reservas
@@ -511,6 +550,7 @@ export const SalasPlugin: React.FC = () => {
if (fTipo) params.set('tipo', fTipo); if (fTipo) params.set('tipo', fTipo);
if (fEstado) params.set('estado', fEstado); if (fEstado) params.set('estado', fEstado);
if (fCidade) params.set('cidade', fCidade); if (fCidade) params.set('cidade', fCidade);
if (fArea) params.set('area', fArea);
// Origem automática: clínica/consultório ativo (backend cai no geo do usuário se faltar). // Origem automática: clínica/consultório ativo (backend cai no geo do usuário se faltar).
const wsId = HybridBackend.getActiveWorkspace()?.id; const wsId = HybridBackend.getActiveWorkspace()?.id;
if (wsId) params.set('clinicaId', wsId); if (wsId) params.set('clinicaId', wsId);
@@ -520,7 +560,7 @@ export const SalasPlugin: React.FC = () => {
if (res.ok) setSalas(await res.json()); if (res.ok) setSalas(await res.json());
} catch { /* silently fail */ } } catch { /* silently fail */ }
finally { setLoading(false); } finally { setLoading(false); }
}, [fTipo, fEstado, fCidade, fCep, fRaio]); }, [fTipo, fEstado, fCidade, fCep, fRaio, fArea]);
const fetchMinhas = useCallback(async () => { const fetchMinhas = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -610,7 +650,14 @@ export const SalasPlugin: React.FC = () => {
{tab === 'marketplace' && enderecoOk !== false && ( {tab === 'marketplace' && enderecoOk !== false && (
<> <>
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5"> <div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4"> <div className="grid grid-cols-2 md:grid-cols-5 gap-3 mb-4">
<div>
<label className={labelCls}>Área</label>
<select className={inputCls} value={fArea} onChange={e => setFArea(e.target.value)}>
<option value="">Todas</option>
{areas.map(a => <option key={a.slug} value={a.slug}>{a.nome}</option>)}
</select>
</div>
<div> <div>
<label className={labelCls}>Tipo</label> <label className={labelCls}>Tipo</label>
<select className={inputCls} value={fTipo} onChange={e => setFTipo(e.target.value)}> <select className={inputCls} value={fTipo} onChange={e => setFTipo(e.target.value)}>