# ET-FRONTEND-MGN-002-003: Asignación de Usuarios a Empresas (Multi-Empresa)

**RF Asociado:** [RF-MGN-002-003](../../requerimientos-funcionales/mgn-002/RF-MGN-002-003-asignación-de-usuarios-a-empresas-multi-empresa.md)
**ET Backend:** [ET-BACKEND-MGN-002-003](../backend/mgn-002/ET-BACKEND-MGN-002-003-asignación-de-usuarios-a-empresas-multi-empresa.md)
**Módulo:** MGN-002
**Complejidad:** Media
**Story Points:** 4 SP (Frontend)
**Estado:** Diseñado
**Fecha:** 2025-11-24

## Resumen Técnico

Implementación frontend para asignación de usuarios a empresas (multi-empresa). Incluye componentes React con TypeScript, formularios con validación, integración con API backend, y arquitectura Feature-Sliced Design (FSD).

## Stack Tecnológico

- **Framework:** React 18.x + TypeScript 5.x
- **Build Tool:** Vite 5.x
- **UI Library:** Ant Design 5.x (AntD)
- **State Management:** Zustand + React Query (TanStack Query)
- **Routing:** React Router 6.x
- **Forms:** React Hook Form + Zod validation
- **HTTP Client:** Axios (con interceptors para auth)
- **Testing:** Vitest + React Testing Library + Playwright

## Arquitectura Frontend (FSD)

Estructura basada en Feature-Sliced Design:

```
src/
├── app/                          # App-level config
│   ├── providers/
│   │   └── router.tsx
│   └── styles/
│       └── theme.ts
├── pages/                        # Route pages
│   └── AsignaciónUsuariosEmpresasmulti-empresaPage/
│       ├── index.tsx
│       └── AsignaciónUsuariosEmpresasmulti-empresaPage.tsx
├── widgets/                      # Complex UI blocks
│   └── AsignaciónUsuariosEmpresasmulti-empresaTable/
│       ├── ui/
│       │   └── AsignaciónUsuariosEmpresasmulti-empresaTable.tsx
│       └── index.ts
├── features/                     # User interactions
│   ├── createAsignaciónUsuariosEmpresasmulti-empresa/
│   │   ├── ui/
│   │   │   └── CreateAsignaciónUsuariosEmpresasmulti-empresaForm.tsx
│   │   ├── model/
│   │   │   └── useAsignaciónUsuariosEmpresasmulti-empresaActions.ts
│   │   └── index.ts
│   ├── updateAsignaciónUsuariosEmpresasmulti-empresa/
│   └── deleteAsignaciónUsuariosEmpresasmulti-empresa/
├── entities/                     # Business entities
│   └── asignaciónUsuariosEmpresasmulti-empresa/
│       ├── model/
│       │   ├── types.ts
│       │   ├── schemas.ts
│       │   └── asignaciónUsuariosEmpresasmulti-empresa.store.ts
│       ├── api/
│       │   ├── asignaciónUsuariosEmpresasmulti-empresa.api.ts
│       │   └── asignaciónUsuariosEmpresasmulti-empresa.queries.ts
│       ├── ui/
│       │   └── AsignaciónUsuariosEmpresasmulti-empresaCard.tsx
│       └── index.ts
└── shared/                       # Shared code
    ├── ui/                       # UI kit
    │   ├── Button/
    │   ├── Modal/
    │   └── Table/
    ├── api/
    │   └── client.ts
    └── lib/
        └── utils.ts
```

## Rutas

```typescript
// src/app/routes/mgn-002.routes.tsx
export const AsignaciónUsuariosEmpresasmulti-empresaRoutes = {
  list: '/mgn-002/asignaciónUsuariosEmpresasmulti-empresa',
  create: '/mgn-002/asignaciónUsuariosEmpresasmulti-empresa/create',
  edit: '/mgn-002/asignaciónUsuariosEmpresasmulti-empresa/:id/edit',
  view: '/mgn-002/asignaciónUsuariosEmpresasmulti-empresa/:id',
};

// Integración en Router
<Route path="/mgn-002">
  <Route path="asignaciónUsuariosEmpresasmulti-empresa" element={<AsignaciónUsuariosEmpresasmulti-empresaPage />} />
  <Route path="asignaciónUsuariosEmpresasmulti-empresa/create" element={<CreateAsignaciónUsuariosEmpresasmulti-empresaPage />} />
  <Route path="asignaciónUsuariosEmpresasmulti-empresa/:id/edit" element={<EditAsignaciónUsuariosEmpresasmulti-empresaPage />} />
  <Route path="asignaciónUsuariosEmpresasmulti-empresa/:id" element={<ViewAsignaciónUsuariosEmpresasmulti-empresaPage />} />
</Route>
```

## Types / Interfaces

