feat(biomedico): catálogo próprio de biomedicina estética (CRUD + ativar/desativar)
- reverte bloqueio anterior: biomédico GERENCIA especialidades/procedimentos do seu consultório (workspace pessoal já criado no cadastro) - acesso liberado p/ o papel biomédico (menu + permissão) - coluna 'ativo' em especialidades/procedimentos + toggle ativar/desativar na UI - auto-seed do catálogo de biomedicina estética (botox, preenchimento, bioestimulador, peeling, microagulhamento, etc.) no cadastro do biomédico + endpoint idempotente /api/me/seed-biomedicina (restrito ao papel biomédico) p/ contas existentes - valor por procedimento já suportado (valorparticular) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+69
-15
@@ -198,15 +198,6 @@ function authGuard(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
// Biomédicos não gerenciam procedimentos/especialidades (são conceitos odontológicos).
|
||||
// Camada de backend além do menu/permissão do front. Fail-open só em erro de DB.
|
||||
async function blockBiomedico(req, res, next) {
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser?.userId]);
|
||||
if (rows[0]?.role === 'biomedico') return res.status(403).json({ error: 'Biomédicos não podem gerenciar procedimentos e especialidades.' });
|
||||
} catch (e) { console.error('[blockBiomedico]', e.message); }
|
||||
next();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDITORIA GENÉRICA + PENDÊNCIAS DE AGENDA (Fase 1)
|
||||
@@ -358,6 +349,45 @@ async function pushGoogleEvent(action, ag) {
|
||||
} catch (e) { console.error('[google-push]', e.message); }
|
||||
}
|
||||
|
||||
// Catálogo padrão de Biomedicina Estética (especialidades + procedimentos com valor sugerido).
|
||||
const BIOMED_ESPECIALIDADES = [
|
||||
'BIOMEDICINA ESTÉTICA', 'HARMONIZAÇÃO OROFACIAL', 'ESTÉTICA FACIAL', 'ESTÉTICA CORPORAL', 'PROCEDIMENTOS INJETÁVEIS',
|
||||
];
|
||||
const BIOMED_PROCEDIMENTOS = [
|
||||
{ nome: 'TOXINA BOTULÍNICA (BOTOX)', esp: 'PROCEDIMENTOS INJETÁVEIS', valor: 800 },
|
||||
{ nome: 'PREENCHIMENTO COM ÁCIDO HIALURÔNICO', esp: 'PROCEDIMENTOS INJETÁVEIS', valor: 1200 },
|
||||
{ nome: 'BIOESTIMULADOR DE COLÁGENO', esp: 'ESTÉTICA FACIAL', valor: 1500 },
|
||||
{ nome: 'SKINBOOSTER', esp: 'ESTÉTICA FACIAL', valor: 900 },
|
||||
{ nome: 'PEELING QUÍMICO', esp: 'ESTÉTICA FACIAL', valor: 300 },
|
||||
{ nome: 'MICROAGULHAMENTO', esp: 'ESTÉTICA FACIAL', valor: 400 },
|
||||
{ nome: 'LIMPEZA DE PELE', esp: 'ESTÉTICA FACIAL', valor: 150 },
|
||||
{ nome: 'FIOS DE SUSTENTAÇÃO (PDO)', esp: 'HARMONIZAÇÃO OROFACIAL', valor: 2000 },
|
||||
{ nome: 'HARMONIZAÇÃO FACIAL', esp: 'HARMONIZAÇÃO OROFACIAL', valor: 2500 },
|
||||
{ nome: 'ESCLEROTERAPIA (VASINHOS)', esp: 'ESTÉTICA CORPORAL', valor: 350 },
|
||||
{ nome: 'INTRADERMOTERAPIA / MESOTERAPIA', esp: 'ESTÉTICA CORPORAL', valor: 400 },
|
||||
{ nome: 'LIPÓLISE ENZIMÁTICA', esp: 'ESTÉTICA CORPORAL', valor: 500 },
|
||||
];
|
||||
// Popula o catálogo de biomedicina de uma clínica/consultório (idempotente: só se vazio).
|
||||
async function seedBiomedicina(clinicaId) {
|
||||
if (!clinicaId) return false;
|
||||
const { rows } = await pool.query('SELECT 1 FROM especialidades WHERE clinica_id = $1 LIMIT 1', [clinicaId]);
|
||||
if (rows.length) return false;
|
||||
const espId = {};
|
||||
for (let i = 0; i < BIOMED_ESPECIALIDADES.length; i++) {
|
||||
const id = `esp_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query('INSERT INTO especialidades (id, nome, clinica_id, ordem, ativo) VALUES ($1,$2,$3,$4,true)', [id, BIOMED_ESPECIALIDADES[i], clinicaId, i]);
|
||||
espId[BIOMED_ESPECIALIDADES[i]] = id;
|
||||
}
|
||||
for (let i = 0; i < BIOMED_PROCEDIMENTOS.length; i++) {
|
||||
const pr = BIOMED_PROCEDIMENTOS[i];
|
||||
const id = `proc_${Date.now()}_${i}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
await pool.query(
|
||||
'INSERT INTO procedimentos (id, nome, especialidadeid, especialidadenome, valorparticular, clinica_id, ordem, ativo) VALUES ($1,$2,$3,$4,$5,$6,$7,true)',
|
||||
[id, pr.nome, espId[pr.esp] || null, pr.esp, pr.valor, clinicaId, i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- REGISTRO DE USUÁRIOS ---
|
||||
app.post('/api/register', async (req, res) => {
|
||||
const { nome, email, senha, role, workspaceNome, especialidade } = req.body;
|
||||
@@ -396,6 +426,8 @@ app.post('/api/register', async (req, res) => {
|
||||
[`v_${userId}_${wsId}`, userId, wsId, userRole]
|
||||
);
|
||||
workspaces = [{ id: wsId, nome: wsNome, cor: '#2563eb', role: userRole }];
|
||||
// Biomédico já nasce com o catálogo de biomedicina estética no consultório dele.
|
||||
if (userRole === 'biomedico') { try { await seedBiomedicina(wsId); } catch (e) { console.error('[seedBiomedicina]', e.message); } }
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId, email: email.toLowerCase().trim() }, JWT_SECRET, { expiresIn: '12h' });
|
||||
@@ -2608,7 +2640,7 @@ app.get('/api/especialidades', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/especialidades', authGuard, blockBiomedico, async (req, res) => {
|
||||
app.post('/api/especialidades', authGuard, async (req, res) => {
|
||||
try {
|
||||
const { rows: userRows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser.userId]);
|
||||
const isSuperAdmin = userRows[0]?.role === 'superadmin';
|
||||
@@ -2635,7 +2667,7 @@ app.put('/api/especialidades/reorder', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/especialidades/:id', authGuard, blockBiomedico, async (req, res) => {
|
||||
app.put('/api/especialidades/:id', async (req, res) => {
|
||||
try {
|
||||
const upd = buildUpdate('especialidades', req.body, 'id', req.params.id);
|
||||
await pool.query(upd.text, upd.values);
|
||||
@@ -2643,7 +2675,7 @@ app.put('/api/especialidades/:id', authGuard, blockBiomedico, async (req, res) =
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.delete('/api/especialidades/:id', tenantGuard, blockBiomedico, async (req, res) => {
|
||||
app.delete('/api/especialidades/:id', tenantGuard, async (req, res) => {
|
||||
try {
|
||||
const clinicaId = req.clinicaId;
|
||||
if (!clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
@@ -2724,7 +2756,7 @@ app.get('/api/procedimentos', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.post('/api/procedimentos', authGuard, blockBiomedico, async (req, res) => {
|
||||
app.post('/api/procedimentos', authGuard, async (req, res) => {
|
||||
try {
|
||||
const { rows: userRows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser.userId]);
|
||||
const isSuperAdmin = userRows[0]?.role === 'superadmin';
|
||||
@@ -2782,7 +2814,7 @@ app.put('/api/procedimentos/reorder', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/procedimentos/:id', authGuard, blockBiomedico, async (req, res) => {
|
||||
app.put('/api/procedimentos/:id', async (req, res) => {
|
||||
try {
|
||||
const { valoresPlanos, ...proc } = req.body;
|
||||
const updateData = { ...proc, exige_face: proc.exige_face ? 1 : 0 };
|
||||
@@ -2804,7 +2836,7 @@ app.put('/api/procedimentos/:id', authGuard, blockBiomedico, async (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/procedimentos/:id', tenantGuard, blockBiomedico, async (req, res) => {
|
||||
app.delete('/api/procedimentos/:id', tenantGuard, async (req, res) => {
|
||||
try {
|
||||
const clinicaId = req.clinicaId;
|
||||
if (!clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' });
|
||||
@@ -4023,6 +4055,8 @@ async function runMigrations() {
|
||||
`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 numero TEXT`,
|
||||
`ALTER TABLE especialidades ADD COLUMN IF NOT EXISTS ativo BOOLEAN DEFAULT true`,
|
||||
`ALTER TABLE procedimentos ADD COLUMN IF NOT EXISTS ativo BOOLEAN DEFAULT true`,
|
||||
`ALTER TABLE clinicas ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_salas_geo ON salas USING GIST (geo)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_usuarios_geo ON usuarios USING GIST (geo)`,
|
||||
@@ -4182,6 +4216,26 @@ app.post('/api/me/plugins', authGuard, async (req, res) => {
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// Semeia o catálogo de biomedicina estética na clínica/consultório do usuário (idempotente).
|
||||
// Usado no 1º acesso do biomédico ao catálogo (cobre contas criadas antes do auto-seed).
|
||||
app.post('/api/me/seed-biomedicina', authGuard, async (req, res) => {
|
||||
try {
|
||||
const clinicaId = String(req.body?.clinicaId || req.query.clinicaId || '').trim();
|
||||
if (!clinicaId) return res.status(400).json({ error: 'clinicaId é obrigatório.' });
|
||||
// só BIOMÉDICO semeia (evita poluir clínica odonto vazia com catálogo de biomedicina)
|
||||
const { rows: u } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser.userId]);
|
||||
if (u[0]?.role !== 'biomedico') return res.status(403).json({ error: 'Apenas biomédicos têm o catálogo de biomedicina.' });
|
||||
// e precisa ser dono/vinculado da unidade
|
||||
const { rows } = await pool.query(
|
||||
`SELECT 1 FROM clinicas WHERE id = $1 AND owner_id = $2
|
||||
UNION SELECT 1 FROM vinculos WHERE clinica_id = $1 AND usuario_id = $2 LIMIT 1`,
|
||||
[clinicaId, req.authUser.userId]);
|
||||
if (!rows.length) return res.status(403).json({ error: 'Sem acesso a esta unidade.' });
|
||||
const seeded = await seedBiomedicina(clinicaId);
|
||||
res.json({ success: true, seeded });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// ── Geocoding de CEP: cache + rotação de provedores grátis ──────────────────
|
||||
// Economia: cada CEP é geocodado UMA vez (cache cep_geo). Rotação robusta p/ o
|
||||
// egress deste servidor: AwesomeAPI → Nominatim(CEP) → ViaCEP+Nominatim(endereço).
|
||||
|
||||
+1
-1
@@ -272,7 +272,7 @@ const App: React.FC = () => {
|
||||
const permissions: Record<string, ViewKey[]> = {
|
||||
paciente: ['tratamentos', 'notificacoes', 'configuracoes'],
|
||||
dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'],
|
||||
biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
protetico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'],
|
||||
};
|
||||
|
||||
@@ -107,8 +107,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
|
||||
if (currentRole === 'paciente') return ['tratamentos', 'notificacoes', 'configuracoes'].includes(item.id);
|
||||
// Dentista: clínico odonto completo (ortodontia, tutoria orto).
|
||||
if (currentRole === 'dentista') return ['dashboard', 'tratamentos', 'agenda', 'ortodontia', 'notificacoes', 'clinicas', 'configuracoes', 'candidatura-tutor', 'diretorio-profissionais'].includes(item.id);
|
||||
// Biomédico(a): SEM itens odontológicos (ortodontia, RX, tutoria orto).
|
||||
if (currentRole === 'biomedico') return ['dashboard', 'agenda', 'tratamentos', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
||||
// Biomédico(a): seu próprio catálogo (especialidades/procedimentos de biomedicina);
|
||||
// SEM itens odontológicos (ortodontia, RX, tutoria orto).
|
||||
if (currentRole === 'biomedico') return ['dashboard', 'agenda', 'tratamentos', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
||||
// Protético: laboratório — agenda própria + GTO; sem ortodontia/RX/pacientes clínicos.
|
||||
if (currentRole === 'protetico') return ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
||||
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'agenda', 'financeiro', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'contratos', 'diretorio-profissionais'].includes(item.id);
|
||||
|
||||
@@ -725,6 +725,20 @@ export const HybridBackend = {
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
// Semeia o catálogo de biomedicina estética no workspace ativo (idempotente).
|
||||
seedBiomedicina: async (): Promise<boolean> => {
|
||||
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
|
||||
if (!clinicaId) return false;
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/me/seed-biomedicina`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clinicaId }),
|
||||
});
|
||||
const d = await res.json();
|
||||
return !!d?.seeded;
|
||||
} catch { return false; }
|
||||
},
|
||||
|
||||
getProcedimentos: async (): Promise<Procedimento[]> => {
|
||||
const clinicaId = HybridBackend.getActiveWorkspace()?.id || '';
|
||||
const res = await apiFetch(`${API_URL}/procedimentos${clinicaId ? `?clinicaId=${clinicaId}` : ''}`);
|
||||
|
||||
@@ -226,6 +226,7 @@ export interface Especialidade {
|
||||
nome: string;
|
||||
descricao: string;
|
||||
ordem: number;
|
||||
ativo?: boolean;
|
||||
}
|
||||
|
||||
export interface ProcedimentoValorPlano {
|
||||
@@ -252,6 +253,7 @@ export interface Procedimento {
|
||||
tipo_regiao: 'DENTE' | 'ARCO' | 'GERAL';
|
||||
exige_face: boolean;
|
||||
valoresPlanos?: ProcedimentoValorPlano[];
|
||||
ativo?: boolean;
|
||||
}
|
||||
|
||||
export interface Lead {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Edit, Trash2, X, FileText, Stethoscope, Award, BookOpen, RefreshCw, Database, Loader2, Mail, Phone, ClipboardList, GripVertical, Share2, Check, Clock, ArrowRight, Activity, AlertTriangle, Users, Calendar, Banknote, UserPlus, Unlink, Copy, KeyRound } from 'lucide-react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Plus, Edit, Trash2, X, FileText, Stethoscope, Award, BookOpen, RefreshCw, Database, Loader2, Mail, Phone, ClipboardList, GripVertical, Share2, Check, Clock, ArrowRight, Activity, AlertTriangle, Users, Calendar, Banknote, UserPlus, Unlink, Copy, KeyRound, ToggleLeft, ToggleRight } from 'lucide-react';
|
||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { Dentista, Especialidade, Plano, Procedimento, ProcedimentoValorPlano, DentistaHorario } from '../types.ts';
|
||||
@@ -338,6 +338,16 @@ export const EspecialidadesView: React.FC = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingEsp, setEditingEsp] = useState<Partial<Especialidade> | null>(null);
|
||||
const toast = useToast();
|
||||
const seedTried = useRef(false);
|
||||
|
||||
// Biomédico com catálogo vazio: semeia biomedicina estética no 1º acesso (idempotente).
|
||||
useEffect(() => {
|
||||
if (seedTried.current || isLoading || !especialidades) return;
|
||||
if (HybridBackend.getCurrentRole() === 'biomedico' && especialidades.length === 0) {
|
||||
seedTried.current = true;
|
||||
HybridBackend.seedBiomedicina().then(seeded => { if (seeded) refresh(); });
|
||||
}
|
||||
}, [isLoading, especialidades, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) return;
|
||||
@@ -378,6 +388,13 @@ export const EspecialidadesView: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleAtivo = async (esp: Especialidade) => {
|
||||
const novo = esp.ativo === false;
|
||||
await HybridBackend.updateEspecialidade({ ...esp, ativo: novo });
|
||||
toast.success(novo ? "Especialidade ativada." : "Especialidade desativada.");
|
||||
refresh();
|
||||
}
|
||||
|
||||
const onDragEnd = async (result: any) => {
|
||||
if (!result.destination || !especialidades) return;
|
||||
|
||||
@@ -426,15 +443,16 @@ export const EspecialidadesView: React.FC = () => {
|
||||
<tr
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`${snapshot.isDragging ? 'bg-blue-50 shadow-md ring-1 ring-blue-200' : 'bg-white'} transition-shadow`}
|
||||
className={`${snapshot.isDragging ? 'bg-blue-50 shadow-md ring-1 ring-blue-200' : 'bg-white'} ${esp.ativo === false ? 'opacity-50' : ''} transition-shadow`}
|
||||
>
|
||||
<td className="px-6 py-4 text-gray-400 cursor-grab active:cursor-grabbing" {...provided.dragHandleProps}>
|
||||
<GripVertical size={18} />
|
||||
</td>
|
||||
<td className="px-6 py-4 font-bold text-gray-800">{esp.nome}</td>
|
||||
<td className="px-6 py-4 font-bold text-gray-800">{esp.nome}{esp.ativo === false && <span className="ml-2 text-[9px] font-black uppercase bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">Inativa</span>}</td>
|
||||
<td className="px-6 py-4 text-gray-600">{esp.descricao}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={() => handleToggleAtivo(esp)} title={esp.ativo === false ? 'Ativar' : 'Desativar'} className="p-2 text-gray-500 hover:bg-gray-100 rounded-md">{esp.ativo === false ? <ToggleLeft size={16} /> : <ToggleRight size={16} className="text-green-600" />}</button>
|
||||
<button onClick={() => { setEditingEsp(esp); setIsModalOpen(true); }} className="p-2 text-gray-500 hover:bg-gray-100 rounded-md"><Edit size={16} /></button>
|
||||
<button onClick={() => handleDelete(esp.id)} className="p-2 text-gray-500 hover:bg-red-50 hover:text-red-600 rounded-md"><Trash2 size={16} /></button>
|
||||
</div>
|
||||
@@ -563,6 +581,13 @@ export const ProcedimentosView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAtivo = async (proc: Procedimento) => {
|
||||
const novo = proc.ativo === false;
|
||||
await HybridBackend.updateProcedimento({ ...proc, ativo: novo });
|
||||
toast.success(novo ? "Procedimento ativado." : "Procedimento desativado.");
|
||||
refresh();
|
||||
};
|
||||
|
||||
const handleAddPlanValue = () => setPlanValues(prev => [...prev, { planoId: '', valor: 0, codigoPlano: '' }]);
|
||||
const handleRemovePlanValue = (index: number) => setPlanValues(prev => prev.filter((_, i) => i !== index));
|
||||
const handlePlanValueChange = (index: number, field: 'planoId' | 'valor' | 'codigoPlano', value: string) => {
|
||||
@@ -628,12 +653,12 @@ export const ProcedimentosView: React.FC = () => {
|
||||
<tr
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`${snapshot.isDragging ? 'bg-blue-50 shadow-md ring-1 ring-blue-200' : 'bg-white'} transition-shadow`}
|
||||
className={`${snapshot.isDragging ? 'bg-blue-50 shadow-md ring-1 ring-blue-200' : 'bg-white'} ${proc.ativo === false ? 'opacity-50' : ''} transition-shadow`}
|
||||
>
|
||||
<td className="px-6 py-4 text-gray-400 cursor-grab active:cursor-grabbing" {...provided.dragHandleProps}>
|
||||
<GripVertical size={18} />
|
||||
</td>
|
||||
<td className="px-6 py-4 font-bold text-gray-800">{proc.nome}</td>
|
||||
<td className="px-6 py-4 font-bold text-gray-800">{proc.nome}{proc.ativo === false && <span className="ml-2 text-[9px] font-black uppercase bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">Inativo</span>}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[10px] font-bold text-gray-400 uppercase">TUSS: {proc.codigo || '---'}</span>
|
||||
@@ -645,6 +670,7 @@ export const ProcedimentosView: React.FC = () => {
|
||||
<td className="px-6 py-4 font-semibold text-gray-700">R$ {Number(proc.valorParticular).toFixed(2)}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={() => handleToggleAtivo(proc)} title={proc.ativo === false ? 'Ativar' : 'Desativar'} className="p-2 text-gray-500 hover:bg-gray-100 rounded-md">{proc.ativo === false ? <ToggleLeft size={16} /> : <ToggleRight size={16} className="text-green-600" />}</button>
|
||||
<button onClick={() => { setEditingProcedimento(proc); setIsModalOpen(true); }} className="p-2 text-gray-500 hover:bg-gray-100 rounded-md"><Edit size={16} /></button>
|
||||
<button onClick={() => handleDelete(proc.id)} className="p-2 text-gray-500 hover:bg-red-50 hover:text-red-600 rounded-md"><Trash2 size={16} /></button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user