# ET-FRONTEND-MGN-006-003: Workflow de Aprobación de Compras

**RF Asociado:** [RF-MGN-006-003](../../requerimientos-funcionales/mgn-006/RF-MGN-006-003-workflow-de-aprobación-de-compras.md)
**ET Backend:** [ET-BACKEND-MGN-006-003](../backend/mgn-006/ET-BACKEND-MGN-006-003-workflow-de-aprobación-de-compras.md)
**Módulo:** MGN-006
**Complejidad:** Baja
**Story Points:** 2 SP (Frontend)
**Estado:** Diseñado
**Fecha:** 2025-11-24

## Resumen Técnico

Implementación frontend para workflow de aprobación de compras. 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
│   └── WorkflowAprobaciónComprasPage/
│       ├── index.tsx
│       └── WorkflowAprobaciónComprasPage.tsx
├── widgets/                      # Complex UI blocks
│   └── WorkflowAprobaciónComprasTable/
│       ├── ui/
│       │   └── WorkflowAprobaciónComprasTable.tsx
│       └── index.ts
├── features/                     # User interactions
│   ├── createWorkflowAprobaciónCompras/
│   │   ├── ui/
│   │   │   └── CreateWorkflowAprobaciónComprasForm.tsx
│   │   ├── model/
│   │   │   └── useWorkflowAprobaciónComprasActions.ts
│   │   └── index.ts
│   ├── updateWorkflowAprobaciónCompras/
│   └── deleteWorkflowAprobaciónCompras/
├── entities/                     # Business entities
│   └── workflowAprobaciónCompras/
│       ├── model/
│       │   ├── types.ts
│       │   ├── schemas.ts
│       │   └── workflowAprobaciónCompras.store.ts
│       ├── api/
│       │   ├── workflowAprobaciónCompras.api.ts
│       │   └── workflowAprobaciónCompras.queries.ts
│       ├── ui/
│       │   └── WorkflowAprobaciónComprasCard.tsx
│       └── index.ts
└── shared/                       # Shared code
    ├── ui/                       # UI kit
    │   ├── Button/
    │   ├── Modal/
    │   └── Table/
    ├── api/
    │   └── client.ts
    └── lib/
        └── utils.ts
```

## Rutas

```typescript
// src/app/routes/mgn-006.routes.tsx
export const WorkflowAprobaciónComprasRoutes = {
  list: '/mgn-006/workflowAprobaciónCompras',
  create: '/mgn-006/workflowAprobaciónCompras/create',
  edit: '/mgn-006/workflowAprobaciónCompras/:id/edit',
  view: '/mgn-006/workflowAprobaciónCompras/:id',
};

// Integración en Router
<Route path="/mgn-006">
  <Route path="workflowAprobaciónCompras" element={<WorkflowAprobaciónComprasPage />} />
  <Route path="workflowAprobaciónCompras/create" element={<CreateWorkflowAprobaciónComprasPage />} />
  <Route path="workflowAprobaciónCompras/:id/edit" element={<EditWorkflowAprobaciónComprasPage />} />
  <Route path="workflowAprobaciónCompras/:id" element={<ViewWorkflowAprobaciónComprasPage />} />
</Route>
```

## Types / Interfaces

```typescript
// src/entities/workflowAprobaciónCompras/model/types.ts

export interface WorkflowAprobaciónCompras {
  id: string;
  tenantId: string;
  name: string;
  code?: string;
  createdAt: string;
  updatedAt?: string;
  deletedAt?: string;
}

export interface CreateWorkflowAprobaciónComprasDto {
  name: string;
  code?: string;
}

export type UpdateWorkflowAprobaciónComprasDto = Partial<CreateWorkflowAprobaciónComprasDto>;

