# Frontend Specifications - ERP Construccion

**Fecha:** 2025-12-05
**Version:** 1.0.0
**Stack:** React 18 / TypeScript 5.3+ / Vite / Zustand / TanStack Query / TailwindCSS

---

## Estructura del Directorio

```
06-frontend-specs/
├── README.md (este archivo)
├── components/
│   ├── COMP-shared.md          [Componentes compartidos]
│   ├── COMP-construction.md    [Componentes de construccion]
│   ├── COMP-compliance.md      [Componentes de cumplimiento]
│   ├── COMP-finance.md         [Componentes financieros]
│   ├── COMP-assets.md          [Componentes de activos]
│   └── COMP-documents.md       [Componentes DMS]
├── pages/
│   ├── PAGES-construction.md   [Paginas de proyectos]
│   ├── PAGES-compliance.md     [Paginas de cumplimiento]
│   ├── PAGES-finance.md        [Paginas financieras]
│   ├── PAGES-assets.md         [Paginas de activos]
│   └── PAGES-documents.md      [Paginas de documentos]
└── stores/
    └── STORES-spec.md          [Especificacion de stores]
```

---

## Arquitectura Frontend

### Estructura de Aplicacion

```
apps/frontend/src/
├── app/
│   ├── App.tsx
│   ├── router.tsx
│   └── providers.tsx
├── features/
│   ├── construction/
│   │   ├── components/
│   │   ├── pages/
│   │   ├── hooks/
│   │   ├── api/
│   │   └── store/
│   ├── compliance/
│   ├── finance/
│   ├── assets/
│   └── documents/
├── shared/
│   ├── components/
│   │   ├── ui/
│   │   ├── forms/
│   │   ├── tables/
│   │   └── charts/
│   ├── hooks/
│   ├── utils/
│   ├── types/
│   └── api/
├── layouts/
│   ├── MainLayout.tsx
│   ├── AuthLayout.tsx
│   └── components/
└── styles/
```

---

## Patrones de Desarrollo

### 1. Feature-Based Structure

Cada modulo sigue una estructura consistente:

```
features/construction/
├── components/
│   ├── ProjectCard.tsx
│   ├── ProjectList.tsx
│   ├── BudgetForm.tsx
│   └── ProgressChart.tsx
├── pages/
│   ├── ProjectsPage.tsx
│   ├── ProjectDetailPage.tsx
│   ├── BudgetPage.tsx
│   └── ProgressPage.tsx
├── hooks/
│   ├── useProjects.ts
│   ├── useBudget.ts
│   └── useProgress.ts
├── api/
│   ├── projects.api.ts
│   ├── budgets.api.ts
│   └── progress.api.ts
├── store/
│   └── construction.store.ts
├── types/
│   └── construction.types.ts
└── index.ts
```

### 2. Component Pattern

```tsx
// components/ProjectCard.tsx
import { FC, memo } from 'react';
import { Card, Badge, Progress } from '@/shared/components/ui';
import { Project } from '../types/construction.types';

interface ProjectCardProps {
  project: Project;
  onClick?: (id: string) => void;
  showProgress?: boolean;
}

export const ProjectCard: FC<ProjectCardProps> = memo(({
  project,
  onClick,
  showProgress = true
}) => {
  return (
    <Card
      className="p-4 hover:shadow-lg transition-shadow cursor-pointer"
      onClick={() => onClick?.(project.id)}
    >
      <div className="flex justify-between items-start">
        <div>
          <h3 className="font-semibold text-lg">{project.name}</h3>
          <p className="text-sm text-gray-500">{project.code}</p>
        </div>
        <Badge variant={getStatusVariant(project.status)}>
          {project.status}
        </Badge>
      </div>

      {showProgress && (
        <div className="mt-4">
          <Progress value={project.progressPercentage} />
          <span className="text-sm text-gray-600">
            {project.progressPercentage}% completado
          </span>
        </div>
      )}
    </Card>
  );
});

ProjectCard.displayName = 'ProjectCard';
```

### 3. Hook Pattern (TanStack Query)

