Compare commits

..

4 Commits

Author SHA1 Message Date
d776af5f24 docs(changelog): etapa 4.5 sprint 8
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 21:57:40 -03:00
cc5563974a fix(library): scroll a sección de org al volver del workspace con ?org param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 21:57:29 -03:00
c6c88501d1 fix(workspace): handleBack pasa ?org=orgId para scroll contextual en biblioteca
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 21:57:25 -03:00
1813a9396a fix(admin): InQuality visible en OrganizationsPage con badge — quitar filtro is_provider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 21:57:20 -03:00
6 changed files with 55 additions and 11 deletions

View File

@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Fixed
- OrganizationsPage: InQuality visible en lista con badge naranja — quitar filtro is_provider (Sprint 8)
- WorkspacePage: back button pasa ?org=orgId para scroll contextual al volver a biblioteca (Sprint 8)
- LibraryPage: scroll automático a sección de org al recibir ?org param desde workspace (Sprint 8)
- router.tsx: redirect de settingsRoute pasa search: { org: undefined } para compatibilidad con validateSearch (Sprint 8)
### Added ### Added
- GlobalSettingsPanel: selector de área reemplaza campo libre "Cliente" (Sprint 8) - GlobalSettingsPanel: selector de área reemplaza campo libre "Cliente" (Sprint 8)
- WorkspacePage topbar: display de `areaName` de solo lectura reemplaza edición inline de cliente (Sprint 8) - WorkspacePage topbar: display de `areaName` de solo lectura reemplaza edición inline de cliente (Sprint 8)

View File