```typescript
// src/entities/asignaciónUsuariosEmpresasmulti-empresa/model/types.ts

export interface AsignaciónUsuariosEmpresasmulti-empresa {
  id: string;
  tenantId: string;
  name: string;
  code?: string;
  createdAt: string;
  updatedAt?: string;
  deletedAt?: string;
}

export interface CreateAsignaciónUsuariosEmpresasmulti-empresaDto {
  name: string;
  code?: string;
}

export type UpdateAsignaciónUsuariosEmpresasmulti-empresaDto = Partial<CreateAsignaciónUsuariosEmpresasmulti-empresaDto>;

export interface AsignaciónUsuariosEmpresasmulti-empresaFilters {
  search?: string;
  page?: number;
  limit?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

export interface AsignaciónUsuariosEmpresasmulti-empresaListResponse {
  data: AsignaciónUsuariosEmpresasmulti-empresa[];
  meta: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}
```

## Schemas de Validación (Zod)

```typescript
// src/entities/asignaciónUsuariosEmpresasmulti-empresa/model/schemas.ts
import { z } from 'zod';

export const createAsignaciónUsuariosEmpresasmulti-empresaSchema = z.object({
  name: z.string()
    .min(3, 'El nombre debe tener al menos 3 caracteres')
    .max(255, 'El nombre no puede exceder 255 caracteres'),
  code: z.string()
    .min(2, 'El código debe tener al menos 2 caracteres')
    .max(50, 'El código no puede exceder 50 caracteres')
    .optional(),
});

export const updateAsignaciónUsuariosEmpresasmulti-empresaSchema = createAsignaciónUsuariosEmpresasmulti-empresaSchema.partial();

export type CreateAsignaciónUsuariosEmpresasmulti-empresaFormData = z.infer<typeof createAsignaciónUsuariosEmpresasmulti-empresaSchema>;
export type UpdateAsignaciónUsuariosEmpresasmulti-empresaFormData = z.infer<typeof updateAsignaciónUsuariosEmpresasmulti-empresaSchema>;

// Validación personalizada (ejemplo)
export const validateAsignaciónUsuariosEmpresasmulti-empresaCode = (code: string): boolean => {
  return /^[A-Z0-9-]+$/.test(code);
};
```

## API Client

```typescript
// src/entities/asignaciónUsuariosEmpresasmulti-empresa/api/asignaciónUsuariosEmpresasmulti-empresa.api.ts
import { apiClient } from '@shared/api/client';
import type {
  AsignaciónUsuariosEmpresasmulti-empresa,
  CreateAsignaciónUsuariosEmpresasmulti-empresaDto,
  UpdateAsignaciónUsuariosEmpresasmulti-empresaDto,
  AsignaciónUsuariosEmpresasmulti-empresaFilters,
  AsignaciónUsuariosEmpresasmulti-empresaListResponse
} from '../model/types';

const BASE_URL = '/api/v1/companies/:id/users';

export const asignaciónUsuariosEmpresasmulti-empresaApi = {
  getAll: async (filters?: AsignaciónUsuariosEmpresasmulti-empresaFilters): Promise<AsignaciónUsuariosEmpresasmulti-empresaListResponse> => {
    const { data } = await apiClient.get<AsignaciónUsuariosEmpresasmulti-empresaListResponse>(BASE_URL, {
      params: filters,
    });
    return data;
  },

  getById: async (id: string): Promise<AsignaciónUsuariosEmpresasmulti-empresa> => {
    const { data } = await apiClient.get<{ data: AsignaciónUsuariosEmpresasmulti-empresa }>(`${BASE_URL}/${id}`);
    return data.data;
  },

  create: async (dto: CreateAsignaciónUsuariosEmpresasmulti-empresaDto): Promise<AsignaciónUsuariosEmpresasmulti-empresa> => {
    const { data } = await apiClient.post<{ data: AsignaciónUsuariosEmpresasmulti-empresa }>(BASE_URL, dto);
    return data.data;
  },

  update: async (id: string, dto: UpdateAsignaciónUsuariosEmpresasmulti-empresaDto): Promise<AsignaciónUsuariosEmpresasmulti-empresa> => {
    const { data } = await apiClient.put<{ data: AsignaciónUsuariosEmpresasmulti-empresa }>(`${BASE_URL}/${id}`, dto);
    return data.data;
  },

  delete: async (id: string): Promise<void> => {
    await apiClient.delete(`${BASE_URL}/${id}`);
  },
};

// Configuración de Axios client
// src/shared/api/client.ts
import axios from 'axios';
import { message } from 'antd';

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
  timeout: 30000,
});

// Request interceptor: agregar auth token
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor: manejar errores globales
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    const { response } = error;

    if (response?.status === 401) {
      // Token expirado: redirigir a login
      localStorage.removeItem('accessToken');
      window.location.href = '/login';
    } else if (response?.status === 403) {
      message.error('No tienes permisos para realizar esta acción');
    } else if (response?.status === 500) {
      message.error('Error interno del servidor');
    }

    return Promise.reject(error);
  }
);
```

## State Management (Zustand + React Query)

