# ET-CATALOG-FRONTEND: Componentes React

## Identificacion

| Campo | Valor |
|-------|-------|
| **ID** | ET-CATALOG-FRONTEND |
| **Modulo** | MGN-005 Catalogs |
| **Version** | 1.0 |
| **Estado** | En Diseno |
| **Framework** | React + TypeScript |
| **UI Library** | shadcn/ui |
| **State** | Zustand |
| **Autor** | Requirements-Analyst |
| **Fecha** | 2025-12-05 |

---

## Descripcion General

Especificacion tecnica del modulo frontend de Catalogs. Incluye paginas, componentes, stores y hooks para la gestion de contactos, catalogos globales y catalogos por tenant.

### Estructura de Archivos

```
apps/frontend/src/modules/catalogs/
├── index.ts
├── routes.tsx
├── pages/
│   ├── ContactsPage.tsx
│   ├── ContactDetailPage.tsx
│   ├── ContactFormPage.tsx
│   ├── CurrenciesPage.tsx
│   ├── UomPage.tsx
│   └── CategoriesPage.tsx
├── components/
│   ├── contacts/
│   │   ├── ContactList.tsx
│   │   ├── ContactCard.tsx
│   │   ├── ContactForm.tsx
│   │   ├── ContactSearch.tsx
│   │   ├── ContactTagBadge.tsx
│   │   └── ContactTypeIcon.tsx
│   ├── currencies/
│   │   ├── CurrencySelect.tsx
│   │   ├── CurrencyRateForm.tsx
│   │   └── CurrencyConverter.tsx
│   ├── countries/
│   │   ├── CountrySelect.tsx
│   │   └── StateSelect.tsx
│   ├── uom/
│   │   ├── UomCategoryList.tsx
│   │   ├── UomList.tsx
│   │   ├── UomForm.tsx
│   │   └── UomConverter.tsx
│   └── categories/
│       ├── CategoryTree.tsx
│       ├── CategoryForm.tsx
│       └── CategorySelect.tsx
├── stores/
│   ├── contacts.store.ts
│   ├── countries.store.ts
│   ├── currencies.store.ts
│   └── uom.store.ts
├── hooks/
│   ├── useContacts.ts
│   ├── useCountries.ts
│   ├── useCurrencies.ts
│   ├── useUom.ts
│   └── useCategories.ts
├── services/
│   ├── contacts.service.ts
│   ├── countries.service.ts
│   ├── currencies.service.ts
│   ├── uom.service.ts
│   └── categories.service.ts
└── types/
    ├── contact.types.ts
    ├── country.types.ts
    ├── currency.types.ts
    ├── uom.types.ts
    └── category.types.ts
```

---

## Types

### Contact Types

```typescript
// types/contact.types.ts

export enum ContactType {
  COMPANY = 'company',
  INDIVIDUAL = 'individual',
  CONTACT = 'contact'
}

export enum ContactRole {
  CUSTOMER = 'customer',
  VENDOR = 'vendor',
  EMPLOYEE = 'employee',
  OTHER = 'other'
}

export interface Contact {
  id: string;
  tenantId: string;
  parentId?: string;
  parent?: Contact;
  children?: Contact[];
  name: string;
  displayName?: string;
  contactType: ContactType;
  roles: ContactRole[];
  ref?: string;
  vat?: string;
  email?: string;
  phone?: string;
  mobile?: string;
  website?: string;
  street?: string;
  street2?: string;
  city?: string;
  zip?: string;
  stateId?: string;
  state?: State;
  countryId?: string;
  country?: Country;
  function?: string;
  userId?: string;
  currencyId?: string;
  currency?: Currency;
  paymentTermDays?: number;
  creditLimit?: number;
  bankAccounts?: BankAccount[];
  notes?: string;
  isActive: boolean;
  tags?: ContactTag[];
  createdAt: string;
  updatedAt: string;
}

export interface ContactTag {
  id: string;
  tenantId: string;
  parentId?: string;
  name: string;
  color: number;
  displayName?: string;
  isActive: boolean;
}

export interface BankAccount {
  bankName: string;
  accountNumber: string;
  accountType: string;
  clabe?: string;
}

export interface CreateContactDto {
  name: string;
  contactType: ContactType;
  roles?: ContactRole[];
  parentId?: string;
  ref?: string;
  vat?: string;
  email?: string;
  phone?: string;
  mobile?: string;
  website?: string;
  address?: {
    street?: string;
    street2?: string;
    city?: string;
    zip?: string;
    stateId?: string;
    countryId?: string;
  };
  function?: string;
  currencyId?: string;
  paymentTermDays?: number;
  creditLimit?: number;
  tagIds?: string[];
}

export interface ContactFilters {
  type?: ContactType;
  roles?: ContactRole[];
  search?: string;
  isCustomer?: boolean;
  isVendor?: boolean;
  tags?: string[];
  includeChildren?: boolean;
  page?: number;
  limit?: number;
}
```