export interface WorkflowAprobaciónComprasFilters {
  search?: string;
  page?: number;
  limit?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

export interface WorkflowAprobaciónComprasListResponse {
  data: WorkflowAprobaciónCompras[];
  meta: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}
```

## Schemas de Validación (Zod)

```typescript
// src/entities/workflowAprobaciónCompras/model/schemas.ts
import { z } from 'zod';

export const createWorkflowAprobaciónComprasSchema = 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 updateWorkflowAprobaciónComprasSchema = createWorkflowAprobaciónComprasSchema.partial();

export type CreateWorkflowAprobaciónComprasFormData = z.infer<typeof createWorkflowAprobaciónComprasSchema>;
export type UpdateWorkflowAprobaciónComprasFormData = z.infer<typeof updateWorkflowAprobaciónComprasSchema>;

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

## API Client

```typescript
// src/entities/workflowAprobaciónCompras/api/workflowAprobaciónCompras.api.ts
import { apiClient } from '@shared/api/client';
import type {
  WorkflowAprobaciónCompras,
  CreateWorkflowAprobaciónComprasDto,
  UpdateWorkflowAprobaciónComprasDto,
  WorkflowAprobaciónComprasFilters,
  WorkflowAprobaciónComprasListResponse
} from '../model/types';

const BASE_URL = '/api/v1/purchase/approvals';

export const workflowAprobaciónComprasApi = {
  getAll: async (filters?: WorkflowAprobaciónComprasFilters): Promise<WorkflowAprobaciónComprasListResponse> => {
    const { data } = await apiClient.get<WorkflowAprobaciónComprasListResponse>(BASE_URL, {
      params: filters,
    });
    return data;
  },

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

  create: async (dto: CreateWorkflowAprobaciónComprasDto): Promise<WorkflowAprobaciónCompras> => {
    const { data } = await apiClient.post<{ data: WorkflowAprobaciónCompras }>(BASE_URL, dto);
    return data.data;
  },

  update: async (id: string, dto: UpdateWorkflowAprobaciónComprasDto): Promise<WorkflowAprobaciónCompras> => {
    const { data } = await apiClient.put<{ data: WorkflowAprobaciónCompras }>(`${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/workflowAprobaciónCompras/model/workflowAprobaciónCompras.store.ts
import { create } from 'zustand';
import { WorkflowAprobaciónCompras } from './types';

interface WorkflowAprobaciónComprasStore {
  selectedWorkflowAprobaciónCompras: WorkflowAprobaciónCompras | null;
  isModalOpen: boolean;
  modalMode: 'create' | 'edit' | 'view' | null;

  setSelectedWorkflowAprobaciónCompras: (entity: WorkflowAprobaciónCompras | null) => void;
  openModal: (mode: 'create' | 'edit' | 'view', entity?: WorkflowAprobaciónCompras) => void;
  closeModal: () => void;
}

export const useWorkflowAprobaciónComprasStore = create<WorkflowAprobaciónComprasStore>((set) => ({
  selectedWorkflowAprobaciónCompras: null,
  isModalOpen: false,
  modalMode: null,

  setSelectedWorkflowAprobaciónCompras: (entity) => set({ selectedWorkflowAprobaciónCompras: entity }),

  openModal: (mode, entity) => set({
    isModalOpen: true,
    modalMode: mode,
    selectedWorkflowAprobaciónCompras: entity || null,
  }),

  closeModal: () => set({
    isModalOpen: false,
    modalMode: null,
    selectedWorkflowAprobaciónCompras: null,
  }),
}));
```

### React Query Hooks (servidor state)
```typescript
// src/entities/workflowAprobaciónCompras/api/workflowAprobaciónCompras.queries.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { workflowAprobaciónComprasApi } from './workflowAprobaciónCompras.api';
import type { CreateWorkflowAprobaciónComprasDto, UpdateWorkflowAprobaciónComprasDto, WorkflowAprobaciónComprasFilters } from '../model/types';

const QUERY_KEY = 'workflowAprobaciónCompras';

// Query: obtener lista
export const useWorkflowAprobaciónComprass = (filters?: WorkflowAprobaciónComprasFilters) => {
  return useQuery({
    queryKey: [QUERY_KEY, filters],
    queryFn: () => workflowAprobaciónComprasApi.getAll(filters),
    staleTime: 5 * 60 * 1000, // 5 minutos
  });
};

// Query: obtener por ID
export const useWorkflowAprobaciónCompras = (id: string) => {
  return useQuery({
    queryKey: [QUERY_KEY, id],
    queryFn: () => workflowAprobaciónComprasApi.getById(id),
    enabled: !!id,
  });
};

// Mutation: crear
export const useCreateWorkflowAprobaciónCompras = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (dto: CreateWorkflowAprobaciónComprasDto) => workflowAprobaciónComprasApi.create(dto),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
      message.success('WorkflowAprobaciónCompras creado exitosamente');
    },
    onError: (error: any) => {
      const errorMsg = error.response?.data?.message || 'Error al crear workflowaprobacióncompras';
      message.error(errorMsg);
    },
  });
};

// Mutation: actualizar
export const useUpdateWorkflowAprobaciónCompras = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, dto }: { id: string; dto: UpdateWorkflowAprobaciónComprasDto }) =>
      workflowAprobaciónComprasApi.update(id, dto),
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
      queryClient.setQueryData([QUERY_KEY, data.id], data);
      message.success('WorkflowAprobaciónCompras actualizado exitosamente');
    },
    onError: (error: any) => {
      const errorMsg = error.response?.data?.message || 'Error al actualizar workflowaprobacióncompras';
      message.error(errorMsg);
    },
  });
};

