La política simulations_update está activa en la DB de desarrollo. El fix B está completo.
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Resumen de todo lo entregado en Etapa 7: Fix A1 (src/auth/AuthContext.tsx): isProfileLoaded: boolean agregado como estado. Arranca en false, se pone true en el finally del sync effect (después de syncProfileFromDB), y cuando onAuthStateChange devuelve null (usuario no autenticado). Se resetea a false en cada SIGN_IN para gatear el re-sync. Fix A2 (src/features/library/LibraryPage.tsx): Guard temprano después de todos los hooks — si !user || !isProfileLoaded devuelve un skeleton de tres columnas animado, idéntico al que usa HomeView. El contenido rol-dependiente solo se renderiza cuando el perfil está sincronizado. Fix A3 (GlobalSettingsPanel.tsx + WorkspacePage.tsx): El campo "Cliente" es texto estático (<p>) para client_editor y client_viewer. En el topbar de WorkspacePage se oculta el input inline y el ícono de lápiz; si hay un clientName se muestra como texto inerte. Fix B (supabase/migrations/016_add_simulations_update_policy.sql): Migración creada + aplicada vía Management API a inq-roi-desa. pg_policies confirma las 4 políticas: simulations_select, simulations_insert, simulations_update, simulations_delete. Tests: tests/features/library/library-ui.test.tsx actualizado — mock incluye isProfileLoaded, 2 tests nuevos para isProfileLoaded: false. Total: 586 tests, todos verdes. Build limpio.
This commit is contained in:
@@ -6,6 +6,7 @@ import type { AuthUser } from './types'
|
||||
interface AuthContextValue {
|
||||
user: AuthUser | null
|
||||
loading: boolean
|
||||
isProfileLoaded: boolean
|
||||
signIn: () => Promise<void>
|
||||
signInWithEmailPassword: (email: string, password: string) => Promise<{ error: string | null }>
|
||||
signOut: () => Promise<void>
|
||||
@@ -65,6 +66,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
// Si es un OAuth redirect, mantener loading=true hasta que Supabase procese el token.
|
||||
const [loading, setLoading] = useState(isOAuthRedirect)
|
||||
const [pendingPasswordReset, setPendingPasswordReset] = useState(false)
|
||||
const [isProfileLoaded, setIsProfileLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Verificar y sincronizar con Supabase en background.
|
||||
@@ -73,6 +75,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const unsubscribe = authService.onAuthStateChange((u) => {
|
||||
setUser(u)
|
||||
setLoading(false)
|
||||
// Si no hay usuario, no hay nada que sincronizar → marcar como cargado.
|
||||
// Si hay usuario, la sincronización con DB reseteará a false → true al completar.
|
||||
if (!u) setIsProfileLoaded(true)
|
||||
else setIsProfileLoaded(false)
|
||||
})
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
@@ -126,6 +132,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
console.error('Error al sincronizar perfil:', err)
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
setIsProfileLoaded(true)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -158,6 +165,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
loading,
|
||||
isProfileLoaded,
|
||||
signIn,
|
||||
signInWithEmailPassword,
|
||||
signOut,
|
||||
|
||||
@@ -773,7 +773,7 @@ function GroupView({
|
||||
export function LibraryPage() {
|
||||
const navigate = useNavigate()
|
||||
const { load, processes, groups, settings } = useLibraryStore()
|
||||
const { user } = useAuth()
|
||||
const { user, isProfileLoaded } = useAuth()
|
||||
const [view, setView] = useState<'home' | 'group'>('home')
|
||||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null)
|
||||
const [transitioning, setTransitioning] = useState(false)
|
||||
@@ -811,10 +811,31 @@ export function LibraryPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const currentUserId = user!.id
|
||||
const currentUserName = user!.name
|
||||
// Esperar a que syncProfileFromDB complete antes de renderizar UI rol-dependiente.
|
||||
// Evita el flash donde 'guest' se muestra como platform_admin durante la ventana de sync.
|
||||
if (!user || !isProfileLoaded) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<AppHeader />
|
||||
<div className="max-w-4xl mx-auto px-6 py-8">
|
||||
<div className="space-y-5 animate-pulse">
|
||||
<div className="h-4 w-32 bg-slate-100 rounded" />
|
||||
<div className="grid gap-2.5" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))' }}>
|
||||
{[1, 2, 3].map((i) => <div key={i} className="h-20 rounded-xl bg-slate-100" />)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2].map((i) => <div key={i} className="h-14 rounded-xl bg-slate-100" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const currentUserId = user.id
|
||||
const currentUserName = user.name
|
||||
const totalGroups = groups.length
|
||||
const isClientRole = user?.platformRole === 'client_editor' || user?.platformRole === 'client_viewer'
|
||||
const isClientRole = user.platformRole === 'client_editor' || user.platformRole === 'client_viewer'
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
|
||||
@@ -26,6 +26,7 @@ const CURRENCIES = [
|
||||
export function GlobalSettingsPanel() {
|
||||
const { currentProcess, updateProcess } = useProcessStore()
|
||||
const { user } = useAuth()
|
||||
const isClientRole = user?.platformRole === 'client_editor' || user?.platformRole === 'client_viewer'
|
||||
const [localName, setLocalName] = useState('')
|
||||
const [localClient, setLocalClient] = useState('')
|
||||
const [localCurrency, setLocalCurrency] = useState('USD')
|
||||
@@ -83,14 +84,18 @@ export function GlobalSettingsPanel() {
|
||||
|
||||
{/* Cliente */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="client-name" className="text-xs font-medium">Cliente</Label>
|
||||
<Input
|
||||
id="client-name"
|
||||
value={localClient}
|
||||
onChange={(e) => { setLocalClient(e.target.value); markDirty() }}
|
||||
placeholder="ej. Empresa ABC"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<Label className="text-xs font-medium">Cliente</Label>
|
||||
{isClientRole ? (
|
||||
<p className="text-sm text-slate-600 h-8 flex items-center px-1">{localClient || '—'}</p>
|
||||
) : (
|
||||
<Input
|
||||
id="client-name"
|
||||
value={localClient}
|
||||
onChange={(e) => { setLocalClient(e.target.value); markDirty() }}
|
||||
placeholder="ej. Empresa ABC"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Moneda */}
|
||||
|
||||
@@ -235,8 +235,12 @@ export function WorkspacePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cliente — editable inline */}
|
||||
{editingField === 'client' ? (
|
||||
{/* Cliente — editable inline solo para roles internos */}
|
||||
{user?.platformRole === 'client_editor' ? (
|
||||
currentProcess.clientName && (
|
||||
<p className="text-xs text-slate-400 truncate">{currentProcess.clientName}</p>
|
||||
)
|
||||
) : editingField === 'client' ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="text-xs text-slate-400 bg-transparent border-b border-[#F59845] outline-none w-full"
|
||||
|
||||
Reference in New Issue
Block a user