### Zustand Store (estado local UI)
```typescript
// src/entities/asignaciónUsuariosEmpresasmulti-empresa/model/asignaciónUsuariosEmpresasmulti-empresa.store.ts
import { create } from 'zustand';
import { AsignaciónUsuariosEmpresasmulti-empresa } from './types';

interface AsignaciónUsuariosEmpresasmulti-empresaStore {
  selectedAsignaciónUsuariosEmpresasmulti-empresa: AsignaciónUsuariosEmpresasmulti-empresa | null;
  isModalOpen: boolean;
  modalMode: 'create' | 'edit' | 'view' | null;

  setSelectedAsignaciónUsuariosEmpresasmulti-empresa: (entity: AsignaciónUsuariosEmpresasmulti-empresa | null) => void;
  openModal: (mode: 'create' | 'edit' | 'view', entity?: AsignaciónUsuariosEmpresasmulti-empresa) => void;
  closeModal: () => void;
}

export const useAsignaciónUsuariosEmpresasmulti-empresaStore = create<AsignaciónUsuariosEmpresasmulti-empresaStore>((set) => ({
  selectedAsignaciónUsuariosEmpresasmulti-empresa: null,
  isModalOpen: false,
  modalMode: null,

  setSelectedAsignaciónUsuariosEmpresasmulti-empresa: (entity) => set({ selectedAsignaciónUsuariosEmpresasmulti-empresa: entity }),

  openModal: (mode, entity) => set({
    isModalOpen: true,
    modalMode: mode,
    selectedAsignaciónUsuariosEmpresasmulti-empresa: entity || null,
  }),

  closeModal: () => set({
    isModalOpen: false,
    modalMode: null,
    selectedAsignaciónUsuariosEmpresasmulti-empresa: null,
  }),
}));
```

### React Query Hooks (servidor state)
```typescript
// src/entities/asignaciónUsuariosEmpresasmulti-empresa/api/asignaciónUsuariosEmpresasmulti-empresa.queries.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { asignaciónUsuariosEmpresasmulti-empresaApi } from './asignaciónUsuariosEmpresasmulti-empresa.api';
import type { CreateAsignaciónUsuariosEmpresasmulti-empresaDto, UpdateAsignaciónUsuariosEmpresasmulti-empresaDto, AsignaciónUsuariosEmpresasmulti-empresaFilters } from '../model/types';

const QUERY_KEY = 'asignaciónUsuariosEmpresasmulti-empresa';

// Query: obtener lista
export const useAsignaciónUsuariosEmpresasmulti-empresas = (filters?: AsignaciónUsuariosEmpresasmulti-empresaFilters) => {
  return useQuery({
    queryKey: [QUERY_KEY, filters],
    queryFn: () => asignaciónUsuariosEmpresasmulti-empresaApi.getAll(filters),
    staleTime: 5 * 60 * 1000, // 5 minutos
  });
};

// Query: obtener por ID
export const useAsignaciónUsuariosEmpresasmulti-empresa = (id: string) => {
  return useQuery({
    queryKey: [QUERY_KEY, id],
    queryFn: () => asignaciónUsuariosEmpresasmulti-empresaApi.getById(id),
    enabled: !!id,
  });
};

// Mutation: crear
export const useCreateAsignaciónUsuariosEmpresasmulti-empresa = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (dto: CreateAsignaciónUsuariosEmpresasmulti-empresaDto) => asignaciónUsuariosEmpresasmulti-empresaApi.create(dto),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
      message.success('AsignaciónUsuariosEmpresasmulti-empresa creado exitosamente');
    },
    onError: (error: any) => {
      const errorMsg = error.response?.data?.message || 'Error al crear asignaciónusuariosempresasmulti-empresa';
      message.error(errorMsg);
    },
  });
};

// Mutation: actualizar
export const useUpdateAsignaciónUsuariosEmpresasmulti-empresa = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, dto }: { id: string; dto: UpdateAsignaciónUsuariosEmpresasmulti-empresaDto }) =>
      asignaciónUsuariosEmpresasmulti-empresaApi.update(id, dto),
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
      queryClient.setQueryData([QUERY_KEY, data.id], data);
      message.success('AsignaciónUsuariosEmpresasmulti-empresa actualizado exitosamente');
    },
    onError: (error: any) => {
      const errorMsg = error.response?.data?.message || 'Error al actualizar asignaciónusuariosempresasmulti-empresa';
      message.error(errorMsg);
    },
  });
};

// Mutation: eliminar
export const useDeleteAsignaciónUsuariosEmpresasmulti-empresa = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: string) => asignaciónUsuariosEmpresasmulti-empresaApi.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
      message.success('AsignaciónUsuariosEmpresasmulti-empresa eliminado exitosamente');
    },
    onError: (error: any) => {
      const errorMsg = error.response?.data?.message || 'Error al eliminar asignaciónusuariosempresasmulti-empresa';
      message.error(errorMsg);
    },
  });
};
```

## Components UI