### Currency Types

```typescript
// types/currency.types.ts

export interface Currency {
  id: string;
  name: string; // ISO code
  fullName: string;
  symbol: string;
  decimalPlaces: number;
  rounding: number;
  position: 'before' | 'after';
  isActive: boolean;
}

export interface CurrencyRate {
  id: string;
  tenantId: string;
  currencyId: string;
  currency?: Currency;
  rate: number;
  name: string; // date
  createdAt: string;
}

export interface ConversionResult {
  from: { currencyId: string; amount: number };
  to: { currencyId: string; amount: number };
  rate: number;
  date: string;
}
```

### UoM Types

```typescript
// types/uom.types.ts

export enum UomType {
  REFERENCE = 'reference',
  BIGGER = 'bigger',
  SMALLER = 'smaller'
}

export interface UomCategory {
  id: string;
  tenantId: string;
  name: string;
  isActive: boolean;
  uomCount?: number;
}

export interface Uom {
  id: string;
  tenantId: string;
  categoryId: string;
  category?: UomCategory;
  name: string;
  uomType: UomType;
  factor: number;
  rounding: number;
  isProtected: boolean;
  isActive: boolean;
}

export interface UomConversionResult {
  from: { uomId: string; quantity: number };
  to: { uomId: string; quantity: number };
  factor: number;
}
```

---

## Stores (Zustand)

### Contacts Store

```typescript
// stores/contacts.store.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { Contact, ContactFilters, CreateContactDto } from '../types/contact.types';
import { contactsService } from '../services/contacts.service';
import { PaginatedResult } from '@core/types/pagination';

interface ContactsState {
  // Data
  contacts: Contact[];
  selectedContact: Contact | null;
  total: number;
  page: number;
  limit: number;

  // UI State
  isLoading: boolean;
  error: string | null;
  filters: ContactFilters;

  // Actions
  fetchContacts: (filters?: ContactFilters) => Promise<void>;
  fetchContact: (id: string) => Promise<void>;
  createContact: (dto: CreateContactDto) => Promise<Contact>;
  updateContact: (id: string, dto: Partial<CreateContactDto>) => Promise<Contact>;
  deleteContact: (id: string) => Promise<void>;
  setFilters: (filters: Partial<ContactFilters>) => void;
  resetFilters: () => void;
  clearError: () => void;
}

const defaultFilters: ContactFilters = {
  page: 1,
  limit: 20,
  search: '',
};

export const useContactsStore = create<ContactsState>()(
  devtools(
    (set, get) => ({
      // Initial state
      contacts: [],
      selectedContact: null,
      total: 0,
      page: 1,
      limit: 20,
      isLoading: false,
      error: null,
      filters: defaultFilters,

      // Actions
      fetchContacts: async (filters?: ContactFilters) => {
        set({ isLoading: true, error: null });
        try {
          const mergedFilters = { ...get().filters, ...filters };
          const result = await contactsService.getAll(mergedFilters);
          set({
            contacts: result.data,
            total: result.meta.total,
            page: result.meta.page,
            limit: result.meta.limit,
            isLoading: false,
            filters: mergedFilters,
          });
        } catch (error: any) {
          set({ error: error.message, isLoading: false });
        }
      },

      fetchContact: async (id: string) => {
        set({ isLoading: true, error: null });
        try {
          const contact = await contactsService.getById(id);
          set({ selectedContact: contact, isLoading: false });
        } catch (error: any) {
          set({ error: error.message, isLoading: false });
        }
      },

      createContact: async (dto: CreateContactDto) => {
        set({ isLoading: true, error: null });
        try {
          const contact = await contactsService.create(dto);
          set((state) => ({
            contacts: [contact, ...state.contacts],
            isLoading: false,
          }));
          return contact;
        } catch (error: any) {
          set({ error: error.message, isLoading: false });
          throw error;
        }
      },

      updateContact: async (id: string, dto: Partial<CreateContactDto>) => {
        set({ isLoading: true, error: null });
        try {
          const contact = await contactsService.update(id, dto);
          set((state) => ({
            contacts: state.contacts.map((c) => (c.id === id ? contact : c)),
            selectedContact: state.selectedContact?.id === id ? contact : state.selectedContact,
            isLoading: false,
          }));
          return contact;
        } catch (error: any) {
          set({ error: error.message, isLoading: false });
          throw error;
        }
      },

      deleteContact: async (id: string) => {
        set({ isLoading: true, error: null });
        try {
          await contactsService.delete(id);
          set((state) => ({
            contacts: state.contacts.filter((c) => c.id !== id),
            selectedContact: state.selectedContact?.id === id ? null : state.selectedContact,
            isLoading: false,
          }));
        } catch (error: any) {
          set({ error: error.message, isLoading: false });
          throw error;
        }
      },

      setFilters: (filters: Partial<ContactFilters>) => {
        set((state) => ({
          filters: { ...state.filters, ...filters, page: 1 },
        }));
      },

      resetFilters: () => {
        set({ filters: defaultFilters });
      },

      clearError: () => {
        set({ error: null });
      },
    }),
    { name: 'contacts-store' }
  )
);
```

