diff --git a/backend/server.js b/backend/server.js index 88eb900..37648aa 100644 --- a/backend/server.js +++ b/backend/server.js @@ -6700,6 +6700,7 @@ async function runMigrations() { `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`, + `ALTER TABLE salas ADD COLUMN IF NOT EXISTS contrato TEXT`, `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`, @@ -7602,14 +7603,15 @@ app.post('/api/salas', authGuard, async (req, res) => { await pool.query( `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, area, - 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)`, + hora_consulta, turno_consulta, diaria_consulta, semanal_consulta, mensal_consulta, contrato) + 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,$25)`, [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.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.disponivel !== false, area, - b.horaConsulta === true, b.turnoConsulta === true, b.diariaConsulta === true, b.semanalConsulta === true, b.mensalConsulta === true]); + b.horaConsulta === true, b.turnoConsulta === true, b.diariaConsulta === true, b.semanalConsulta === true, b.mensalConsulta === true, + b.contrato || null]); 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 } }); res.json({ success: true, id }); @@ -7626,7 +7628,7 @@ app.put('/api/salas/:id', authGuard, async (req, res) => { 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, valor_hora: b.valorHora, valor_turno: b.valorTurno, valor_diaria: b.valorDiaria, valor_semanal: b.valorSemanal, valor_mensal: b.valorMensal, - area: b.area, disponivel: b.disponivel, + area: b.area, disponivel: b.disponivel, contrato: b.contrato, 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); if (!keys.length) return res.json({ success: true }); @@ -7683,7 +7685,7 @@ app.get('/api/salas/marketplace', authGuard, async (req, res) => { 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, s.area, 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.hora_consulta, s.turno_consulta, s.diaria_consulta, s.semanal_consulta, s.mensal_consulta, s.contrato, 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 ${where} ${order} LIMIT 200`, params); diff --git a/frontend/views/plugins/SalasPlugin.tsx b/frontend/views/plugins/SalasPlugin.tsx index 75beeac..f91dedf 100644 --- a/frontend/views/plugins/SalasPlugin.tsx +++ b/frontend/views/plugins/SalasPlugin.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { DoorOpen, MapPin, Search, RefreshCw, Plus, Pencil, Trash2, X, Save, - CalendarClock, CheckCircle2, XCircle, Clock, Building2, Briefcase, Home, Inbox, Send, Eraser, ChevronUp, ChevronLeft, ChevronRight + CalendarClock, CheckCircle2, XCircle, Clock, Building2, Briefcase, Home, Inbox, Send, Eraser, ChevronUp, ChevronLeft, ChevronRight, FileText } from 'lucide-react'; import { PageHeader } from '../../components/PageHeader.tsx'; import { useToast } from '../../contexts/ToastContext.tsx'; @@ -41,6 +41,7 @@ interface Sala { valor_diaria: string | null; valor_semanal: string | null; valor_mensal: string | null; + contrato?: string | null; hora_consulta?: boolean; turno_consulta?: boolean; diaria_consulta?: boolean; @@ -161,6 +162,29 @@ const UNIDADE: Record = { mensal: { sing: 'mês', plur: 'meses', max: 12 }, }; +// Modelo padrão de contrato de locação — o dono pode editar por sala; se em branco, vale este. +const CONTRATO_MODELO = `CONTRATO DE LOCAÇÃO DE ESPAÇO / SALA + +1. OBJETO +A presente locação tem por objeto a cessão temporária do espaço/sala identificado nesta plataforma, para uso profissional durante o período reservado. + +2. RESPONSABILIDADE +Todos os procedimentos, atendimentos e serviços realizados no espaço durante o período locado são de INTEIRA E EXCLUSIVA RESPONSABILIDADE DO LOCATÁRIO (quem aluga), abrangendo as esferas civil, criminal, ética, sanitária e trabalhista, perante pacientes, terceiros e órgãos de classe. O LOCADOR (dono da sala) não responde por qualquer ato praticado pelo locatário. + +3. USO E CONSERVAÇÃO +O locatário compromete-se a utilizar o espaço e os equipamentos com zelo, devolvendo-os nas mesmas condições em que os recebeu. Eventuais danos serão integralmente ressarcidos pelo locatário. + +4. REGULARIDADE PROFISSIONAL +O locatário declara possuir registro profissional ativo e todas as licenças e autorizações exigidas para o exercício de sua atividade, isentando o locador de qualquer responsabilidade a esse respeito. + +5. PAGAMENTO +O valor e a forma de pagamento são os informados no pedido de reserva e combinados diretamente entre as partes. + +6. CANCELAMENTO +Cancelamentos devem ser comunicados com antecedência razoável, conforme acordado entre as partes. + +Ao aceitar, o LOCATÁRIO declara ter lido e concordado integralmente com os termos acima.`; + 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 padrão de filtros — fonte única em components/filterStyles. @@ -184,6 +208,7 @@ const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved: diariaConsulta: sala?.diaria_consulta === true, semanalConsulta: sala?.semanal_consulta === true, mensalConsulta: sala?.mensal_consulta === true, disponivel: sala?.disponivel !== false, + contrato: sala?.contrato ?? CONTRATO_MODELO, }); const set = (k: string, v: any) => setF(p => ({ ...p, [k]: v })); @@ -250,6 +275,14 @@ const SalaFormModal: React.FC<{ sala: Sala | null; onClose: () => void; onSaved: set('equipamentos', e.target.value)} placeholder="CADEIRA, FOTOPOLIMERIZADOR, RX..." /> +
+
+ + +
+