diff --git a/docs/guidelines/architecture.md b/docs/guidelines/architecture.md
index 79d7c05..3e24e3d 100644
--- a/docs/guidelines/architecture.md
+++ b/docs/guidelines/architecture.md
@@ -156,8 +156,8 @@ index.html → src/main.tsx →
**Router — registro único de rutas:**
-- `routes.tsx`: **fuente única de verdad** de las rutas. Exporta `appRoutes` (array de `{ path, element, nav?, layout?, feature? }`, con las páginas cargadas vía `lazy()`), `navRoutesFor(features)` (las que tienen `nav` **y** cuya feature está habilitada para el tenant activo, ya angostadas para el Sidebar) y la partición `shellRoutes` / `bareRoutes` según `layout`. Sumar una ruta = agregar **una** entrada acá.
-- **Rutas por feature flag (`feature`):** una ruta con `feature` sólo aparece en el Sidebar si la config del tenant la trae encendida, y `FeatureGate` la reemplaza por una pantalla de "no disponible" si se navega directo. Un flag ausente es un flag apagado.
+- `routes.tsx`: **fuente única de verdad** de las rutas. Exporta `appRoutes` (array de `{ path, element, nav?, layout? }`, con las páginas cargadas vía `lazy()`), `navRoutes` (las que tienen `nav`, ya angostadas para el Sidebar) y la partición `shellRoutes` / `bareRoutes` según `layout`. Sumar una ruta = agregar **una** entrada acá. La raíz `/` es el panel de operación: es lo primero que se ve al iniciar sesión.
+- **Feature flags del tenant:** hoy ninguna ruta depende de un flag. La última que lo hacía era Integraciones, que se sacó en TESIS-140 porque las conexiones las administra el equipo y no la empresa; con ella se fueron el campo `feature` de la ruta y el `FeatureGate` que la cubría. El flag sigue vivo y sigue decidiendo contenido **dentro** de una pantalla: `useTenantFeature('integrations')` muestra u oculta la tarjeta de canales de venta del detalle de producto. Un flag ausente es un flag apagado.
- `AppRouter.tsx`: monta `bareRoutes` sueltas y públicas — cada una con su propio `ErrorBoundary` — y `shellRoutes` detrás de `` y dentro de ``. Todo envuelto en `}>`. El catch-all vive dentro del guard, así una URL desconocida sin sesión también termina en el login.
- El Sidebar y el Router se derivan del mismo `routes.tsx`, por lo que no pueden divergir.
- **Rutas sin shell (`layout: 'bare'`):** públicas y full-bleed — hoy `/login`. No pasan por el guard ni renderizan Header/Sidebar. Como el `ErrorBoundary` de `AppLayout` sólo envuelve al `Outlet` privado, cada ruta bare lleva el suyo.
@@ -167,7 +167,7 @@ index.html → src/main.tsx →
- `AppLayout.tsx`: flex `Header + Sidebar + `. El margen izquierdo responde a `sidebarOpen` del `uiStore` (drawer width: 240px). El `` va envuelto en ``: los errores de render (o de carga de un chunk lazy) muestran el `ErrorFallback` en el área de contenido sin tumbar Header ni Sidebar, y se limpian al navegar.
- `Header.tsx`: único punto de cableado del `TopNavBar` (`shared/components`) — lee `themeMode`/`toggleTheme`/`toggleSidebar` de `useUiStore` y el email + `logout` de `useAuthStore`, todo con selectores individuales, y se los pasa por props. El componente en sí es presentacional — sin datos ni `uiStore`; su único acople es el `Link` de react-router (brand y engranaje), correcto para esta app (ver tabla de `shared/` más abajo). El logout no navega: limpia la sesión y el guard hace el redirect.
-- `Sidebar.tsx`: Drawer persistente. Los ítems salen de `navRoutesFor(features)` con los feature flags del tenant activo. Usa `NavLink` con clase `active` que resalta en `primary.main`.
+- `Sidebar.tsx`: Drawer persistente. Los ítems salen de `navRoutes`. Usa `NavLink` con clase `active` que resalta en `primary.main`.
**Tema** (`createAppTheme(branding)`):
@@ -310,9 +310,8 @@ features/[nombre]/
| Feature | Contenido |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `dashboard` | Panel de operación (`/dashboard`) y la sección de Integraciones (`/integrations`, detrás del feature flag `integrations` del tenant). Comparten `useInfraHealth` e `IntegrationNodeList`. El panel suma la tarjeta de alertas de inventario, la carga por depósito y la tabla de órdenes recientes (`useInventoryAlerts`, `WarehouseLoadCard`, `useRecentOrders`, `RecentOrdersTable`) |
+| `dashboard` | Panel de operación y pantalla de entrada de la app (`/`, S03). Salud de los nodos de integración (`useInfraHealth`, `IntegrationNodeList`), alertas de inventario, carga por depósito y órdenes recientes (`useInventoryAlerts`, `WarehouseLoadCard`, `useRecentOrders`, `RecentOrdersTable`) |
| `design-system` | Catálogo de tokens y componentes del DS (`/design-system`). No tiene datos ni hooks |
-| `home` | Landing de la app (`/`) |
| `inventory` | Catálogo de productos y stock por depósito (`/inventory`). Hoy el alta y la edición de producto (`CreateProductModal`, `EditProductModal`) sobre la API real; la vista real del catálogo la construye TESIS-62, que reemplaza el cuerpo de `InventoryPage` |
| `warehouses` | Depósitos de la empresa (`/warehouses`, RF-03): listado, alta, edición y baja sobre `/api/v1/warehouses`. Sin maqueta: tabla, modal y confirmación del DS. Sus mutaciones invalidan todas las queries, porque los depósitos llenan selects de otras features cuyas claves esta no puede importar |
| `failed-events` | Cola de eventos fallidos (`/failed-events`, RF-16): lo que el motor de reintentos todavía intenta, lo agotado y lo resuelto, con reintento y descarte manuales. Las acciones de cada fila salen de `utils/actions.ts`, espejo de los estados que la API acepta |
diff --git a/src/app/layout/Sidebar.tsx b/src/app/layout/Sidebar.tsx
index 2a66911..7e316b7 100644
--- a/src/app/layout/Sidebar.tsx
+++ b/src/app/layout/Sidebar.tsx
@@ -8,19 +8,14 @@ import {
ListItemText,
Toolbar,
} from '@mui/material'
-import { navRoutesFor } from 'app/router/routes'
-import { useMemo } from 'react'
+import { navRoutes } from 'app/router/routes'
import { NavLink } from 'react-router-dom'
-import { useTenantStore, useUiStore } from 'shared/store'
+import { useUiStore } from 'shared/store'
const DRAWER_WIDTH = 240
export function Sidebar() {
const { sidebarOpen } = useUiStore()
- // La navegación sale de la config del tenant: una feature que la empresa no
- // tiene no se lista (TESIS-121).
- const features = useTenantStore((state) => state.config?.features)
- const navRoutes = useMemo(() => navRoutesFor(features), [features])
return (
}>
}>
{shellRoutes.map((route) => (
- {route.element}
- ) : (
- route.element
- )
- }
- />
+
))}
} />
diff --git a/src/app/router/FeatureGate.tsx b/src/app/router/FeatureGate.tsx
deleted file mode 100644
index 35f2066..0000000
--- a/src/app/router/FeatureGate.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Stack, Typography } from '@mui/material'
-import type { ReactNode } from 'react'
-import type { TenantFeature } from 'shared/api'
-import { PageWrapper } from 'shared/components'
-import { useTenantFeature } from 'shared/store'
-
-const featureGateContent = {
- title: 'Esta sección no está disponible',
- body: 'Tu empresa no tiene habilitada esta funcionalidad. Si creés que debería estarlo, hablá con quien administra la cuenta.',
-} as const
-
-/**
- * Guard de las rutas que dependen de un feature flag del tenant.
- *
- * El Sidebar ya no lista la ruta cuando la feature está apagada, pero la URL
- * sigue existiendo (un favorito, un enlace pegado). Sin esto la página se
- * montaría y pediría datos que el backend no le va a dar: mejor una pantalla que
- * dice qué pasa que un error de la API o una vista vacía sin explicación.
- */
-export function FeatureGate({
- feature,
- children,
-}: {
- feature: TenantFeature
- children: ReactNode
-}) {
- const enabled = useTenantFeature(feature)
-
- if (enabled) return children
-
- return (
-
-
- {featureGateContent.title}
-
- {featureGateContent.body}
-
-
-
- )
-}
diff --git a/src/app/router/routes.test.tsx b/src/app/router/routes.test.tsx
index 661b9f2..93c7b2b 100644
--- a/src/app/router/routes.test.tsx
+++ b/src/app/router/routes.test.tsx
@@ -1,32 +1,19 @@
-import type { TenantFeatureFlags } from 'shared/api'
import { describe, expect, it } from 'vitest'
-import { navRoutesFor } from './routes'
+import { navRoutes } from './routes'
-function navPaths(features: TenantFeatureFlags | undefined) {
- return navRoutesFor(features).map((route) => route.path)
-}
+const paths = navRoutes.map((route) => route.path)
-describe('navRoutesFor', () => {
- it('lists the sections of a feature the tenant has enabled', () => {
- expect(navPaths({ integrations: true })).toContain('/integrations')
+describe('navRoutes', () => {
+ // El panel es la pantalla de entrada desde TESIS-140: antes `/` era una
+ // pantalla de Inicio sin datos y el panel vivía en `/dashboard`.
+ it('opens the sidebar with the dashboard, which is the root of the app', () => {
+ expect(navRoutes[0]).toMatchObject({ path: '/', nav: { label: 'Dashboard' } })
})
- it('hides them for a tenant that has the feature off', () => {
- expect(navPaths({ integrations: false })).not.toContain('/integrations')
- })
-
- // Un flag ausente es un flag apagado: la empresa que no compró la feature no
- // la tiene declarada, y el default no puede ser mostrarla.
- it('hides them when the config does not mention the feature at all', () => {
- expect(navPaths({})).not.toContain('/integrations')
- expect(navPaths(undefined)).not.toContain('/integrations')
- })
-
- it('keeps the sections that belong to the product for every tenant', () => {
- expect(navPaths({})).toEqual([
+ it('lists the sections of the product, and only those', () => {
+ expect(paths).toEqual([
'/',
- '/dashboard',
'/orders',
'/inventory',
'/warehouses',
@@ -35,10 +22,15 @@ describe('navRoutesFor', () => {
])
})
- it('never lists a route that has no place in the sidebar', () => {
- const paths = navPaths({ integrations: true })
+ // Integraciones se sacó en TESIS-140: las conexiones las administra el
+ // equipo desde el backoffice, no la empresa desde la app.
+ it('does not offer a section for the integrations of the company', () => {
+ expect(paths).not.toContain('/integrations')
+ })
+ it('never lists a route that has no place in the sidebar', () => {
expect(paths).not.toContain('/login')
expect(paths).not.toContain('/design-system')
+ expect(paths).not.toContain('/orders/new')
})
})
diff --git a/src/app/router/routes.tsx b/src/app/router/routes.tsx
index 061841f..6223c5e 100644
--- a/src/app/router/routes.tsx
+++ b/src/app/router/routes.tsx
@@ -1,24 +1,16 @@
import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined'
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
-import HomeIcon from '@mui/icons-material/Home'
-import HubOutlinedIcon from '@mui/icons-material/HubOutlined'
import InsightsOutlinedIcon from '@mui/icons-material/InsightsOutlined'
import Inventory2Icon from '@mui/icons-material/Inventory2'
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'
import { lazy } from 'react'
import type { ReactNode } from 'react'
-import { isFeatureEnabled } from 'shared/api'
-import type { TenantFeature, TenantFeatureFlags } from 'shared/api'
// Páginas cargadas de forma diferida (code-splitting por ruta).
-const HomePage = lazy(() => import('features/home').then((m) => ({ default: m.HomePage })))
const DashboardPage = lazy(() =>
import('features/dashboard').then((m) => ({ default: m.DashboardPage })),
)
-const IntegrationsPage = lazy(() =>
- import('features/integrations').then((m) => ({ default: m.IntegrationsPage })),
-)
const DesignSystemPage = lazy(() =>
import('features/design-system').then((m) => ({ default: m.DesignSystemPage })),
)
@@ -70,12 +62,6 @@ export interface AppRoute {
* Ver docs/guidelines/architecture.md §4.1.
*/
layout?: 'app' | 'bare'
- /**
- * Feature flag del tenant que habilita la ruta. Sin flag la ruta es del
- * producto y la ve todo el mundo; con flag, sólo las empresas que la tienen
- * encendida en su config (TESIS-121).
- */
- feature?: TenantFeature
}
/** Ruta ya angostada para el Sidebar: `nav` garantizado. */
@@ -92,12 +78,11 @@ export const appRoutes: AppRoute[] = [
layout: 'bare',
},
{
+ // El panel es la pantalla de entrada: es lo primero que se ve al iniciar
+ // sesión (LoginPage navega a `from ?? '/'`). Antes acá vivía una pantalla
+ // de Inicio que no mostraba datos y anunciaba el panel como algo que
+ // llegaba más adelante (TESIS-111); el panel existe desde TESIS-53.
path: '/',
- element: ,
- nav: { label: 'Inicio', icon: },
- },
- {
- path: '/dashboard',
element: ,
nav: { label: 'Dashboard', icon: },
},
@@ -132,14 +117,6 @@ export const appRoutes: AppRoute[] = [
element: ,
// Sin `nav`: se llega desde el listado, no desde el Sidebar.
},
- {
- path: '/integrations',
- element: ,
- nav: { label: 'Integraciones', icon: },
- // La feature que diferencia a las dos empresas de la demo: Norte la tiene
- // encendida y Sur no (§2 del contrato).
- feature: 'integrations',
- },
{
path: '/inventory',
element: ,
@@ -173,18 +150,22 @@ export const appRoutes: AppRoute[] = [
]
/**
- * Rutas que muestra el Sidebar para la config de tenant activa.
+ * Rutas que lista el Sidebar.
+ *
+ * Era `navRoutesFor(features)`, porque Integraciones se mostraba sólo a los
+ * tenants con ese flag encendido. Esa sección se sacó en TESIS-140 —la
+ * administra el equipo, no la empresa— y con ella la última ruta con flag, así
+ * que hoy la navegación es la misma para todos y una función que recibiera la
+ * config y no la mirara diría algo falso.
*
- * Es una función y no una constante porque la navegación depende de los feature
- * flags de la empresa: la diferencia entre dos clientes es su config, nunca una
- * rama del código.
+ * El flag del tenant no desaparece: sigue decidiendo si el detalle de producto
+ * muestra la tarjeta de canales de venta (`useTenantFeature('integrations')`).
+ * La diferencia entre dos clientes sigue estando en su config y no en una rama
+ * del código; lo que cambió es dónde se nota.
*/
-export function navRoutesFor(features: TenantFeatureFlags | undefined): NavRoute[] {
- return appRoutes.filter(
- (route): route is NavRoute =>
- Boolean(route.nav) && (!route.feature || isFeatureEnabled(features, route.feature)),
- )
-}
+export const navRoutes: NavRoute[] = appRoutes.filter((route): route is NavRoute =>
+ Boolean(route.nav),
+)
// Partición por layout — AppRouter monta `shellRoutes` detrás del guard y dentro
// de `AppLayout`, y `bareRoutes` sueltas, públicas y sin shell.
diff --git a/src/features/dashboard/pages/DashboardPage.test.tsx b/src/features/dashboard/pages/DashboardPage.test.tsx
index 3db38c2..8c6cb10 100644
--- a/src/features/dashboard/pages/DashboardPage.test.tsx
+++ b/src/features/dashboard/pages/DashboardPage.test.tsx
@@ -71,9 +71,9 @@ const alertCard = () => screen.getByRole('link', { name: /Alertas de inventario/
function renderPage() {
return renderWithTheme(
-
+
- } />
+ } />
Inventario} />
,
diff --git a/src/features/home/content.ts b/src/features/home/content.ts
deleted file mode 100644
index 5e65b02..0000000
--- a/src/features/home/content.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copy centralizado de la feature — sin literales sueltos en el JSX.
-// Mismo criterio que `features/inventory/content.ts`: si más adelante entra
-// i18n, este módulo es el único punto a migrar a claves de traducción.
-
-export const homeCopy = {
- page: {
- title: 'OneStock',
- subtitle: 'Gestión de órdenes, inventario y envíos.',
- /** El email lo confirma la sesión; no se pinta si todavía no hay uno. */
- session: (email: string) => `Sesión iniciada como ${email}`,
- },
- sections: {
- shortcuts: 'Dónde ir',
- },
- shortcuts: {
- inventory: {
- label: 'Inventario',
- description: 'Catálogo de productos y stock consolidado por depósito.',
- },
- },
- // El panel de operación con métricas es TESIS-53 a TESIS-56 y depende de
- // endpoints que la API todavía no expone. Decirlo es más honesto que pintar
- // tarjetas con números inventados.
- pending: 'El panel de operación, con las métricas de la empresa, llega más adelante.',
- designSystem: 'Design System',
-} as const
diff --git a/src/features/home/index.ts b/src/features/home/index.ts
deleted file mode 100644
index c136dc5..0000000
--- a/src/features/home/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { HomePage } from './pages/HomePage'
diff --git a/src/features/home/pages/HomePage.tsx b/src/features/home/pages/HomePage.tsx
deleted file mode 100644
index 0783693..0000000
--- a/src/features/home/pages/HomePage.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import Inventory2Icon from '@mui/icons-material/Inventory2'
-import NorthEastIcon from '@mui/icons-material/NorthEast'
-import { Box, Card, CardActionArea, Link, Stack, Typography } from '@mui/material'
-import { Link as RouterLink } from 'react-router-dom'
-import { PageWrapper } from 'shared/components'
-import { useAuthStore } from 'shared/store'
-
-import { homeCopy } from '../content'
-
-const { page, sections, shortcuts, pending, designSystem } = homeCopy
-
-/**
- * Inicio de la sesión.
- *
- * **No es el panel de operación.** Ese es TESIS-53 a TESIS-56 y hoy está
- * bloqueado: no hay endpoints que alimenten KPIs, salud de nodos, capacidad ni
- * actividad reciente. Hasta que existan, esta pantalla muestra sólo lo que la
- * app puede afirmar —quién tiene la sesión abierta y a qué se puede entrar— en
- * vez de tarjetas con números de mentira.
- */
-export function HomePage() {
- const email = useAuthStore((state) => state.user?.email)
-
- return (
-
-
- {page.title}
-
-
- {page.subtitle}
-
- {email !== undefined && (
-
- {page.session(email)}
-
- )}
-
-
- {sections.shortcuts}
-
-
-
-
-
-
-
-
- {shortcuts.inventory.label}
-
-
- {shortcuts.inventory.description}
-
-
-
-
-
-
-
- {pending}
-
-
- {/* Herramienta del equipo, no una función del producto: queda accesible
- pero fuera del cuerpo de la pantalla. */}
-
-
- {designSystem}
-
-
-
-
- )
-}
diff --git a/src/features/integrations/components/IntegrationCard/IntegrationCard.styles.ts b/src/features/integrations/components/IntegrationCard/IntegrationCard.styles.ts
deleted file mode 100644
index 86f7fc5..0000000
--- a/src/features/integrations/components/IntegrationCard/IntegrationCard.styles.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Box, Card } from '@mui/material'
-import { styled } from '@mui/material/styles'
-
-// El Card del tema ya trae borde, sombra y radio: acá sólo el layout interno.
-export const CardRoot = styled(Card)(({ theme }) => ({
- padding: theme.spacing(2.5),
- display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(1),
- height: '100%',
- // Un nombre de tienda largo no desborda la tarjeta.
- '& > *': { minWidth: 0, overflowWrap: 'anywhere' },
-}))
-
-export const CardHeader = styled(Box)(({ theme }) => ({
- display: 'flex',
- alignItems: 'flex-start',
- justifyContent: 'space-between',
- gap: theme.spacing(1),
-}))
diff --git a/src/features/integrations/components/IntegrationCard/IntegrationCard.test.tsx b/src/features/integrations/components/IntegrationCard/IntegrationCard.test.tsx
deleted file mode 100644
index ac04562..0000000
--- a/src/features/integrations/components/IntegrationCard/IntegrationCard.test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { screen } from '@testing-library/react'
-import type { IntegrationService } from 'shared/api'
-import { describe, expect, it } from 'vitest'
-
-import { renderWithTheme } from '../../../../test/renderWithTheme'
-
-import { IntegrationCard } from './IntegrationCard'
-
-function service(overrides: Partial = {}): IntegrationService {
- return {
- serviceId: 7,
- name: 'Shopify',
- type: 'ecommerce',
- configured: true,
- isActive: true,
- integrationId: 3,
- accountName: 'Tienda Norte',
- lastSyncedAt: null,
- ...overrides,
- }
-}
-
-describe('IntegrationCard', () => {
- it('shows the status and the connected account', () => {
- renderWithTheme()
-
- expect(screen.getByText('Conectado')).toBeInTheDocument()
- expect(screen.getByText('Cuenta: Tienda Norte')).toBeInTheDocument()
- })
-
- it('tells a configured but paused integration apart from a connected one', () => {
- renderWithTheme()
-
- expect(screen.getByText('Inactivo')).toBeInTheDocument()
- })
-
- it('shows no account when the company is not connected', () => {
- renderWithTheme()
-
- expect(screen.getByText('No conectado')).toBeInTheDocument()
- expect(screen.queryByText(/Cuenta:/)).not.toBeInTheDocument()
- })
-
- // Las conexiones las carga el equipo de OneStock desde el backoffice (ADR-018).
- it('offers no action to connect or change the integration', () => {
- renderWithTheme()
-
- expect(screen.queryByRole('button')).not.toBeInTheDocument()
- })
-})
diff --git a/src/features/integrations/components/IntegrationCard/IntegrationCard.tsx b/src/features/integrations/components/IntegrationCard/IntegrationCard.tsx
deleted file mode 100644
index ddeb9b0..0000000
--- a/src/features/integrations/components/IntegrationCard/IntegrationCard.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Typography } from '@mui/material'
-import type { IntegrationService } from 'shared/api'
-import { StatusBadge } from 'shared/components'
-import type { StatusVariant } from 'shared/components'
-
-import { integrationsCopy } from '../../content'
-
-import { CardHeader, CardRoot } from './IntegrationCard.styles'
-import type { IntegrationCardProps } from './IntegrationCard.types'
-
-const { status: statusCopy, account } = integrationsCopy
-
-function resolveStatus(service: IntegrationService): { label: string; variant: StatusVariant } {
- if (!service.configured) return { label: statusCopy.notConnected, variant: 'neutral' }
- if (!service.isActive) return { label: statusCopy.inactive, variant: 'warning' }
- return { label: statusCopy.connected, variant: 'success' }
-}
-
-/**
- * Un proveedor en la pantalla de integraciones: su estado para la empresa y la
- * cuenta a la que está conectada. Sólo lectura: las conexiones las carga el
- * equipo de OneStock desde el backoffice.
- */
-export function IntegrationCard({ service }: IntegrationCardProps) {
- const status = resolveStatus(service)
- const accountName = service.configured ? service.accountName : null
-
- return (
-
-
-
- {service.name}
-
-
-
-
- {accountName ? (
-
- {account(accountName)}
-
- ) : null}
-
- )
-}
diff --git a/src/features/integrations/components/IntegrationCard/IntegrationCard.types.ts b/src/features/integrations/components/IntegrationCard/IntegrationCard.types.ts
deleted file mode 100644
index f92d1f8..0000000
--- a/src/features/integrations/components/IntegrationCard/IntegrationCard.types.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { IntegrationService } from 'shared/api'
-
-export interface IntegrationCardProps {
- service: IntegrationService
-}
diff --git a/src/features/integrations/components/IntegrationCard/index.ts b/src/features/integrations/components/IntegrationCard/index.ts
deleted file mode 100644
index 0d63018..0000000
--- a/src/features/integrations/components/IntegrationCard/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { IntegrationCard } from './IntegrationCard'
-export type { IntegrationCardProps } from './IntegrationCard.types'
diff --git a/src/features/integrations/content.ts b/src/features/integrations/content.ts
deleted file mode 100644
index d645fc6..0000000
--- a/src/features/integrations/content.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copy centralizado de la feature — sin literales sueltos en el JSX (mismo
-// criterio que `features/inventory/content.ts`).
-
-export const integrationsCopy = {
- page: {
- title: 'Integraciones',
- subtitle:
- 'Los canales de venta y operadores logísticos conectados a tu empresa. Para conectar uno nuevo o cambiar una cuenta, contactá al equipo de OneStock.',
- error: 'No pudimos cargar las integraciones.',
- retry: 'Reintentar',
- empty: 'No hay proveedores disponibles.',
- },
- groups: {
- ecommerce: 'Canales de venta',
- courier: 'Operadores logísticos',
- },
- status: {
- connected: 'Conectado',
- inactive: 'Inactivo',
- notConnected: 'No conectado',
- },
- account: (name: string) => `Cuenta: ${name}`,
-} as const
diff --git a/src/features/integrations/index.ts b/src/features/integrations/index.ts
deleted file mode 100644
index 9909eba..0000000
--- a/src/features/integrations/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { IntegrationsPage } from './pages/IntegrationsPage'
diff --git a/src/features/integrations/pages/IntegrationsPage.tsx b/src/features/integrations/pages/IntegrationsPage.tsx
deleted file mode 100644
index 4557d94..0000000
--- a/src/features/integrations/pages/IntegrationsPage.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { Alert, Box, Button, Grid, Stack, Typography } from '@mui/material'
-import type { ServiceType } from 'shared/api'
-import { LoadingSpinner, PageWrapper } from 'shared/components'
-import { useIntegrations } from 'shared/hooks/useIntegrations'
-
-import { IntegrationCard } from '../components/IntegrationCard'
-import { integrationsCopy } from '../content'
-
-const { page, groups } = integrationsCopy
-
-const GROUP_ORDER: ServiceType[] = ['ecommerce', 'courier']
-
-/**
- * Integraciones: qué proveedores tiene conectados la empresa y en qué estado
- * está cada uno (TESIS-139).
- *
- * Es de sólo lectura. Las credenciales las carga el equipo de OneStock desde el
- * backoffice (ADR-018 del backend): la empresa no maneja client secrets ni
- * dominios técnicos que usa una sola vez.
- *
- * La ruta está detrás del feature flag `integrations` del tenant (TESIS-121).
- */
-export function IntegrationsPage() {
- const { data, isLoading, isError, refetch } = useIntegrations()
- const services = data ?? []
-
- return (
-
-
-
- {page.title}
-
- {page.subtitle}
-
-
-
- {isError ? (
- void refetch()}>
- {page.retry}
-
- }
- >
- {page.error}
-
- ) : null}
-
- {isLoading ? : null}
-
- {!isLoading && !isError && services.length === 0 ? (
-
- {page.empty}
-
- ) : null}
-
- {GROUP_ORDER.map((type) => {
- const group = services.filter((service) => service.type === type)
- if (group.length === 0) return null
-
- return (
-
- {groups[type]}
-
- {group.map((service) => (
-
-
-
- ))}
-
-
- )
- })}
-
-
- )
-}
diff --git a/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.tsx b/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.tsx
index bfc027a..09044fc 100644
--- a/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.tsx
+++ b/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.tsx
@@ -1,7 +1,6 @@
import { Alert, Button, MenuItem, TextField, Typography } from '@mui/material'
import { useState } from 'react'
import type { FormEvent } from 'react'
-import { Link } from 'react-router-dom'
import { ConfirmDialog, LabeledField, LoadingSpinner } from 'shared/components'
import { useIntegrations } from 'shared/hooks/useIntegrations'
import { notify } from 'shared/store'
@@ -34,7 +33,7 @@ const { channels: copy } = inventoryCopy
* vínculos tienen su endpoint y sus mutaciones, y así la página no carga con
* ellos cuando la empresa no tiene la feature `integrations`.
*/
-export function SalesChannelsCard({ productId, integrationsPath }: SalesChannelsCardProps) {
+export function SalesChannelsCard({ productId }: SalesChannelsCardProps) {
const mappings = useProductMappings(productId)
const integrations = useIntegrations()
const link = useLinkProduct(productId)
@@ -147,12 +146,12 @@ export function SalesChannelsCard({ productId, integrationsPath }: SalesChannels
) : null}
+ {/* Sin canales conectados no hay a dónde mandar al usuario: la pantalla
+ de Integraciones se sacó en TESIS-140 porque las conexiones las
+ administra el equipo. El aviso dice a quién pedírselo. */}
{!integrations.isLoading && channels.length === 0 && rows.length === 0 ? (
- {copy.noChannels}{' '}
-
+ {copy.noChannels}
) : null}
diff --git a/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.types.ts b/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.types.ts
index 5770a80..1142833 100644
--- a/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.types.ts
+++ b/src/features/inventory/components/SalesChannelsCard/SalesChannelsCard.types.ts
@@ -1,5 +1,3 @@
export interface SalesChannelsCardProps {
productId: number
- /** Ruta de la pantalla de integraciones, para el aviso de «no hay canales». */
- integrationsPath: string
}
diff --git a/src/features/inventory/content.ts b/src/features/inventory/content.ts
index 7b8f25e..9a65745 100644
--- a/src/features/inventory/content.ts
+++ b/src/features/inventory/content.ts
@@ -202,8 +202,8 @@ export const inventoryCopy = {
subtitle: 'Publicaciones vinculadas: el stock de OneStock se publica en cada una.',
externalId: (id: string) => `ID de la publicación: ${id}`,
empty: 'Este producto todavía no está vinculado a ningún canal.',
- noChannels: 'No hay canales conectados donde vincularlo.',
- goToIntegrations: 'Ir a Integraciones',
+ noChannels:
+ 'No hay canales conectados donde vincularlo. Las conexiones las configura el equipo de OneStock: escribinos para sumar uno.',
channel: 'Canal',
externalIdLabel: 'ID de la publicación',
externalIdHelper: 'Dejalo vacío para buscarla por el SKU del producto.',
diff --git a/src/features/inventory/pages/ProductDetailPage.tsx b/src/features/inventory/pages/ProductDetailPage.tsx
index 67b06b6..9543d1b 100644
--- a/src/features/inventory/pages/ProductDetailPage.tsx
+++ b/src/features/inventory/pages/ProductDetailPage.tsx
@@ -40,7 +40,6 @@ const { specs: specsCopy, master: masterCopy, distribution: distributionCopy } =
const CATALOG_PATH = '/inventory'
// Misma razón: la pantalla de integraciones, a la que manda la tarjeta de
// canales cuando no hay ninguno conectado.
-const INTEGRATIONS_PATH = '/integrations'
// La grilla del diseño: columna fija para el stock maestro y el resto para la
// distribución. En pantallas angostas se apilan.
@@ -293,9 +292,7 @@ export function ProductDetailPage() {
{/* Canales de venta: sólo para las empresas con la feature encendida,
igual que la ruta de integraciones (TESIS-121). */}
- {integrationsEnabled ? (
-
- ) : null}
+ {integrationsEnabled ? : null}
{/* El 412 no es un error a mostrar acá: lo explica el propio modal, que
queda abierto con lo que el usuario cargó. La condición mira