### Countries Store

```typescript
// stores/countries.store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Country, State } from '../types/country.types';
import { countriesService } from '../services/countries.service';

interface CountriesState {
  countries: Country[];
  statesByCountry: Record<string, State[]>;
  isLoading: boolean;

  fetchCountries: () => Promise<void>;
  fetchStates: (countryCode: string) => Promise<State[]>;
  getCountryByCode: (code: string) => Country | undefined;
}

export const useCountriesStore = create<CountriesState>()(
  persist(
    (set, get) => ({
      countries: [],
      statesByCountry: {},
      isLoading: false,

      fetchCountries: async () => {
        if (get().countries.length > 0) return; // Cache hit

        set({ isLoading: true });
        try {
          const countries = await countriesService.getAll();
          set({ countries, isLoading: false });
        } catch {
          set({ isLoading: false });
        }
      },

      fetchStates: async (countryCode: string) => {
        const cached = get().statesByCountry[countryCode];
        if (cached) return cached;

        const states = await countriesService.getStates(countryCode);
        set((state) => ({
          statesByCountry: {
            ...state.statesByCountry,
            [countryCode]: states,
          },
        }));
        return states;
      },

      getCountryByCode: (code: string) => {
        return get().countries.find((c) => c.code === code);
      },
    }),
    {
      name: 'countries-cache',
      partialize: (state) => ({
        countries: state.countries,
        statesByCountry: state.statesByCountry,
      }),
    }
  )
);
```

---

## Components

### ContactList