```tsx
// hooks/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { projectsApi } from '../api/projects.api';
import { CreateProjectDto, ProjectQueryDto } from '../types/construction.types';

const PROJECTS_KEY = ['projects'];

export function useProjects(query?: ProjectQueryDto) {
  return useQuery({
    queryKey: [...PROJECTS_KEY, query],
    queryFn: () => projectsApi.findAll(query),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useProject(id: string) {
  return useQuery({
    queryKey: [...PROJECTS_KEY, id],
    queryFn: () => projectsApi.findOne(id),
    enabled: !!id,
  });
}

export function useCreateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (dto: CreateProjectDto) => projectsApi.create(dto),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
    },
  });
}

export function useUpdateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, dto }: { id: string; dto: UpdateProjectDto }) =>
      projectsApi.update(id, dto),
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({ queryKey: [...PROJECTS_KEY, id] });
      queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
    },
  });
}

export function useDeleteProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (id: string) => projectsApi.delete(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: PROJECTS_KEY });
    },
  });
}
```

### 4. API Layer Pattern

```tsx
// api/projects.api.ts
import { apiClient } from '@/shared/api/client';
import { PaginatedResponse } from '@/shared/types';
import {
  Project,
  ProjectDetail,
  CreateProjectDto,
  UpdateProjectDto,
  ProjectQueryDto
} from '../types/construction.types';

const BASE_URL = '/api/v1/projects';

export const projectsApi = {
  findAll: async (query?: ProjectQueryDto): Promise<PaginatedResponse<Project>> => {
    const response = await apiClient.get(BASE_URL, { params: query });
    return response.data;
  },

  findOne: async (id: string): Promise<ProjectDetail> => {
    const response = await apiClient.get(`${BASE_URL}/${id}`);
    return response.data;
  },

  create: async (dto: CreateProjectDto): Promise<Project> => {
    const response = await apiClient.post(BASE_URL, dto);
    return response.data;
  },

  update: async (id: string, dto: UpdateProjectDto): Promise<Project> => {
    const response = await apiClient.put(`${BASE_URL}/${id}`, dto);
    return response.data;
  },

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

  updateStatus: async (id: string, status: ProjectStatus): Promise<Project> => {
    const response = await apiClient.put(`${BASE_URL}/${id}/status`, { status });
    return response.data;
  }
};
```

### 5. Store Pattern (Zustand)

```tsx
// store/construction.store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { Project, ProjectFilters } from '../types/construction.types';

interface ConstructionState {
  // State
  selectedProjectId: string | null;
  filters: ProjectFilters;
  viewMode: 'list' | 'grid' | 'kanban';

  // Actions
  setSelectedProject: (id: string | null) => void;
  setFilters: (filters: Partial<ProjectFilters>) => void;
  resetFilters: () => void;
  setViewMode: (mode: 'list' | 'grid' | 'kanban') => void;
}

const initialFilters: ProjectFilters = {
  status: undefined,
  search: '',
  dateRange: undefined,
};

export const useConstructionStore = create<ConstructionState>()(
  devtools(
    persist(
      (set) => ({
        // Initial State
        selectedProjectId: null,
        filters: initialFilters,
        viewMode: 'list',

        // Actions
        setSelectedProject: (id) =>
          set({ selectedProjectId: id }, false, 'setSelectedProject'),

        setFilters: (filters) =>
          set(
            (state) => ({ filters: { ...state.filters, ...filters } }),
            false,
            'setFilters'
          ),

        resetFilters: () =>
          set({ filters: initialFilters }, false, 'resetFilters'),

        setViewMode: (mode) =>
          set({ viewMode: mode }, false, 'setViewMode'),
      }),
      {
        name: 'construction-store',
        partialize: (state) => ({ viewMode: state.viewMode }),
      }
    ),
    { name: 'ConstructionStore' }
  )
);
```

### 6. Page Pattern

