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