### Tabla de Listado
```typescript
// src/widgets/AsignaciónUsuariosEmpresasmulti-empresaTable/ui/AsignaciónUsuariosEmpresasmulti-empresaTable.tsx
import React, { useState } from 'react';
import { Table, Button, Space, Input, Modal } from 'antd';
import { EditOutlined, DeleteOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { useAsignaciónUsuariosEmpresasmulti-empresas, useDeleteAsignaciónUsuariosEmpresasmulti-empresa } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';
import { useAsignaciónUsuariosEmpresasmulti-empresaStore } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';
import type { AsignaciónUsuariosEmpresasmulti-empresa, AsignaciónUsuariosEmpresasmulti-empresaFilters } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';

const { Search } = Input;

export const AsignaciónUsuariosEmpresasmulti-empresaTable: React.FC = () => {
  const [filters, setFilters] = useState<AsignaciónUsuariosEmpresasmulti-empresaFilters>({
    page: 1,
    limit: 20,
    sortBy: 'createdAt',
    sortOrder: 'desc',
  });

  const { data, isLoading } = useAsignaciónUsuariosEmpresasmulti-empresas(filters);
  const deleteMutation = useDeleteAsignaciónUsuariosEmpresasmulti-empresa();
  const { openModal } = useAsignaciónUsuariosEmpresasmulti-empresaStore();

  const handleSearch = (value: string) => {
    setFilters((prev) => ({ ...prev, search: value, page: 1 }));
  };

  const handleTableChange = (pagination: any, filters: any, sorter: any) => {
    setFilters((prev) => ({
      ...prev,
      page: pagination.current,
      limit: pagination.pageSize,
      sortBy: sorter.field || 'createdAt',
      sortOrder: sorter.order === 'ascend' ? 'asc' : 'desc',
    }));
  };

  const handleDelete = (id: string, name: string) => {
    Modal.confirm({
      title: '¿Confirmar eliminación?',
      content: `¿Está seguro de eliminar "${name}"? Esta acción no se puede deshacer.`,
      okText: 'Eliminar',
      okType: 'danger',
      cancelText: 'Cancelar',
      onOk: () => deleteMutation.mutate(id),
    });
  };

  const columns: ColumnsType<AsignaciónUsuariosEmpresasmulti-empresa> = [
    {
      title: 'Nombre',
      dataIndex: 'name',
      key: 'name',
      sorter: true,
      width: '40%',
    },
    {
      title: 'Código',
      dataIndex: 'code',
      key: 'code',
      width: '20%',
    },
    {
      title: 'Fecha Creación',
      dataIndex: 'createdAt',
      key: 'createdAt',
      sorter: true,
      width: '20%',
      render: (date: string) => new Date(date).toLocaleDateString('es-ES'),
    },
    {
      title: 'Acciones',
      key: 'actions',
      width: '20%',
      render: (_, record) => (
        <Space>
          <Button
            icon={<EyeOutlined />}
            onClick={() => openModal('view', record)}
            title="Ver detalles"
          />
          <Button
            icon={<EditOutlined />}
            onClick={() => openModal('edit', record)}
            title="Editar"
          />
          <Button
            icon={<DeleteOutlined />}
            danger
            onClick={() => handleDelete(record.id, record.name)}
            title="Eliminar"
          />
        </Space>
      ),
    },
  ];

  return (
    <div>
      <Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
        <Search
          placeholder="Buscar por nombre o código"
          onSearch={handleSearch}
          style={{ width: 300 }}
          allowClear
        />
        <Button
          type="primary"
          icon={<PlusOutlined />}
          onClick={() => openModal('create')}
        >
          Crear AsignaciónUsuariosEmpresasmulti-empresa
        </Button>
      </Space>

      <Table
        columns={columns}
        dataSource={data?.data || []}
        loading={isLoading || deleteMutation.isPending}
        rowKey="id"
        pagination={{
          current: filters.page,
          pageSize: filters.limit,
          total: data?.meta.total || 0,
          showSizeChanger: true,
          showTotal: (total) => `Total: ${total} registros`,
        }}
        onChange={handleTableChange}
      />
    </div>
  );
};
```