@@ -17,6 +17,7 @@ import {
interface OrgRow { interface OrgRow {
id: string id: string
name: string name: string
is_provider: boolean
created_at: string created_at: string
processes: [{ count: number }] processes: [{ count: number }]
users: [{ count: number }] users: [{ count: number }]
@@ -41,10 +42,16 @@ export function OrganizationsPage() {
const fetchOrgs = async () => { const fetchOrgs = async () => {
const { data } = await supabase const { data } = await supabase
.from('organizations') .from('organizations')
.select('id, name, created_at, processes:processes(count), users:users(count)') .select('id, name, is_provider, created_at, processes:processes(count), users:users(count)')
.eq('is_provider', false)
.order('name') .order('name')
setOrgs((data as OrgRow[] | null) ?? [])
const sorted = ((data as OrgRow[] | null) ?? []).sort((a, b) => {
if (a.is_provider && !b.is_provider) return -1
if (!a.is_provider && b.is_provider) return 1
return a.name.localeCompare(b.name)
})
setOrgs(sorted)
setLoading(false) setLoading(false)
} }
@@ -84,7 +91,7 @@ export function OrganizationsPage() {
return ( return (
<> <>
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-5">
<h2 className="text-lg font-semibold text-slate-900">Organizaciones clientes</h2> <h2 className="text-lg font-semibold text-slate-900">Organizaciones</h2>
<Button size="sm" onClick={openCreate}> <Button size="sm" onClick={openCreate}>
<Plus className="h-4 w-4 mr-1.5" /> <Plus className="h-4 w-4 mr-1.5" />
Nueva organización Nueva organización
@@ -113,7 +120,17 @@ export function OrganizationsPage() {
<tbody> <tbody>
{orgs.map((org) => ( {orgs.map((org) => (
<tr key={org.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/50 transition-colors"> <tr key={org.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/50 transition-colors">
<td className="py-3 px-4 font-medium text-slate-900">{org.name}</td> <td className="py-3 px-4 font-medium text-slate-900">
<div className="flex items-center gap-2">
{org.name}
{org.is_provider && (
<span className="text-[10px] font-semibold px-1.5 py-0.5 rounded-full border"
style={{ color: '#F59845', background: '#fff8f0', borderColor: '#fcd9aa' }}>
InQuality
</span>
)}
</div>
</td>
<td className="py-3 px-4 text-right text-slate-600 tabular-nums"> <td className="py-3 px-4 text-right text-slate-600 tabular-nums">
{org.processes[0]?.count ?? 0} {org.processes[0]?.count ?? 0}
</td> </td>

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'
import { useNavigate } from '@tanstack/react-router' import { useNavigate, useLocation } from '@tanstack/react-router'
import { import {
Upload, Search, Building2, GitBranch, Upload, Search, Building2, GitBranch,
Clock, TrendingUp, BarChart2, FileX2, X, Settings, Clock, TrendingUp, BarChart2, FileX2, X, Settings,
@@ -508,7 +508,7 @@ function HomeView({
const areaGroups = getAreaGroups(orgProcesses) const areaGroups = getAreaGroups(orgProcesses)
return ( return (
<div key={orgId} className="border border-border rounded-xl overflow-hidden"> <div key={orgId} id={`org-section-${orgId}`} className="border border-border rounded-xl overflow-hidden">
{/* Header de org — colapsable */} {/* Header de org — colapsable */}
<button <button
onClick={() => toggleOrg(orgId)} onClick={() => toggleOrg(orgId)}
@@ -731,7 +731,7 @@ function GroupView({
export function LibraryPage() { export function LibraryPage() {
const navigate = useNavigate() const navigate = useNavigate()
const { load, processes } = useLibraryStore() const { load, processes, isLoading } = useLibraryStore()
const { user, isProfileLoaded } = useAuth() const { user, isProfileLoaded } = useAuth()
const [view, setView] = useState<'home' | 'group'>('home') const [view, setView] = useState<'home' | 'group'>('home')
const [activeGroupId, setActiveGroupId] = useState<string | null>(null) const [activeGroupId, setActiveGroupId] = useState<string | null>(null)
@@ -739,11 +739,26 @@ export function LibraryPage() {
const [ownerFilter, setOwnerFilter] = useState<'all' | 'mine'>('all') const [ownerFilter, setOwnerFilter] = useState<'all' | 'mine'>('all')
const { message: toastMessage, show: showToast } = useToast() const { message: toastMessage, show: showToast } = useToast()
const location = useLocation()
const orgParam = useMemo(
() => new URLSearchParams(location.search).get('org') ?? undefined,
[location.search]
)
useEffect(() => { useEffect(() => {
if (!user) return // no fetchear hasta tener usuario confirmado (evita race condition con sesión Supabase) if (!user) return // no fetchear hasta tener usuario confirmado (evita race condition con sesión Supabase)
load() load()
}, [load, user?.id]) }, [load, user?.id])
// Scroll a la sección del org si venimos del workspace
useEffect(() => {
if (!orgParam || isLoading || processes.length === 0) return
requestAnimationFrame(() => {
const el = document.getElementById(`org-section-${orgParam}`)
el?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
}, [orgParam, isLoading, processes.length])
function navigateHome() { function navigateHome() {
setTransitioning(true) setTransitioning(true)
setTimeout(() => { setTimeout(() => {

View File

@@ -192,8 +192,10 @@ export function WorkspacePage() {
: currentProcess.orgName ?? 'Biblioteca' : currentProcess.orgName ?? 'Biblioteca'
function handleBack() { function handleBack() {
// Navega siempre a /library (sin ?org=) hasta que exista una vista filtrada por org void navigate({
void navigate({ to: '/library' }) to: '/library',
search: { org: currentProcess?.orgId ?? undefined },
})
} }
// Label del tab "elemento" según lo seleccionado // Label del tab "elemento" según lo seleccionado

View File

@@ -107,6 +107,9 @@ const libraryRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: '/library', path: '/library',
component: LibraryPage, component: LibraryPage,
validateSearch: (search: Record<string, unknown>) => ({
org: typeof search.org === 'string' ? search.org : undefined,
}),
}) })
const importRoute = createRoute({ const importRoute = createRoute({
@@ -134,7 +137,7 @@ const settingsRoute = createRoute({
beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
const role = (context as { auth?: { user?: { platformRole?: string } } }).auth?.user?.platformRole const role = (context as { auth?: { user?: { platformRole?: string } } }).auth?.user?.platformRole
if (role === 'client_editor' || role === 'client_viewer') { if (role === 'client_editor' || role === 'client_viewer') {
throw redirect({ to: '/library' }) throw redirect({ to: '/library', search: { org: undefined } })
} }
}, },
}) })

View File

@@ -74,6 +74,7 @@ vi.mock('@/components/AppHeader', () => ({ AppHeader: () => null }))
vi.mock('@tanstack/react-router', () => ({ vi.mock('@tanstack/react-router', () => ({
useNavigate: () => vi.fn(), useNavigate: () => vi.fn(),
useRouterState: () => ({ location: { search: '' } }), useRouterState: () => ({ location: { search: '' } }),
useLocation: () => ({ search: '' }),
Link: ({ children }: { children: React.ReactNode }) => <>{children}</>, Link: ({ children }: { children: React.ReactNode }) => <>{children}</>,
})) }))