From e0b37797b939c7e438e2ce6f3291047b516f4bc3 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Tue, 7 Jul 2026 02:26:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(salas):=20filtros=20do=20alugar-salas=20ig?= =?UTF-8?q?uais=20ao=20marketplace=20de=20profissionais=20+=20fix=20pr?= =?UTF-8?q?=C3=B3pria=20sala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Área de filtros de /alugar-salas restilizada igual ao marketplace de profissionais: card com busca no topo, filtros recolhíveis (Ocultar/Mostrar), slider de raio com badge KM, barra de ações (Limpar + Buscar) e busca por texto (nome/cidade/bairro) sobre o resultado. - Fix: o marketplace listava as PRÓPRIAS salas (e as da unidade ativa) — agora exclui owner_usuario_id do usuário e clinica_id da unidade ativa. Não se aluga a própria sala. Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 3 + frontend/views/plugins/SalasPlugin.tsx | 124 +++++++++++++++++-------- 2 files changed, 87 insertions(+), 40 deletions(-) diff --git a/backend/server.js b/backend/server.js index 46a8c57..88eb900 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7662,6 +7662,9 @@ app.get('/api/salas/marketplace', authGuard, async (req, res) => { const params = []; let where = `WHERE s.disponivel = true`; + // Não listar as PRÓPRIAS salas (não se aluga a própria) nem as da unidade ativa. + params.push(req.authUser.userId); where += ` AND s.owner_usuario_id <> $${params.length}`; + if (clinicaId) { params.push(String(clinicaId)); where += ` AND (s.clinica_id IS NULL OR s.clinica_id <> $${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 (cidade) { params.push(`%${String(cidade).toUpperCase()}%`); where += ` AND s.cidade ILIKE $${params.length}`; } diff --git a/frontend/views/plugins/SalasPlugin.tsx b/frontend/views/plugins/SalasPlugin.tsx index 9152203..e8acb04 100644 --- a/frontend/views/plugins/SalasPlugin.tsx +++ b/frontend/views/plugins/SalasPlugin.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { DoorOpen, MapPin, Search, RefreshCw, Plus, Pencil, Trash2, X, Save, - CalendarClock, CheckCircle2, XCircle, Clock, Building2, Briefcase, Home, Inbox, Send + CalendarClock, CheckCircle2, XCircle, Clock, Building2, Briefcase, Home, Inbox, Send, Eraser, ChevronUp } from 'lucide-react'; import { PageHeader } from '../../components/PageHeader.tsx'; import { useToast } from '../../contexts/ToastContext.tsx'; @@ -108,6 +108,9 @@ const precos = (s: Sala): Array<[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 labelCls = 'text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1'; +// Estilo da área de filtros (mesmo do marketplace de profissionais). +const inputNew = 'w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm font-semibold text-gray-700 outline-none focus:border-teal-400 focus:bg-white transition-colors'; +const labelNew = 'text-[11px] font-bold text-gray-500 mb-1.5 block'; // ─── Modal: cadastrar / editar sala ────────────────────────────────────────── const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved: () => void }> = ({ sala, onClose, onSaved }) => { @@ -516,8 +519,15 @@ export const SalasPlugin: React.FC<{ view?: 'minhas' | 'alugar' }> = ({ view }) const [fCep, setFCep] = useState(''); const [fRaio, setFRaio] = useState(''); const [fArea, setFArea] = useState(''); + const [fBusca, setFBusca] = useState(''); + const [showFiltros, setShowFiltros] = useState(true); const [areas, setAreas] = useState<{ slug: string; nome: string }[]>([]); useEffect(() => { HybridBackend.getAreas().then(setAreas); }, []); + const limparFiltros = () => { setFTipo(''); setFEstado(''); setFCidade(''); setFCep(''); setFRaio(''); setFArea(''); setFBusca(''); }; + // Busca por texto (nome/cidade/bairro) aplicada sobre o resultado do backend. + const salasFiltradas = fBusca.trim() + ? salas.filter(s => `${s.nome} ${(s as any).cidade || ''} ${(s as any).bairro || ''}`.toLowerCase().includes(fBusca.trim().toLowerCase())) + : salas; // minhas const [minhas, setMinhas] = useState([]); // reservas @@ -669,50 +679,84 @@ export const SalasPlugin: React.FC<{ view?: 'minhas' | 'alugar' }> = ({ view }) {tab === 'marketplace' && enderecoOk !== false && ( <>
-
-
- - + {/* Busca por nome/cidade/bairro (sobre o resultado) */} +
+ + setFBusca(e.target.value)} + placeholder="Buscar sala por nome, cidade ou bairro..." /> +
+ + {showFiltros && ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + setFCidade(e.target.value.toUpperCase())} placeholder="CAMPO GRANDE" /> +
+
+ + setFCep(e.target.value)} placeholder="79000-000" /> +
+
+
+ + {fRaio || 0} KM +
+ setFRaio(e.target.value === '0' ? '' : e.target.value)} + className="w-full accent-teal-600 cursor-pointer" /> + {!fCep &&

Informe o CEP para filtrar por raio.

} +
+
-
- - -
-
- - -
-
- - setFCidade(e.target.value.toUpperCase())} placeholder="CAMPO GRANDE" /> -
-
- - setFCep(e.target.value)} placeholder="00000-000" /> -
-
- - setFRaio(e.target.value)} placeholder="10" /> + )} + +
+ +
+ +
-
{loading ? (
- ) : salas.length === 0 ? ( + ) : salasFiltradas.length === 0 ? (

Nenhuma sala encontrada

@@ -720,7 +764,7 @@ export const SalasPlugin: React.FC<{ view?: 'minhas' | 'alugar' }> = ({ view })
) : (
- {salas.map(s => setReservingSala(s)} />)} + {salasFiltradas.map(s => setReservingSala(s)} />)}
)}