### Formulario de Creación/Edición
```typescript
// src/features/createAsignaciónUsuariosEmpresasmulti-empresa/ui/CreateAsignaciónUsuariosEmpresasmulti-empresaForm.tsx
import React, { useEffect } from 'react';
import { Form, Input, Button, Space } from 'antd';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
  createAsignaciónUsuariosEmpresasmulti-empresaSchema,
  updateAsignaciónUsuariosEmpresasmulti-empresaSchema,
  type CreateAsignaciónUsuariosEmpresasmulti-empresaFormData
} from '@entities/asignaciónUsuariosEmpresasmulti-empresa';
import { useCreateAsignaciónUsuariosEmpresasmulti-empresa, useUpdateAsignaciónUsuariosEmpresasmulti-empresa } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';
import type { AsignaciónUsuariosEmpresasmulti-empresa } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';

interface AsignaciónUsuariosEmpresasmulti-empresaFormProps {
  mode: 'create' | 'edit';
  initialData?: AsignaciónUsuariosEmpresasmulti-empresa;
  onSuccess?: () => void;
}

export const AsignaciónUsuariosEmpresasmulti-empresaForm: React.FC<AsignaciónUsuariosEmpresasmulti-empresaFormProps> = ({
  mode,
  initialData,
  onSuccess,
}) => {
  const isEditMode = mode === 'edit';
  const schema = isEditMode ? updateAsignaciónUsuariosEmpresasmulti-empresaSchema : createAsignaciónUsuariosEmpresasmulti-empresaSchema;

  const { control, handleSubmit, formState: { errors }, reset } = useForm<CreateAsignaciónUsuariosEmpresasmulti-empresaFormData>({
    resolver: zodResolver(schema),
    defaultValues: initialData || {
      name: '',
      code: '',
    },
  });

  const createMutation = useCreateAsignaciónUsuariosEmpresasmulti-empresa();
  const updateMutation = useUpdateAsignaciónUsuariosEmpresasmulti-empresa();

  const mutation = isEditMode ? updateMutation : createMutation;

  useEffect(() => {
    if (initialData) {
      reset(initialData);
    }
  }, [initialData, reset]);

  const onSubmit = (data: CreateAsignaciónUsuariosEmpresasmulti-empresaFormData) => {
    if (isEditMode && initialData) {
      updateMutation.mutate(
        { id: initialData.id, dto: data },
        { onSuccess: () => onSuccess?.() }
      );
    } else {
      createMutation.mutate(data, {
        onSuccess: () => {
          reset();
          onSuccess?.();
        },
      });
    }
  };

  return (
    <Form layout="vertical" onFinish={handleSubmit(onSubmit)}>
      <Controller
        name="name"
        control={control}
        render={({ field }) => (
          <Form.Item
            label="Nombre"
            validateStatus={errors.name ? 'error' : ''}
            help={errors.name?.message}
            required
          >
            <Input {...field} placeholder="Ingrese el nombre" />
          </Form.Item>
        )}
      />

      <Controller
        name="code"
        control={control}
        render={({ field }) => (
          <Form.Item
            label="Código"
            validateStatus={errors.code ? 'error' : ''}
            help={errors.code?.message}
          >
            <Input {...field} placeholder="Código único (opcional)" />
          </Form.Item>
        )}
      />

      <Form.Item>
        <Space>
          <Button
            type="primary"
            htmlType="submit"
            loading={mutation.isPending}
          >
            {isEditMode ? 'Actualizar' : 'Crear'}
          </Button>
          <Button onClick={() => reset()}>
            Limpiar
          </Button>
        </Space>
      </Form.Item>
    </Form>
  );
};
```

### Modal Wrapper
```typescript
// src/features/createAsignaciónUsuariosEmpresasmulti-empresa/ui/CreateAsignaciónUsuariosEmpresasmulti-empresaModal.tsx
import React from 'react';
import { Modal } from 'antd';
import { useAsignaciónUsuariosEmpresasmulti-empresaStore } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';
import { AsignaciónUsuariosEmpresasmulti-empresaForm } from './CreateAsignaciónUsuariosEmpresasmulti-empresaForm';

export const AsignaciónUsuariosEmpresasmulti-empresaModal: React.FC = () => {
  const { isModalOpen, modalMode, selectedAsignaciónUsuariosEmpresasmulti-empresa, closeModal } = useAsignaciónUsuariosEmpresasmulti-empresaStore();

  const title = {
    create: 'Crear AsignaciónUsuariosEmpresasmulti-empresa',
    edit: 'Editar AsignaciónUsuariosEmpresasmulti-empresa',
    view: 'Ver AsignaciónUsuariosEmpresasmulti-empresa',
  }[modalMode || 'create'];

  return (
    <Modal
      title={title}
      open={isModalOpen}
      onCancel={closeModal}
      footer={null}
      width={600}
      destroyOnClose
    >
      {modalMode !== 'view' ? (
        <AsignaciónUsuariosEmpresasmulti-empresaForm
          mode={modalMode === 'edit' ? 'edit' : 'create'}
          initialData={selectedAsignaciónUsuariosEmpresasmulti-empresa || undefined}
          onSuccess={closeModal}
        />
      ) : (
        <div>
          <p><strong>Nombre:</strong> {selectedAsignaciónUsuariosEmpresasmulti-empresa?.name}</p>
          <p><strong>Código:</strong> {selectedAsignaciónUsuariosEmpresasmulti-empresa?.code || 'N/A'}</p>
          <p><strong>Creado:</strong> {new Date(selectedAsignaciónUsuariosEmpresasmulti-empresa?.createdAt || '').toLocaleString('es-ES')}</p>
        </div>
      )}
    </Modal>
  );
};
```

