feat(clinicas): permite criar unidades adicionais (multi-clínica)
- ClinicasView: botão '+ Nova clínica/consultório' + modal de criação, gateado pelo papel da conta (donoclinica/donoconsultorio/admin). - Corrige applyWorkspace destrutivo no onboarding: usa refreshWorkspaces (acrescenta a unidade à lista, em vez de sobrescrever as existentes). Antes não havia caminho de UI para criar uma 2ª clínica fora do onboarding; o backend já suportava N clínicas. Validado em DEV (8020). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Building2, ArrowRight, Shield, MapPin, BadgeCheck, Palette, Save, Loader2 } from 'lucide-react';
|
||||
import { Building2, ArrowRight, Shield, MapPin, BadgeCheck, Palette, Save, Loader2, Plus, X } from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
import { EmptyState } from '../components/EmptyState.tsx';
|
||||
@@ -24,9 +24,43 @@ export const ClinicasView: React.FC = () => {
|
||||
const currentRole = HybridBackend.getCurrentRole();
|
||||
const isAdmin = currentRole === 'admin' || currentRole === 'donoclinica';
|
||||
|
||||
// Papel da CONTA (não da unidade ativa) — é o que o backend usa para decidir
|
||||
// se a nova unidade nasce como 'clinica' ou 'consultorio'.
|
||||
const accountRole = (() => {
|
||||
try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}').role as string | undefined; }
|
||||
catch { return undefined; }
|
||||
})();
|
||||
const isConsultorioAccount = accountRole === 'donoconsultorio';
|
||||
const unitLabel = isConsultorioAccount ? 'CONSULTÓRIO' : 'CLÍNICA';
|
||||
const canCreate = ['donoclinica', 'donoconsultorio', 'admin'].includes(accountRole || '');
|
||||
|
||||
const [isEditingColor, setIsEditingColor] = useState<string | null>(null);
|
||||
const [savingColor, setSavingColor] = useState(false);
|
||||
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [newNome, setNewNome] = useState('');
|
||||
const [newDoc, setNewDoc] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newNome.trim()) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const result = await HybridBackend.createClinica(newNome.trim(), newDoc.trim() || undefined);
|
||||
if (!result.success) { toast.error(result.message || 'ERRO AO CRIAR UNIDADE.'); return; }
|
||||
// Recarrega a lista COMPLETA do servidor (acrescenta a nova unidade às existentes,
|
||||
// em vez de sobrescrever). A tela relê os workspaces no reload.
|
||||
await HybridBackend.refreshWorkspaces();
|
||||
toast.success(`${unitLabel} CRIADA!`);
|
||||
setTimeout(() => window.location.reload(), 900);
|
||||
} catch {
|
||||
toast.error('ERRO DE CONEXÃO.');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitch = (workspaceId: string) => {
|
||||
if (activeWorkspace?.id === workspaceId) {
|
||||
toast.info("VOCÊ JÁ ESTÁ NESTA UNIDADE.");
|
||||
@@ -81,6 +115,17 @@ export const ClinicasView: React.FC = () => {
|
||||
description="Configure a identidade visual e selecione a clínica para gerenciar atendimentos."
|
||||
/>
|
||||
|
||||
{canCreate && (
|
||||
<div className="flex justify-end -mt-4">
|
||||
<button
|
||||
onClick={() => { setNewNome(''); setNewDoc(''); setShowCreate(true); }}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-2xl bg-teal-600 hover:bg-teal-700 text-white text-xs font-black uppercase tracking-widest shadow-lg transition-colors"
|
||||
>
|
||||
<Plus size={16} /> Nova {unitLabel.toLowerCase()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{workspaces.length === 0 && (
|
||||
<EmptyState icon={Building2} title="Nenhuma unidade ainda" description="Você ainda não tem clínicas ou consultórios vinculados a esta conta." />
|
||||
)}
|
||||
@@ -208,6 +253,54 @@ export const ClinicasView: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
onClick={() => { if (!creating) setShowCreate(false); }}
|
||||
>
|
||||
<div className="bg-white rounded-3xl w-full max-w-md p-8 shadow-2xl" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Nova {unitLabel.toLowerCase()}</h3>
|
||||
<button
|
||||
onClick={() => { if (!creating) setShowCreate(false); }}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-xl transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">Nome do {unitLabel.toLowerCase()} *</label>
|
||||
<input
|
||||
autoFocus
|
||||
required
|
||||
value={newNome}
|
||||
onChange={e => setNewNome(e.target.value.toUpperCase())}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-teal-500 outline-none text-sm font-medium uppercase"
|
||||
placeholder={isConsultorioAccount ? 'EX: DR. SILVA - CONSULTÓRIO' : 'EX: CLÍNICA SORRISO'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-500 mb-1 uppercase">CNPJ / CPF (opcional)</label>
|
||||
<input
|
||||
value={newDoc}
|
||||
onChange={e => setNewDoc(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-teal-500 outline-none text-sm font-medium"
|
||||
placeholder="00.000.000/0000-00"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating || !newNome.trim()}
|
||||
className="w-full bg-teal-600 hover:bg-teal-700 text-white py-3 rounded-xl font-black uppercase flex items-center justify-center gap-2 transition-all shadow-lg disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{creating ? <><Loader2 className="animate-spin" size={18} /> Criando...</> : <><Plus size={18} /> Criar {unitLabel.toLowerCase()}</>}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,17 +10,6 @@ function getStoredUser(): { nome?: string; role?: string } {
|
||||
try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}'); } catch { return {}; }
|
||||
}
|
||||
|
||||
function applyWorkspace(result: any, userRole: string) {
|
||||
const ws = {
|
||||
id: result.clinica.id,
|
||||
nome: result.clinica.nome_fantasia,
|
||||
cor: '#2563eb',
|
||||
role: userRole,
|
||||
};
|
||||
localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify([ws]));
|
||||
localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(ws));
|
||||
}
|
||||
|
||||
export const CreateClinicView: React.FC<CreateClinicViewProps> = ({ onCreated }) => {
|
||||
const storedUser = getStoredUser();
|
||||
const userRole = storedUser.role || 'donoclinica';
|
||||
@@ -37,7 +26,9 @@ export const CreateClinicView: React.FC<CreateClinicViewProps> = ({ onCreated })
|
||||
const doCreate = async (nomeFinal: string, doc?: string) => {
|
||||
const result = await HybridBackend.createClinica(nomeFinal.trim(), doc?.trim() || undefined);
|
||||
if (!result.success) return result;
|
||||
applyWorkspace(result, userRole);
|
||||
// Recarrega a lista do servidor (acrescenta a nova unidade às existentes em vez de
|
||||
// sobrescrever). No onboarding o usuário tem só esta unidade, então vira a ativa.
|
||||
await HybridBackend.refreshWorkspaces();
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user