```tsx
// components/contacts/ContactList.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Pagination } from '@/components/ui/pagination';
import { ContactTypeIcon } from './ContactTypeIcon';
import { ContactTagBadge } from './ContactTagBadge';
import { useContactsStore } from '../../stores/contacts.store';
import { Contact, ContactRole } from '../../types/contact.types';

interface ContactListProps {
  onSelect?: (contact: Contact) => void;
}

export function ContactList({ onSelect }: ContactListProps) {
  const navigate = useNavigate();
  const {
    contacts,
    total,
    page,
    limit,
    isLoading,
    filters,
    fetchContacts,
    setFilters,
  } = useContactsStore();

  useEffect(() => {
    fetchContacts();
  }, [filters]);

  const handleRowClick = (contact: Contact) => {
    if (onSelect) {
      onSelect(contact);
    } else {
      navigate(`/catalogs/contacts/${contact.id}`);
    }
  };

  const handlePageChange = (newPage: number) => {
    setFilters({ page: newPage });
  };

  if (isLoading) {
    return (
      <div className="space-y-2">
        {[...Array(5)].map((_, i) => (
          <Skeleton key={i} className="h-12 w-full" />
        ))}
      </div>
    );
  }

  return (
    <div className="space-y-4">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead className="w-8"></TableHead>
            <TableHead>Nombre</TableHead>
            <TableHead>Email</TableHead>
            <TableHead>Telefono</TableHead>
            <TableHead>Tipo</TableHead>
            <TableHead>Tags</TableHead>
            <TableHead className="text-right">Acciones</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {contacts.map((contact) => (
            <TableRow
              key={contact.id}
              className="cursor-pointer hover:bg-muted/50"
              onClick={() => handleRowClick(contact)}
            >
              <TableCell>
                <ContactTypeIcon type={contact.contactType} />
              </TableCell>
              <TableCell>
                <div>
                  <div className="font-medium">{contact.name}</div>
                  {contact.vat && (
                    <div className="text-sm text-muted-foreground">
                      {contact.vat}
                    </div>
                  )}
                </div>
              </TableCell>
              <TableCell>{contact.email}</TableCell>
              <TableCell>{contact.phone || contact.mobile}</TableCell>
              <TableCell>
                <div className="flex gap-1">
                  {contact.roles.includes(ContactRole.CUSTOMER) && (
                    <Badge variant="secondary">Cliente</Badge>
                  )}
                  {contact.roles.includes(ContactRole.VENDOR) && (
                    <Badge variant="outline">Proveedor</Badge>
                  )}
                </div>
              </TableCell>
              <TableCell>
                <div className="flex gap-1">
                  {contact.tags?.slice(0, 2).map((tag) => (
                    <ContactTagBadge key={tag.id} tag={tag} />
                  ))}
                  {(contact.tags?.length ?? 0) > 2 && (
                    <Badge variant="secondary">
                      +{contact.tags!.length - 2}
                    </Badge>
                  )}
                </div>
              </TableCell>
              <TableCell className="text-right">
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={(e) => {
                    e.stopPropagation();
                    navigate(`/catalogs/contacts/${contact.id}/edit`);
                  }}
                >
                  Editar
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>

      <Pagination
        currentPage={page}
        totalPages={Math.ceil(total / limit)}
        onPageChange={handlePageChange}
      />
    </div>
  );
}
```

### ContactForm