### Página Principal
```typescript
// src/pages/AsignaciónUsuariosEmpresasmulti-empresaPage/AsignaciónUsuariosEmpresasmulti-empresaPage.tsx
import React from 'react';
import { Card } from 'antd';
import { AsignaciónUsuariosEmpresasmulti-empresaTable } from '@widgets/AsignaciónUsuariosEmpresasmulti-empresaTable';
import { AsignaciónUsuariosEmpresasmulti-empresaModal } from '@features/createAsignaciónUsuariosEmpresasmulti-empresa';

export const AsignaciónUsuariosEmpresasmulti-empresaPage: React.FC = () => {
  return (
    <div style={{ padding: 24 }}>
      <Card title="Asignación de Usuarios a Empresas (Multi-Empresa)">
        <AsignaciónUsuariosEmpresasmulti-empresaTable />
      </Card>
      <AsignaciónUsuariosEmpresasmulti-empresaModal />
    </div>
  );
};
```

## Validaciones del Cliente

### Validación en Tiempo Real
```typescript
// Las validaciones Zod se ejecutan automáticamente con React Hook Form

// Validación custom adicional (ejemplo)
const validateUniqueAsignaciónUsuariosEmpresasmulti-empresaCode = async (code: string): Promise<boolean> => {
  try {
    const { data } = await asignaciónUsuariosEmpresasmulti-empresaApi.getAll({ search: code });
    return data.data.length === 0;
  } catch {
    return false;
  }
};

// Uso en formulario
<Controller
  name="code"
  control={control}
  rules={{
    validate: async (value) => {
      if (value && !(await validateUniqueAsignaciónUsuariosEmpresasmulti-empresaCode(value))) {
        return 'Este código ya existe';
      }
      return true;
    },
  }}
  render={...}
/>
```

### Mensajes de Error
- Español claro y conciso
- Sugerencias de corrección cuando sea posible
- Highlight visual de campos con error (Ant Design `validateStatus`)

## UX/UI

### Diseño Visual
- **Layout:** Ant Design Pro Layout (Header + Sidebar + Content)
- **Colores:**
  - Primary: `#1890ff` (Ant Design default)
  - Success: `#52c41a`
  - Warning: `#faad14`
  - Error: `#f5222d`
- **Tipografía:**
  - Headings: Inter font family
  - Body: Roboto font family
- **Iconos:** Ant Design Icons

### Feedback al Usuario
- **Loading States:** Spinners en botones (`Button loading={true}`) y tablas (`Table loading={true}`)
- **Success Messages:** `message.success('Operación exitosa')`
- **Error Messages:** `message.error('Error: detalles...')`
- **Confirmations:** `Modal.confirm()` para acciones destructivas (delete)
- **Progress:** `Progress` para operaciones largas

### Responsiveness
- **Desktop (1200px+):** Tabla completa con todas las columnas
- **Tablet (768-1199px):** Tabla adaptada, algunas columnas ocultas
- **Mobile (<768px):** Reemplazar tabla por cards (`<List>` de Ant Design)

```typescript
// Ejemplo responsive
import { useMediaQuery } from '@shared/hooks/useMediaQuery';

const isMobile = useMediaQuery('(max-width: 768px)');

return isMobile ? (
  <List
    dataSource={data?.data}
    renderItem={(item) => (
      <List.Item>
        <Card>{/* Card layout */}</Card>
      </List.Item>
    )}
  />
) : (
  <Table ... />
);
```

## Permisos (RBAC en UI)

```typescript
// src/shared/hooks/usePermissions.ts
import { useAuth } from '@modules/auth';

export const usePermissions = () => {
  const { user } = useAuth();

  const can = (permission: string): boolean => {
    return user?.permissions?.includes(permission) ?? false;
  };

  const hasRole = (role: string): boolean => {
    return user?.roles?.includes(role) ?? false;
  };

  return { can, hasRole };
};

// Uso en componentes
import { usePermissions } from '@shared/hooks/usePermissions';

const { can } = usePermissions();

{can('mgn-002.asignaciónUsuariosEmpresasmulti-empresa.create') && (
  <Button type="primary" onClick={handleCreate}>
    Crear AsignaciónUsuariosEmpresasmulti-empresa
  </Button>
)}

{can('mgn-002.asignaciónUsuariosEmpresasmulti-empresa.delete') && (
  <Button danger onClick={handleDelete}>
    Eliminar
  </Button>
)}
```

## Testing

