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:
@@ -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