```tsx
// components/contacts/ContactForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CountrySelect } from '../countries/CountrySelect';
import { StateSelect } from '../countries/StateSelect';
import { CurrencySelect } from '../currencies/CurrencySelect';
import { Contact, ContactType, ContactRole, CreateContactDto } from '../../types/contact.types';

const contactSchema = z.object({
  name: z.string().min(1, 'Nombre requerido').max(255),
  contactType: z.nativeEnum(ContactType),
  roles: z.array(z.nativeEnum(ContactRole)).optional(),
  parentId: z.string().uuid().optional().nullable(),
  ref: z.string().max(50).optional(),
  vat: z.string().max(50).optional(),
  email: z.string().email('Email invalido').optional().or(z.literal('')),
  phone: z.string().max(50).optional(),
  mobile: z.string().max(50).optional(),
  website: z.string().url('URL invalida').optional().or(z.literal('')),
  street: z.string().max(255).optional(),
  street2: z.string().max(255).optional(),
  city: z.string().max(100).optional(),
  zip: z.string().max(20).optional(),
  stateId: z.string().uuid().optional().nullable(),
  countryId: z.string().uuid().optional().nullable(),
  function: z.string().max(100).optional(),
  currencyId: z.string().uuid().optional().nullable(),
  paymentTermDays: z.number().min(0).optional(),
  creditLimit: z.number().min(0).optional(),
  notes: z.string().optional(),
});

type ContactFormData = z.infer<typeof contactSchema>;

interface ContactFormProps {
  contact?: Contact;
  onSubmit: (data: CreateContactDto) => Promise<void>;
  onCancel: () => void;
  isLoading?: boolean;
}

export function ContactForm({
  contact,
  onSubmit,
  onCancel,
  isLoading,
}: ContactFormProps) {
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      name: contact?.name ?? '',
      contactType: contact?.contactType ?? ContactType.COMPANY,
      roles: contact?.roles ?? [],
      parentId: contact?.parentId,
      ref: contact?.ref ?? '',
      vat: contact?.vat ?? '',
      email: contact?.email ?? '',
      phone: contact?.phone ?? '',
      mobile: contact?.mobile ?? '',
      website: contact?.website ?? '',
      street: contact?.street ?? '',
      street2: contact?.street2 ?? '',
      city: contact?.city ?? '',
      zip: contact?.zip ?? '',
      stateId: contact?.stateId,
      countryId: contact?.countryId,
      function: contact?.function ?? '',
      currencyId: contact?.currencyId,
      paymentTermDays: contact?.paymentTermDays,
      creditLimit: contact?.creditLimit,
      notes: contact?.notes ?? '',
    },
  });

  const contactType = form.watch('contactType');
  const countryId = form.watch('countryId');

  const handleSubmit = async (data: ContactFormData) => {
    const dto: CreateContactDto = {
      ...data,
      address: {
        street: data.street,
        street2: data.street2,
        city: data.city,
        zip: data.zip,
        stateId: data.stateId ?? undefined,
        countryId: data.countryId ?? undefined,
      },
    };
    await onSubmit(dto);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
        <Tabs defaultValue="general">
          <TabsList>
            <TabsTrigger value="general">General</TabsTrigger>
            <TabsTrigger value="address">Direccion</TabsTrigger>
            <TabsTrigger value="commercial">Comercial</TabsTrigger>
          </TabsList>

          <TabsContent value="general" className="space-y-4">
            <div className="grid grid-cols-2 gap-4">
              <FormField
                control={form.control}
                name="name"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Nombre *</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="Nombre del contacto" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="contactType"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Tipo *</FormLabel>
                    <Select
                      onValueChange={field.onChange}
                      defaultValue={field.value}
                    >
                      <FormControl>
                        <SelectTrigger>
                          <SelectValue placeholder="Selecciona tipo" />
                        </SelectTrigger>
                      </FormControl>
                      <SelectContent>
                        <SelectItem value={ContactType.COMPANY}>
                          Empresa
                        </SelectItem>
                        <SelectItem value={ContactType.INDIVIDUAL}>
                          Persona
                        </SelectItem>
                        <SelectItem value={ContactType.CONTACT}>
                          Contacto de Empresa
                        </SelectItem>
                      </SelectContent>
                    </Select>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>

            <FormField
              control={form.control}
              name="roles"
              render={() => (
                <FormItem>
                  <FormLabel>Roles</FormLabel>
                  <div className="flex gap-4">
                    {Object.values(ContactRole).map((role) => (
                      <FormField
                        key={role}
                        control={form.control}
                        name="roles"
                        render={({ field }) => (
                          <FormItem className="flex items-center space-x-2">
                            <FormControl>
                              <Checkbox
                                checked={field.value?.includes(role)}
                                onCheckedChange={(checked) => {
                                  const current = field.value ?? [];
                                  if (checked) {
                                    field.onChange([...current, role]);
                                  } else {
                                    field.onChange(
                                      current.filter((r) => r !== role)
                                    );
                                  }
                                }}
                              />
                            </FormControl>
                            <FormLabel className="!mt-0 capitalize">
                              {role === 'customer'
                                ? 'Cliente'
                                : role === 'vendor'
                                ? 'Proveedor'
                                : role === 'employee'
                                ? 'Empleado'
                                : 'Otro'}
                            </FormLabel>
                          </FormItem>
                        )}
                      />
                    ))}
                  </div>
                </FormItem>
              )}
            />

            <div className="grid grid-cols-2 gap-4">
              <FormField
                control={form.control}
                name="vat"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>RFC / NIT</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="Identificador fiscal" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="ref"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Codigo Interno</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="Referencia interna" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>

            <div className="grid grid-cols-2 gap-4">
              <FormField
                control={form.control}
                name="email"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Email</FormLabel>
                    <FormControl>
                      <Input
                        {...field}
                        type="email"
                        placeholder="email@ejemplo.com"
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="phone"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Telefono</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="+52 55 1234 5678" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>

            {contactType === ContactType.CONTACT && (
              <FormField
                control={form.control}
                name="function"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Cargo / Puesto</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="Director Comercial" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            )}
          </TabsContent>

          <TabsContent value="address" className="space-y-4">
            <FormField
              control={form.control}
              name="street"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Calle</FormLabel>
                  <FormControl>
                    <Input {...field} placeholder="Av. Principal 123" />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="street2"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Linea 2</FormLabel>
                  <FormControl>
                    <Input {...field} placeholder="Colonia, Interior, etc." />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <div className="grid grid-cols-3 gap-4">
              <FormField
                control={form.control}
                name="city"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Ciudad</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="Ciudad" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="zip"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Codigo Postal</FormLabel>
                    <FormControl>
                      <Input {...field} placeholder="06600" />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="countryId"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Pais</FormLabel>
                    <FormControl>
                      <CountrySelect
                        value={field.value ?? undefined}
                        onChange={field.onChange}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>

            {countryId && (
              <FormField
                control={form.control}
                name="stateId"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Estado / Provincia</FormLabel>
                    <FormControl>
                      <StateSelect
                        countryId={countryId}
                        value={field.value ?? undefined}
                        onChange={field.onChange}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            )}
          </TabsContent>

          <TabsContent value="commercial" className="space-y-4">
            <div className="grid grid-cols-2 gap-4">
              <FormField
                control={form.control}
                name="currencyId"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Moneda Preferida</FormLabel>
                    <FormControl>
                      <CurrencySelect
                        value={field.value ?? undefined}
                        onChange={field.onChange}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="paymentTermDays"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Dias de Credito</FormLabel>
                    <FormControl>
                      <Input
                        {...field}
                        type="number"
                        min={0}
                        onChange={(e) =>
                          field.onChange(parseInt(e.target.value) || 0)
                        }
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>

            <FormField
              control={form.control}
              name="creditLimit"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Limite de Credito</FormLabel>
                  <FormControl>
                    <Input
                      {...field}
                      type="number"
                      min={0}
                      step="0.01"
                      onChange={(e) =>
                        field.onChange(parseFloat(e.target.value) || 0)
                      }
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="notes"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Notas</FormLabel>
                  <FormControl>
                    <Textarea
                      {...field}
                      placeholder="Notas adicionales..."
                      rows={4}
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
          </TabsContent>
        </Tabs>

        <div className="flex justify-end gap-2">
          <Button type="button" variant="outline" onClick={onCancel}>
            Cancelar
          </Button>
          <Button type="submit" disabled={isLoading}>
            {isLoading ? 'Guardando...' : contact ? 'Actualizar' : 'Crear'}
          </Button>
        </div>
      </form>
    </Form>
  );
}
```