### Component Tests (Vitest + React Testing Library)
```typescript
// src/widgets/AsignaciónUsuariosEmpresasmulti-empresaTable/ui/AsignaciónUsuariosEmpresasmulti-empresaTable.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AsignaciónUsuariosEmpresasmulti-empresaTable } from './AsignaciónUsuariosEmpresasmulti-empresaTable';
import { asignaciónUsuariosEmpresasmulti-empresaApi } from '@entities/asignaciónUsuariosEmpresasmulti-empresa';

// Mock API
vi.mock('@entities/asignaciónUsuariosEmpresasmulti-empresa', () => ({
  asignaciónUsuariosEmpresasmulti-empresaApi: {
    getAll: vi.fn(),
    delete: vi.fn(),
  },
}));

describe('AsignaciónUsuariosEmpresasmulti-empresaTable', () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });

  const wrapper = ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should render table with data', async () => {
    const mockData = {
      data: [
        { id: '1', name: 'Test AsignaciónUsuariosEmpresasmulti-empresa 1', code: 'TEST1', createdAt: '2025-11-24' },
        { id: '2', name: 'Test AsignaciónUsuariosEmpresasmulti-empresa 2', code: 'TEST2', createdAt: '2025-11-24' },
      ],
      meta: { page: 1, limit: 20, total: 2, totalPages: 1 },
    };

    vi.mocked(asignaciónUsuariosEmpresasmulti-empresaApi.getAll).mockResolvedValue(mockData);

    render(<AsignaciónUsuariosEmpresasmulti-empresaTable />, { wrapper });

    await waitFor(() => {
      expect(screen.getByText('Test AsignaciónUsuariosEmpresasmulti-empresa 1')).toBeInTheDocument();
      expect(screen.getByText('Test AsignaciónUsuariosEmpresasmulti-empresa 2')).toBeInTheDocument();
    });
  });

  it('should call delete mutation on delete button click', async () => {
    const user = userEvent.setup();

    const mockData = {
      data: [{ id: '1', name: 'To Delete', code: 'DEL', createdAt: '2025-11-24' }],
      meta: { page: 1, limit: 20, total: 1, totalPages: 1 },
    };

    vi.mocked(asignaciónUsuariosEmpresasmulti-empresaApi.getAll).mockResolvedValue(mockData);
    vi.mocked(asignaciónUsuariosEmpresasmulti-empresaApi.delete).mockResolvedValue();

    render(<AsignaciónUsuariosEmpresasmulti-empresaTable />, { wrapper });

    await waitFor(() => {
      expect(screen.getByText('To Delete')).toBeInTheDocument();
    });

    const deleteBtn = screen.getByTitle('Eliminar');
    await user.click(deleteBtn);

    // Confirmar modal
    const confirmBtn = screen.getByText('Eliminar');
    await user.click(confirmBtn);

    await waitFor(() => {
      expect(asignaciónUsuariosEmpresasmulti-empresaApi.delete).toHaveBeenCalledWith('1');
    });
  });

  it('should filter data on search', async () => {
    const user = userEvent.setup();

    vi.mocked(asignaciónUsuariosEmpresasmulti-empresaApi.getAll).mockResolvedValue({
      data: [],
      meta: { page: 1, limit: 20, total: 0, totalPages: 0 },
    });

    render(<AsignaciónUsuariosEmpresasmulti-empresaTable />, { wrapper });

    const searchInput = screen.getByPlaceholderText('Buscar por nombre o código');
    await user.type(searchInput, 'test');
    await user.keyboard('{Enter}');

    await waitFor(() => {
      expect(asignaciónUsuariosEmpresasmulti-empresaApi.getAll).toHaveBeenCalledWith(
        expect.objectContaining({ search: 'test' })
      );
    });
  });
});
```

### E2E Tests (Playwright)
```typescript
// e2e/asignaciónUsuariosEmpresasmulti-empresa.spec.ts
import { test, expect } from '@playwright/test';

test.describe('AsignaciónUsuariosEmpresasmulti-empresa Management', () => {
  test.beforeEach(async ({ page }) => {
    // Login
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'Test1234!');
    await page.click('button[type="submit"]');
    await page.waitForURL('/mgn-002/asignaciónUsuariosEmpresasmulti-empresa');
  });

  test('should create new asignaciónUsuariosEmpresasmulti-empresa', async ({ page }) => {
    await page.goto('/mgn-002/asignaciónUsuariosEmpresasmulti-empresa');

    // Click create button
    await page.click('text=Crear AsignaciónUsuariosEmpresasmulti-empresa');

    // Fill form
    await page.fill('[name="name"]', 'E2E Test AsignaciónUsuariosEmpresasmulti-empresa');
    await page.fill('[name="code"]', 'E2E001');

    // Submit
    await page.click('button[type="submit"]');

    // Verify success message
    await expect(page.locator('.ant-message-success')).toContainText('creado exitosamente');

    // Verify in table
    await expect(page.locator('table')).toContainText('E2E Test AsignaciónUsuariosEmpresasmulti-empresa');
  });

  test('should edit existing asignaciónUsuariosEmpresasmulti-empresa', async ({ page }) => {
    await page.goto('/mgn-002/asignaciónUsuariosEmpresasmulti-empresa');

    // Click edit on first row
    await page.click('table tbody tr:first-child button[title="Editar"]');

    // Update name
    await page.fill('[name="name"]', 'Updated Name');

    // Submit
    await page.click('button[type="submit"]');

    // Verify success
    await expect(page.locator('.ant-message-success')).toContainText('actualizado exitosamente');
  });

  test('should delete asignaciónUsuariosEmpresasmulti-empresa with confirmation', async ({ page }) => {
    await page.goto('/mgn-002/asignaciónUsuariosEmpresasmulti-empresa');

    // Click delete on first row
    await page.click('table tbody tr:first-child button[title="Eliminar"]');

    // Confirm deletion
    await page.click('.ant-modal-confirm button.ant-btn-dangerous');

    // Verify success
    await expect(page.locator('.ant-message-success')).toContainText('eliminado exitosamente');
  });

  test('should validate required fields', async ({ page }) => {
    await page.goto('/mgn-002/asignaciónUsuariosEmpresasmulti-empresa');

    // Click create button
    await page.click('text=Crear AsignaciónUsuariosEmpresasmulti-empresa');

    // Try to submit empty form
    await page.click('button[type="submit"]');

    // Verify validation errors
    await expect(page.locator('.ant-form-item-explain-error')).toContainText('al menos 3 caracteres');
  });

  test('should search and filter asignaciónUsuariosEmpresasmulti-empresas', async ({ page }) => {
    await page.goto('/mgn-002/asignaciónUsuariosEmpresasmulti-empresa');

    // Search
    await page.fill('[placeholder="Buscar por nombre o código"]', 'test');
    await page.keyboard.press('Enter');

    // Verify API call (check network tab or wait for results)
    await page.waitForTimeout(500);

    // Results should be filtered
    const rows = page.locator('table tbody tr');
    await expect(rows.first()).toBeVisible();
  });
});
```