// Mutation: eliminar
export const useDeleteWorkflowAprobaciónCompras = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: string) => workflowAprobaciónComprasApi.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
      message.success('WorkflowAprobaciónCompras eliminado exitosamente');
    },
    onError: (error: any) => {
      const errorMsg = error.response?.data?.message || 'Error al eliminar workflowaprobacióncompras';
      message.error(errorMsg);
    },
  });
};
```

## Components UI

### Tabla de Listado
```typescript
// src/widgets/WorkflowAprobaciónComprasTable/ui/WorkflowAprobaciónComprasTable.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 { useWorkflowAprobaciónComprass, useDeleteWorkflowAprobaciónCompras } from '@entities/workflowAprobaciónCompras';
import { useWorkflowAprobaciónComprasStore } from '@entities/workflowAprobaciónCompras';
import type { WorkflowAprobaciónCompras, WorkflowAprobaciónComprasFilters } from '@entities/workflowAprobaciónCompras';

const { Search } = Input;

export const WorkflowAprobaciónComprasTable: React.FC = () => {
  const [filters, setFilters] = useState<WorkflowAprobaciónComprasFilters>({
    page: 1,
    limit: 20,
    sortBy: 'createdAt',
    sortOrder: 'desc',
  });

  const { data, isLoading } = useWorkflowAprobaciónComprass(filters);
  const deleteMutation = useDeleteWorkflowAprobaciónCompras();
  const { openModal } = useWorkflowAprobaciónComprasStore();

  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<WorkflowAprobaciónCompras> = [
    {
      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 WorkflowAprobaciónCompras
        </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/createWorkflowAprobaciónCompras/ui/CreateWorkflowAprobaciónComprasForm.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 {
  createWorkflowAprobaciónComprasSchema,
  updateWorkflowAprobaciónComprasSchema,
  type CreateWorkflowAprobaciónComprasFormData
} from '@entities/workflowAprobaciónCompras';
import { useCreateWorkflowAprobaciónCompras, useUpdateWorkflowAprobaciónCompras } from '@entities/workflowAprobaciónCompras';
import type { WorkflowAprobaciónCompras } from '@entities/workflowAprobaciónCompras';

interface WorkflowAprobaciónComprasFormProps {
  mode: 'create' | 'edit';
  initialData?: WorkflowAprobaciónCompras;
  onSuccess?: () => void;
}

export const WorkflowAprobaciónComprasForm: React.FC<WorkflowAprobaciónComprasFormProps> = ({
  mode,
  initialData,
  onSuccess,
}) => {
  const isEditMode = mode === 'edit';
  const schema = isEditMode ? updateWorkflowAprobaciónComprasSchema : createWorkflowAprobaciónComprasSchema;

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

  const createMutation = useCreateWorkflowAprobaciónCompras();
  const updateMutation = useUpdateWorkflowAprobaciónCompras();

  const mutation = isEditMode ? updateMutation : createMutation;

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

  const onSubmit = (data: CreateWorkflowAprobaciónComprasFormData) => {
    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/createWorkflowAprobaciónCompras/ui/CreateWorkflowAprobaciónComprasModal.tsx
import React from 'react';
import { Modal } from 'antd';
import { useWorkflowAprobaciónComprasStore } from '@entities/workflowAprobaciónCompras';
import { WorkflowAprobaciónComprasForm } from './CreateWorkflowAprobaciónComprasForm';

export const WorkflowAprobaciónComprasModal: React.FC = () => {
  const { isModalOpen, modalMode, selectedWorkflowAprobaciónCompras, closeModal } = useWorkflowAprobaciónComprasStore();

  const title = {
    create: 'Crear WorkflowAprobaciónCompras',
    edit: 'Editar WorkflowAprobaciónCompras',
    view: 'Ver WorkflowAprobaciónCompras',
  }[modalMode || 'create'];

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

### Página Principal
```typescript
// src/pages/WorkflowAprobaciónComprasPage/WorkflowAprobaciónComprasPage.tsx
import React from 'react';
import { Card } from 'antd';
import { WorkflowAprobaciónComprasTable } from '@widgets/WorkflowAprobaciónComprasTable';
import { WorkflowAprobaciónComprasModal } from '@features/createWorkflowAprobaciónCompras';

export const WorkflowAprobaciónComprasPage: React.FC = () => {
  return (
    <div style={{ padding: 24 }}>
      <Card title="Workflow de Aprobación de Compras">
        <WorkflowAprobaciónComprasTable />
      </Card>
      <WorkflowAprobaciónComprasModal />
    </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 validateUniqueWorkflowAprobaciónComprasCode = async (code: string): Promise<boolean> => {
  try {
    const { data } = await workflowAprobaciónComprasApi.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 validateUniqueWorkflowAprobaciónComprasCode(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-006.workflowAprobaciónCompras.create') && (
  <Button type="primary" onClick={handleCreate}>
    Crear WorkflowAprobaciónCompras
  </Button>
)}

{can('mgn-006.workflowAprobaciónCompras.delete') && (
  <Button danger onClick={handleDelete}>
    Eliminar
  </Button>
)}
```

## Testing

### Component Tests (Vitest + React Testing Library)
```typescript
// src/widgets/WorkflowAprobaciónComprasTable/ui/WorkflowAprobaciónComprasTable.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 { WorkflowAprobaciónComprasTable } from './WorkflowAprobaciónComprasTable';
import { workflowAprobaciónComprasApi } from '@entities/workflowAprobaciónCompras';

// Mock API
vi.mock('@entities/workflowAprobaciónCompras', () => ({
  workflowAprobaciónComprasApi: {
    getAll: vi.fn(),
    delete: vi.fn(),
  },
}));

describe('WorkflowAprobaciónComprasTable', () => {
  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 WorkflowAprobaciónCompras 1', code: 'TEST1', createdAt: '2025-11-24' },
        { id: '2', name: 'Test WorkflowAprobaciónCompras 2', code: 'TEST2', createdAt: '2025-11-24' },
      ],
      meta: { page: 1, limit: 20, total: 2, totalPages: 1 },
    };

    vi.mocked(workflowAprobaciónComprasApi.getAll).mockResolvedValue(mockData);

    render(<WorkflowAprobaciónComprasTable />, { wrapper });

    await waitFor(() => {
      expect(screen.getByText('Test WorkflowAprobaciónCompras 1')).toBeInTheDocument();
      expect(screen.getByText('Test WorkflowAprobaciónCompras 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(workflowAprobaciónComprasApi.getAll).mockResolvedValue(mockData);
    vi.mocked(workflowAprobaciónComprasApi.delete).mockResolvedValue();

    render(<WorkflowAprobaciónComprasTable />, { 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(workflowAprobaciónComprasApi.delete).toHaveBeenCalledWith('1');
    });
  });

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

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

    render(<WorkflowAprobaciónComprasTable />, { wrapper });

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

    await waitFor(() => {
      expect(workflowAprobaciónComprasApi.getAll).toHaveBeenCalledWith(
        expect.objectContaining({ search: 'test' })
      );
    });
  });
});
```

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

test.describe('WorkflowAprobaciónCompras 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-006/workflowAprobaciónCompras');
  });

  test('should create new workflowAprobaciónCompras', async ({ page }) => {
    await page.goto('/mgn-006/workflowAprobaciónCompras');

    // Click create button
    await page.click('text=Crear WorkflowAprobaciónCompras');

    // Fill form
    await page.fill('[name="name"]', 'E2E Test WorkflowAprobaciónCompras');
    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 WorkflowAprobaciónCompras');
  });

  test('should edit existing workflowAprobaciónCompras', async ({ page }) => {
    await page.goto('/mgn-006/workflowAprobaciónCompras');

    // 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 workflowAprobaciónCompras with confirmation', async ({ page }) => {
    await page.goto('/mgn-006/workflowAprobaciónCompras');

    // 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-006/workflowAprobaciónCompras');

    // Click create button
    await page.click('text=Crear WorkflowAprobaciónCompras');

    // 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 workflowAprobaciónComprass', async ({ page }) => {
    await page.goto('/mgn-006/workflowAprobaciónCompras');

    // 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 WorkflowAprobaciónComprasPage = React.lazy(() => import('@pages/WorkflowAprobaciónComprasPage'));

// 2. Memo para componentes pesados
export const WorkflowAprobaciónComprasCard = React.memo<WorkflowAprobaciónComprasCardProps>(({ 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-006/RF-MGN-006-003-workflow-de-aprobación-de-compras.md)
- [ET Backend](../backend/mgn-006/ET-BACKEND-MGN-006-003-workflow-de-aprobación-de-compras.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-006-003 Backend completado

## Notas de Implementación

- [ ] Crear estructura FSD: `entities/workflowAprobaciónCompras`, `features/createWorkflowAprobaciónCompras`, `widgets/WorkflowAprobaciónComprasTable`
- [ ] Definir types e interfaces en `entities/workflowAprobaciónCompras/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:** 2 SP
- **Testing (Unit + e2e):** 1 SP
- **Code Review + QA:** 1 SP
- **Total:** 5 SP

---

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