### CurrencySelect

```tsx
// components/currencies/CurrencySelect.tsx
import { useEffect } from 'react';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { useCurrenciesStore } from '../../stores/currencies.store';

interface CurrencySelectProps {
  value?: string;
  onChange: (value: string) => void;
  placeholder?: string;
  disabled?: boolean;
}

export function CurrencySelect({
  value,
  onChange,
  placeholder = 'Selecciona moneda',
  disabled,
}: CurrencySelectProps) {
  const { currencies, isLoading, fetchCurrencies } = useCurrenciesStore();

  useEffect(() => {
    fetchCurrencies();
  }, []);

  return (
    <Select value={value} onValueChange={onChange} disabled={disabled || isLoading}>
      <SelectTrigger>
        <SelectValue placeholder={placeholder} />
      </SelectTrigger>
      <SelectContent>
        {currencies.map((currency) => (
          <SelectItem key={currency.id} value={currency.id}>
            {currency.symbol} {currency.name} - {currency.fullName}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}
```

---

## Pages

### ContactsPage

```tsx
// pages/ContactsPage.tsx
import { useNavigate } from 'react-router-dom';
import { Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ContactList } from '../components/contacts/ContactList';
import { ContactSearch } from '../components/contacts/ContactSearch';
import { useContactsStore } from '../stores/contacts.store';

export function ContactsPage() {
  const navigate = useNavigate();
  const { filters, setFilters } = useContactsStore();

  return (
    <div className="container mx-auto py-6 space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold">Contactos</h1>
          <p className="text-muted-foreground">
            Gestiona clientes, proveedores y otros contactos
          </p>
        </div>
        <Button onClick={() => navigate('/catalogs/contacts/new')}>
          <Plus className="mr-2 h-4 w-4" />
          Nuevo Contacto
        </Button>
      </div>

      <Card>
        <CardHeader>
          <CardTitle>Buscar y Filtrar</CardTitle>
        </CardHeader>
        <CardContent>
          <ContactSearch
            filters={filters}
            onFiltersChange={setFilters}
          />
        </CardContent>
      </Card>

      <Card>
        <CardContent className="pt-6">
          <ContactList />
        </CardContent>
      </Card>
    </div>
  );
}
```