## Performance

### Optimizaciones React
```typescript
// 1. Lazy loading de páginas
const AsignaciónUsuariosEmpresasmulti-empresaPage = React.lazy(() => import('@pages/AsignaciónUsuariosEmpresasmulti-empresaPage'));

// 2. Memo para componentes pesados
export const AsignaciónUsuariosEmpresasmulti-empresaCard = React.memo<AsignaciónUsuariosEmpresasmulti-empresaCardProps>(({ data }) => {
  // ...
});

// 3. useMemo para cálculos pesados
const filteredData = useMemo(() => {
  return data?.data.filter((item) => item.status === 'active');
}, [data]);

// 4. useCallback para funciones pasadas como props
const handleEdit = useCallback((id: string) => {
  openModal('edit', data.find((item) => item.id === id));
}, [data, openModal]);
```

### Virtualización para Listas Largas
```typescript
// Para tablas con >100 rows
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={600}
  itemCount={data?.data.length || 0}
  itemSize={50}
  width={'100%'}
>
  {({ index, style }) => (
    <div style={style}>{/* Row content */}</div>
  )}
</FixedSizeList>
```

### Debounce en Búsqueda
```typescript
import { useDebouncedCallback } from 'use-debounce';

const debouncedSearch = useDebouncedCallback(
  (value: string) => {
    setFilters((prev) => ({ ...prev, search: value, page: 1 }));
  },
  300
);

<Search onChange={(e) => debouncedSearch(e.target.value)} />
```

### Bundle Size
- **Code splitting:** Lazy loading de páginas (`React.lazy()`)
- **Tree shaking:** Importar solo lo necesario de Ant Design
- **Chunk optimization:** Vite automático
- **Target bundle size:** <200 KB por chunk

## Referencias

- [RF Asociado](../../requerimientos-funcionales/mgn-002/RF-MGN-002-003-asignación-de-usuarios-a-empresas-multi-empresa.md)
- [ET Backend](../backend/mgn-002/ET-BACKEND-MGN-002-003-asignación-de-usuarios-a-empresas-multi-empresa.md)
- [Gamilit Frontend Patterns](../../00-analisis-referencias/gamilit/frontend-patterns.md)
- [ADR-009: Frontend Architecture](../../adr/ADR-009-frontend-architecture.md)

## Dependencias

### Módulos Frontend
- `AuthModule` - Autenticación y autorización
- `SharedModule` - Componentes y utilities compartidos

### RF Bloqueantes
- RF-MGN-001-001 (Autenticación de Usuarios)
- RF-MGN-002-003 Backend completado

## Notas de Implementación

- [ ] Crear estructura FSD: `entities/asignaciónUsuariosEmpresasmulti-empresa`, `features/createAsignaciónUsuariosEmpresasmulti-empresa`, `widgets/AsignaciónUsuariosEmpresasmulti-empresaTable`
- [ ] Definir types e interfaces en `entities/asignaciónUsuariosEmpresasmulti-empresa/model/types.ts`
- [ ] Crear schemas de validación Zod
- [ ] Implementar API client con axios
- [ ] Crear React Query hooks (queries + mutations)
- [ ] Implementar Zustand store para UI state
- [ ] Crear componentes UI (Table, Form, Modal)
- [ ] Implementar rutas en React Router
- [ ] Crear tests (componentes + e2e)
- [ ] Validar responsiveness (desktop + tablet + mobile)
- [ ] Validar con criterios de aceptación del RF
- [ ] Code review por Tech Lead
- [ ] QA testing

## Estimación

- **Frontend Development:** 4 SP
- **Testing (Unit + e2e):** 2 SP
- **Code Review + QA:** 1 SP
- **Total:** 8 SP

---

**Documento generado:** 2025-11-24
**Versión:** 1.0
**Estado:** Diseñado
**Próximo paso:** Implementación