```tsx
// pages/ProjectsPage.tsx
import { FC, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
  PageHeader,
  SearchInput,
  FilterBar,
  Pagination,
  LoadingSpinner,
  ErrorMessage,
  EmptyState
} from '@/shared/components';
import { useProjects, useCreateProject } from '../hooks/useProjects';
import { useConstructionStore } from '../store/construction.store';
import { ProjectList, ProjectGrid, ProjectKanban } from '../components';
import { CreateProjectModal } from '../components/CreateProjectModal';

export const ProjectsPage: FC = () => {
  const navigate = useNavigate();
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);

  const { filters, viewMode, setFilters, setViewMode } = useConstructionStore();
  const { data, isLoading, error, refetch } = useProjects(filters);
  const createProject = useCreateProject();

  const handleProjectClick = (id: string) => {
    navigate(`/projects/${id}`);
  };

  const handleCreateProject = async (dto: CreateProjectDto) => {
    await createProject.mutateAsync(dto);
    setIsCreateModalOpen(false);
  };

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} onRetry={refetch} />;

  return (
    <div className="space-y-6">
      <PageHeader
        title="Proyectos"
        subtitle={`${data?.total || 0} proyectos encontrados`}
        actions={
          <Button onClick={() => setIsCreateModalOpen(true)}>
            + Nuevo Proyecto
          </Button>
        }
      />

      <div className="flex gap-4 items-center">
        <SearchInput
          value={filters.search}
          onChange={(search) => setFilters({ search })}
          placeholder="Buscar proyectos..."
        />
        <FilterBar
          filters={filters}
          onChange={setFilters}
          options={filterOptions}
        />
        <ViewToggle value={viewMode} onChange={setViewMode} />
      </div>

      {data?.items.length === 0 ? (
        <EmptyState
          title="Sin proyectos"
          description="No hay proyectos que coincidan con los filtros"
          action={
            <Button onClick={() => setIsCreateModalOpen(true)}>
              Crear primer proyecto
            </Button>
          }
        />
      ) : (
        <>
          {viewMode === 'list' && (
            <ProjectList projects={data.items} onClick={handleProjectClick} />
          )}
          {viewMode === 'grid' && (
            <ProjectGrid projects={data.items} onClick={handleProjectClick} />
          )}
          {viewMode === 'kanban' && (
            <ProjectKanban projects={data.items} onClick={handleProjectClick} />
          )}

          <Pagination
            total={data.total}
            page={filters.page}
            limit={filters.limit}
            onChange={(page) => setFilters({ page })}
          />
        </>
      )}

      <CreateProjectModal
        isOpen={isCreateModalOpen}
        onClose={() => setIsCreateModalOpen(false)}
        onSubmit={handleCreateProject}
        isLoading={createProject.isPending}
      />
    </div>
  );
};
```

---

## Componentes Compartidos

### UI Components

| Componente | Descripcion | Props |
|------------|-------------|-------|
| Button | Boton con variantes | variant, size, disabled, loading |
| Card | Contenedor con sombra | className, onClick, children |
| Badge | Etiqueta de estado | variant, size, children |
| Modal | Dialogo modal | isOpen, onClose, title, children |
| Tooltip | Tooltip informativo | content, position, children |
| Dropdown | Menu desplegable | items, trigger, onSelect |
| Avatar | Avatar de usuario | src, name, size |
| Progress | Barra de progreso | value, max, variant |
| Spinner | Indicador de carga | size, color |
| Alert | Mensaje de alerta | type, title, message, onClose |

### Form Components

| Componente | Descripcion | Props |
|------------|-------------|-------|
| Input | Campo de texto | label, error, helper, ...inputProps |
| Select | Selector | options, label, error, onChange |
| Checkbox | Casilla de verificacion | label, checked, onChange |
| Radio | Boton de radio | options, value, onChange |
| DatePicker | Selector de fecha | value, onChange, format |
| FileUpload | Carga de archivos | accept, maxSize, onUpload |
| TextArea | Area de texto | label, rows, maxLength |
| NumberInput | Campo numerico | min, max, step, format |
| CurrencyInput | Campo de moneda | currency, locale, onChange |

### Table Components

| Componente | Descripcion | Props |
|------------|-------------|-------|
| DataTable | Tabla de datos | columns, data, onSort, onFilter |
| TablePagination | Paginacion de tabla | total, page, limit, onChange |
| TableFilters | Filtros de tabla | filters, onChange |
| TableActions | Acciones de fila | actions, row |
| ExportButton | Exportar datos | formats, onExport |

### Chart Components

| Componente | Descripcion | Props |
|------------|-------------|-------|
| LineChart | Grafico de lineas | data, xKey, yKey, options |
| BarChart | Grafico de barras | data, xKey, yKey, options |
| PieChart | Grafico circular | data, nameKey, valueKey |
| GaugeChart | Indicador circular | value, max, thresholds |
| AreaChart | Grafico de area | data, xKey, yKey, options |

---

## Modulos Frontend