---

## Routes

```tsx
// routes.tsx
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';

const ContactsPage = lazy(() => import('./pages/ContactsPage'));
const ContactDetailPage = lazy(() => import('./pages/ContactDetailPage'));
const ContactFormPage = lazy(() => import('./pages/ContactFormPage'));
const CurrenciesPage = lazy(() => import('./pages/CurrenciesPage'));
const UomPage = lazy(() => import('./pages/UomPage'));
const CategoriesPage = lazy(() => import('./pages/CategoriesPage'));

export const catalogsRoutes: RouteObject[] = [
  {
    path: 'catalogs',
    children: [
      {
        path: 'contacts',
        element: <ContactsPage />,
      },
      {
        path: 'contacts/new',
        element: <ContactFormPage />,
      },
      {
        path: 'contacts/:id',
        element: <ContactDetailPage />,
      },
      {
        path: 'contacts/:id/edit',
        element: <ContactFormPage />,
      },
      {
        path: 'currencies',
        element: <CurrenciesPage />,
      },
      {
        path: 'uom',
        element: <UomPage />,
      },
      {
        path: 'categories',
        element: <CategoriesPage />,
      },
    ],
  },
];
```

---

## Wireframes

### Lista de Contactos

```
+------------------------------------------------------------------+
| Contactos                                            [+ Nuevo]   |
+------------------------------------------------------------------+
| Buscar y Filtrar                                                 |
| +--------------------------------------------------------------+ |
| | [Buscar...                    ] [Tipo ▼] [Cliente] [Proveedor]| |
| | [Tags: VIP, Preferido ×]                                      | |
| +--------------------------------------------------------------+ |
+------------------------------------------------------------------+
| Nombre          | Email              | Tel      | Tipo    | Tags |
|-----------------|--------------------+----------|---------|------|
| 🏢 Empresa Demo | contacto@demo.com  | 55-1234  | Cliente | VIP  |
| 👤 Juan Perez   | juan@empresa.com   | 55-5678  | Empleado|      |
| 🏢 Proveedor X  | ventas@provx.com   | 55-9012  | Proveedor| Pref|
|-----------------|--------------------+----------|---------|------|
| [< Anterior]  Pagina 1 de 10  [Siguiente >]                      |
+------------------------------------------------------------------+
```

### Formulario de Contacto

```
+------------------------------------------------------------------+
| Nuevo Contacto                                                    |
+------------------------------------------------------------------+
| [General] [Direccion] [Comercial]                                |
+------------------------------------------------------------------+
| General                                                          |
| +--------------------------------------------------------------+ |
| | Nombre *              | Tipo *                                | |
| | [Empresa Demo S.A.  ] | [Empresa ▼]                          | |
| |                       |                                       | |
| | Roles                                                         | |
| | [x] Cliente  [ ] Proveedor  [ ] Empleado  [ ] Otro           | |
| |                                                               | |
| | RFC / NIT             | Codigo Interno                        | |
| | [ABC123456789       ] | [CLI-001       ]                     | |
| |                       |                                       | |
| | Email                 | Telefono                              | |
| | [contacto@demo.com  ] | [+52 55 1234 5678]                   | |
| +--------------------------------------------------------------+ |
+------------------------------------------------------------------+
|                                        [Cancelar] [Guardar]      |
+------------------------------------------------------------------+
```

---

## Historial de Cambios

| Version | Fecha | Autor | Cambios |
|---------|-------|-------|---------|
| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial |

---

## Aprobaciones

| Rol | Nombre | Fecha | Firma |
|-----|--------|-------|-------|
| Frontend Lead | - | - | [ ] |
| UX Designer | - | - | [ ] |