| Modulo | Spec Componentes | Spec Paginas | Estado |
|--------|------------------|--------------|--------|
| shared | [COMP-shared.md](components/COMP-shared.md) | - | Documentado |
| construction | [COMP-construction.md](components/COMP-construction.md) | [PAGES-construction.md](pages/PAGES-construction.md) | Documentado |
| compliance | [COMP-compliance.md](components/COMP-compliance.md) | [PAGES-compliance.md](pages/PAGES-compliance.md) | Documentado |
| finance | [COMP-finance.md](components/COMP-finance.md) | [PAGES-finance.md](pages/PAGES-finance.md) | Documentado |
| assets | [COMP-assets.md](components/COMP-assets.md) | [PAGES-assets.md](pages/PAGES-assets.md) | Documentado |
| documents | [COMP-documents.md](components/COMP-documents.md) | [PAGES-documents.md](pages/PAGES-documents.md) | Documentado |

---

## Routing

### Estructura de Rutas

```tsx
// app/router.tsx
import { createBrowserRouter } from 'react-router-dom';
import { MainLayout } from '@/layouts/MainLayout';
import { AuthLayout } from '@/layouts/AuthLayout';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <MainLayout />,
    children: [
      // Dashboard
      { index: true, element: <DashboardPage /> },

      // Construction
      { path: 'projects', element: <ProjectsPage /> },
      { path: 'projects/:id', element: <ProjectDetailPage /> },
      { path: 'projects/:id/budget', element: <BudgetPage /> },
      { path: 'projects/:id/progress', element: <ProgressPage /> },
      { path: 'developments', element: <DevelopmentsPage /> },
      { path: 'developments/:id', element: <DevelopmentDetailPage /> },

      // Compliance
      { path: 'compliance', element: <ComplianceDashboardPage /> },
      { path: 'compliance/programs', element: <ProgramsPage /> },
      { path: 'compliance/project/:id', element: <ProjectCompliancePage /> },
      { path: 'compliance/audits', element: <AuditsPage /> },
      { path: 'compliance/audits/:id', element: <AuditDetailPage /> },

      // Finance
      { path: 'finance', element: <FinanceDashboardPage /> },
      { path: 'finance/accounting', element: <AccountingPage /> },
      { path: 'finance/payables', element: <PayablesPage /> },
      { path: 'finance/receivables', element: <ReceivablesPage /> },
      { path: 'finance/cash-flow', element: <CashFlowPage /> },
      { path: 'finance/bank', element: <BankReconciliationPage /> },
      { path: 'finance/reports', element: <FinanceReportsPage /> },

      // Assets
      { path: 'assets', element: <AssetsPage /> },
      { path: 'assets/:id', element: <AssetDetailPage /> },
      { path: 'assets/maintenance', element: <MaintenancePage /> },
      { path: 'assets/work-orders', element: <WorkOrdersPage /> },
      { path: 'assets/tracking', element: <TrackingMapPage /> },

      // Documents
      { path: 'documents', element: <DocumentsPage /> },
      { path: 'documents/folder/:id', element: <FolderPage /> },
      { path: 'documents/plans', element: <PlansPage /> },
      { path: 'documents/approvals', element: <ApprovalsPage /> },

      // Settings
      { path: 'settings', element: <SettingsPage /> },
    ],
  },
  {
    path: '/auth',
    element: <AuthLayout />,
    children: [
      { path: 'login', element: <LoginPage /> },
      { path: 'forgot-password', element: <ForgotPasswordPage /> },
      { path: 'reset-password', element: <ResetPasswordPage /> },
    ],
  },
]);
```

---

## State Management

### Global State (Zustand)

```
stores/
├── auth.store.ts        // Autenticacion y usuario actual
├── tenant.store.ts      // Contexto de tenant
├── ui.store.ts          // Estado de UI (sidebar, theme, etc.)
├── notifications.store.ts // Notificaciones
└── preferences.store.ts // Preferencias de usuario
```

### Server State (TanStack Query)

- Datos de servidor manejados por TanStack Query
- Cache automatico con invalidacion
- Prefetching para navegacion rapida
- Optimistic updates para mejor UX

---

## Referencias

- [Backend Specifications](../05-backend-specs/)
- [Domain Models](../04-modelado/domain-models/)
- [Epicas](../08-epicas/)

---

*Ultima actualizacion: 2025-12-05*
