From 62251228322402745a97d25aac51a1c5eb35e971 Mon Sep 17 00:00:00 2001 From: acazares Date: Wed, 12 Nov 2025 16:08:42 -0600 Subject: [PATCH] feat: Implement client and provider management dashboard - Added data table component for displaying clients and providers with infinite scroll functionality. - Created dropdown actions for each client/provider including copy ID, copy RFC, view details, edit, toggle status, and delete. - Implemented dialogs for creating, editing, viewing details, and deleting clients/providers. - Integrated API calls for fetching, creating, editing, deleting, and toggling status of clients/providers. - Enhanced error handling and loading states for better user experience. - Updated server-side logic to handle pagination and company selection for client/provider data retrieval. --- .../{q/q_classes => fa/fa_classes}/models.py | 2 +- .../s_classes => inv/inv_classes}/models.py | 2 +- backend/api/v1/modules/a76/classes/models.py | 4 +- .../__init__.py | 0 .../dto.py | 10 +- .../models.py | 53 +- .../routes.py | 14 +- .../service.py | 38 +- .../test_client_and_provider.py | 0 .../a76/pedmientos/models/pedimentos.py | 2 +- backend/api/v1/modules/a76/router.py | 2 +- docs/ARCHITECTURE.md | 145 +++++- docs/KEYCLOAK_SETUP.md | 31 +- docs/MODULOS_A76_IMPLEMENTADOS.md | 174 ------- docs/RELATIONSHIPS.md | 107 ---- docs/SCHEMA_A76_UPDATE.md | 126 ----- frontend/messages/en.json | 2 + frontend/messages/es.json | 6 + frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 12 + .../api/dashboard/a76/clients-providers.ts | 202 ++++++++ .../clients_and_providers/columns.ts | 111 +++++ .../create-edit-dialog.svelte | 461 ++++++++++++++++++ .../data-table-actions.svelte | 103 ++++ .../clients_and_providers/data-table.svelte | 123 +++++ .../delete-dialog.svelte | 125 +++++ .../details-dialog.svelte | 217 +++++++++ .../src/lib/components/sidebar/modules.ts | 54 +- .../lib/components/sidebar/nav-main.svelte | 87 ++-- .../clients_and_providers/+page.server.ts | 92 ++++ .../clients_and_providers/+page.svelte | 204 ++++++++ package.json | 5 - 32 files changed, 1974 insertions(+), 543 deletions(-) rename backend/api/v1/modules/a24/{q/q_classes => fa/fa_classes}/models.py (97%) rename backend/api/v1/modules/a24/{s/s_classes => inv/inv_classes}/models.py (95%) rename backend/api/v1/modules/a76/{client_and_provider => clients_and_providers}/__init__.py (100%) rename backend/api/v1/modules/a76/{client_and_provider => clients_and_providers}/dto.py (96%) rename backend/api/v1/modules/a76/{client_and_provider => clients_and_providers}/models.py (73%) rename backend/api/v1/modules/a76/{client_and_provider => clients_and_providers}/routes.py (93%) rename backend/api/v1/modules/a76/{client_and_provider => clients_and_providers}/service.py (94%) rename backend/api/v1/modules/a76/{client_and_provider => clients_and_providers}/test_client_and_provider.py (100%) delete mode 100644 docs/MODULOS_A76_IMPLEMENTADOS.md delete mode 100644 docs/RELATIONSHIPS.md delete mode 100644 docs/SCHEMA_A76_UPDATE.md create mode 100644 frontend/src/lib/api/dashboard/a76/clients-providers.ts create mode 100644 frontend/src/lib/components/dashboard/clients_and_providers/columns.ts create mode 100644 frontend/src/lib/components/dashboard/clients_and_providers/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte create mode 100644 frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/clients_and_providers/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/clients_and_providers/details-dialog.svelte create mode 100644 frontend/src/routes/dashboard/clients_and_providers/+page.server.ts create mode 100644 frontend/src/routes/dashboard/clients_and_providers/+page.svelte delete mode 100644 package.json diff --git a/backend/api/v1/modules/a24/q/q_classes/models.py b/backend/api/v1/modules/a24/fa/fa_classes/models.py similarity index 97% rename from backend/api/v1/modules/a24/q/q_classes/models.py rename to backend/api/v1/modules/a24/fa/fa_classes/models.py index 98f99c73..af307830 100644 --- a/backend/api/v1/modules/a24/q/q_classes/models.py +++ b/backend/api/v1/modules/a24/fa/fa_classes/models.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import Mapped, mapped_column class QClasses(Base, TenantScopedMixin): - __tablename__ = "q_classes" # QClases + __tablename__ = "fa_classes" # QClases __table_args__ = ( PrimaryKeyConstraint("id", name="qclases_pk"), ForeignKeyConstraint( diff --git a/backend/api/v1/modules/a24/s/s_classes/models.py b/backend/api/v1/modules/a24/inv/inv_classes/models.py similarity index 95% rename from backend/api/v1/modules/a24/s/s_classes/models.py rename to backend/api/v1/modules/a24/inv/inv_classes/models.py index f128c2b7..578b9a64 100644 --- a/backend/api/v1/modules/a24/s/s_classes/models.py +++ b/backend/api/v1/modules/a24/inv/inv_classes/models.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column class SClasses(Base, TenantScopedMixin): - __tablename__ = "s_classes" # SClases + __tablename__ = "inv_classes" # SClases __table_args__ = ( ForeignKeyConstraint( ["tenant_id"], ["a76.tenants.id"], name="fk_sclasses_tenants" diff --git a/backend/api/v1/modules/a76/classes/models.py b/backend/api/v1/modules/a76/classes/models.py index 4ae1d80b..54188b67 100644 --- a/backend/api/v1/modules/a76/classes/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -37,7 +37,7 @@ class Class(Base, TenantScopedMixin): ["company_id"], ["a76.company.id"], name="fk_classes_company" ), ForeignKeyConstraint( - ["client_id"], ["a76.client_provider.id"], name="fk_classes_client" + ["client_id"], ["a76.clients_and_providers.id"], name="fk_classes_client" ), ForeignKeyConstraint( ["material_key"], @@ -48,7 +48,7 @@ class Class(Base, TenantScopedMixin): "tenant_id", "company_id", "class_code", - name="uq_classes_client_id_class_code", + name="ufa_classes_client_id_class_code", ), {"schema": "a76"}, ) diff --git a/backend/api/v1/modules/a76/client_and_provider/__init__.py b/backend/api/v1/modules/a76/clients_and_providers/__init__.py similarity index 100% rename from backend/api/v1/modules/a76/client_and_provider/__init__.py rename to backend/api/v1/modules/a76/clients_and_providers/__init__.py diff --git a/backend/api/v1/modules/a76/client_and_provider/dto.py b/backend/api/v1/modules/a76/clients_and_providers/dto.py similarity index 96% rename from backend/api/v1/modules/a76/client_and_provider/dto.py rename to backend/api/v1/modules/a76/clients_and_providers/dto.py index 0cb1b7bf..10fa8858 100644 --- a/backend/api/v1/modules/a76/client_and_provider/dto.py +++ b/backend/api/v1/modules/a76/clients_and_providers/dto.py @@ -4,7 +4,7 @@ Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ from decimal import Decimal -from typing import Optional +from typing import Literal, Optional from pydantic import BaseModel, Field @@ -100,8 +100,8 @@ class ClientProviderCreateDTO(BaseModel): short_name: Optional[str] = Field(None, max_length=10, description="Short name") rfc: Optional[str] = Field(None, max_length=30, description="RFC") curp: Optional[str] = Field(None, max_length=19, description="CURP") - client_or_provider: Optional[str] = Field( - None, max_length=1, description="Client or provider" + client_or_provider: Optional[Literal["client", "provider", "both"]] = Field( + None, description="Client or provider" ) linking: Optional[str] = Field(None, max_length=1, description="Linking") transform_subassembly: Optional[str] = Field( @@ -143,8 +143,8 @@ class ClientProviderUpdateDTO(BaseModel): short_name: Optional[str] = Field(None, max_length=10, description="Short name") rfc: Optional[str] = Field(None, max_length=30, description="RFC") curp: Optional[str] = Field(None, max_length=19, description="CURP") - client_or_provider: Optional[str] = Field( - None, max_length=1, description="Client or provider" + client_or_provider: Optional[Literal["client", "provider", "both"]] = Field( + None, description="Client or provider" ) linking: Optional[str] = Field(None, max_length=1, description="Linking") transform_subassembly: Optional[str] = Field( diff --git a/backend/api/v1/modules/a76/client_and_provider/models.py b/backend/api/v1/modules/a76/clients_and_providers/models.py similarity index 73% rename from backend/api/v1/modules/a76/client_and_provider/models.py rename to backend/api/v1/modules/a76/clients_and_providers/models.py index ed97f149..da2a02eb 100644 --- a/backend/api/v1/modules/a76/client_and_provider/models.py +++ b/backend/api/v1/modules/a76/clients_and_providers/models.py @@ -15,8 +15,15 @@ from sqlalchemy import ( PrimaryKeyConstraint, SmallInteger, String, + Enum as PgEnum ) from sqlalchemy.orm import Mapped, mapped_column, relationship +from enum import Enum + +class ClientOrProviderEnum(str, Enum): + CLIENT = "client" + PROVIDER = "provider" + BOTH = "both" class ClientProvider(Base, TenantScopedMixin): @@ -24,14 +31,14 @@ class ClientProvider(Base, TenantScopedMixin): Modelo para la tabla GClientesPro - Información de clientes y proveedores """ - __tablename__ = "client_provider" + __tablename__ = "clients_and_providers" __table_args__ = ( - PrimaryKeyConstraint("id", name="client_provider_pkey"), + PrimaryKeyConstraint("id", name="clients_and_providers_pkey"), ForeignKeyConstraint( - ["tenant_id"], ["a76.tenants.id"], name="fk_client_provider_tenant" + ["tenant_id"], ["a76.tenants.id"], name="fk_clients_and_providers_tenant" ), ForeignKeyConstraint( - ["company_id"], ["a76.company.id"], name="fk_client_provider_company" + ["company_id"], ["a76.company.id"], name="fk_clients_and_providers_company" ), {"schema": "a76"}, ) @@ -40,14 +47,12 @@ class ClientProvider(Base, TenantScopedMixin): id: Mapped[int] = mapped_column(Integer, primary_key=True) # Basic information - type_nat_foreign: Mapped[Optional[str]] = mapped_column( - String(1) - ) # TIPO NACIONAL/EXTRANJERO + type_nat_foreign: Mapped[Optional[str]] = mapped_column(String(1)) # TIPO NACIONAL/EXTRANJERO name: Mapped[Optional[str]] = mapped_column(String(256)) short_name: Mapped[Optional[str]] = mapped_column(String(10)) rfc: Mapped[Optional[str]] = mapped_column(String(30)) curp: Mapped[Optional[str]] = mapped_column(String(19)) - client_or_provider: Mapped[Optional[str]] = mapped_column(String(1)) + client_or_provider: Mapped[ClientOrProviderEnum] = mapped_column(PgEnum(ClientOrProviderEnum, name="entity_client_or_provider", create_type=True, native_enum=True),nullable=False) linking: Mapped[Optional[str]] = mapped_column(String(1)) transform_subassembly: Mapped[Optional[str]] = mapped_column(String(1)) extra_information: Mapped[Optional[str]] = mapped_column(String(399)) @@ -60,10 +65,10 @@ class ClientProvider(Base, TenantScopedMixin): # Relationships address: Mapped[Optional["ClientProviderAddress"]] = relationship( - back_populates="client_provider", uselist=False, cascade="all, delete-orphan" + back_populates="clients_and_providers", uselist=False, cascade="all, delete-orphan" ) programs: Mapped[Optional["ClientProviderPrograms"]] = relationship( - back_populates="client_provider", uselist=False, cascade="all, delete-orphan" + back_populates="clients_and_providers", uselist=False, cascade="all, delete-orphan" ) @@ -72,17 +77,17 @@ class ClientProviderAddress(Base, TenantScopedMixin): Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores """ - __tablename__ = "client_provider_address" + __tablename__ = "clients_and_providers_address" __table_args__ = ( - PrimaryKeyConstraint("id", name="client_provider_address_pkey"), + PrimaryKeyConstraint("id", name="clients_and_providers_address_pkey"), ForeignKeyConstraint( - ["tenant_id"], ["a76.tenants.id"], name="fk_client_provider_address_tenant" + ["tenant_id"], ["a76.tenants.id"], name="fk_clients_and_providers_address_tenant" ), ForeignKeyConstraint( ["client_id"], - ["a76.client_provider.id"], + ["a76.clients_and_providers.id"], ondelete="CASCADE", - name="fk_client_provider_address_client", + name="fk_clients_and_providers_address_client", ), {"schema": "a76"}, ) @@ -90,7 +95,7 @@ class ClientProviderAddress(Base, TenantScopedMixin): # Primary key (foreign key) id: Mapped[int] = mapped_column(Integer, primary_key=True) client_id: Mapped[int] = mapped_column( - Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE") + Integer, ForeignKey("a76.clients_and_providers.id", ondelete="CASCADE") ) # Address information @@ -110,7 +115,7 @@ class ClientProviderAddress(Base, TenantScopedMixin): reference: Mapped[Optional[str]] = mapped_column(String(250)) # Relationship - client_provider: Mapped["ClientProvider"] = relationship(back_populates="address") + clients_and_providers: Mapped["ClientProvider"] = relationship(back_populates="address") class ClientProviderPrograms(Base, TenantScopedMixin): @@ -118,17 +123,17 @@ class ClientProviderPrograms(Base, TenantScopedMixin): Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores """ - __tablename__ = "client_provider_programs" + __tablename__ = "clients_and_providers_programs" __table_args__ = ( - PrimaryKeyConstraint("id", name="client_provider_programs_pkey"), + PrimaryKeyConstraint("id", name="clients_and_providers_programs_pkey"), ForeignKeyConstraint( - ["tenant_id"], ["a76.tenants.id"], name="fk_client_provider_programs_tenant" + ["tenant_id"], ["a76.tenants.id"], name="fk_clients_and_providers_programs_tenant" ), ForeignKeyConstraint( ["client_id"], - ["a76.client_provider.id"], + ["a76.clients_and_providers.id"], ondelete="CASCADE", - name="fk_client_provider_programs_client", + name="fk_clients_and_providers_programs_client", ), {"schema": "a76"}, ) @@ -136,7 +141,7 @@ class ClientProviderPrograms(Base, TenantScopedMixin): # Primary key (foreign key) id: Mapped[int] = mapped_column(Integer, primary_key=True) client_id: Mapped[int] = mapped_column( - Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE") + Integer, ForeignKey("a76.clients_and_providers.id", ondelete="CASCADE") ) # Program information @@ -162,4 +167,4 @@ class ClientProviderPrograms(Base, TenantScopedMixin): autse_number: Mapped[Optional[str]] = mapped_column(String(300)) # Relationship - client_provider: Mapped["ClientProvider"] = relationship(back_populates="programs") + clients_and_providers: Mapped["ClientProvider"] = relationship(back_populates="programs") diff --git a/backend/api/v1/modules/a76/client_and_provider/routes.py b/backend/api/v1/modules/a76/clients_and_providers/routes.py similarity index 93% rename from backend/api/v1/modules/a76/client_and_provider/routes.py rename to backend/api/v1/modules/a76/clients_and_providers/routes.py index 22d41081..68e74d13 100644 --- a/backend/api/v1/modules/a76/client_and_provider/routes.py +++ b/backend/api/v1/modules/a76/clients_and_providers/routes.py @@ -25,7 +25,7 @@ base_router = TenantCRUDRoutes( create_schema=ClientProviderCreateDTO, update_schema=ClientProviderUpdateDTO, response_schema=ClientProviderResponseDTO, - prefix="/clients-providers", + prefix="", tags=[], resource_name="Client/Provider", id_name="id", # Using numeric ID @@ -51,7 +51,7 @@ async def get_clients_only( db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): - """Get only clients (client_or_provider = 'C')""" + """Get only clients (client_or_provider = 'client')""" tenant_id = validate_access_to_resource(db, company_id, current_user) clients = ( @@ -59,7 +59,7 @@ async def get_clients_only( .filter( ClientProvider.tenant_id == tenant_id, ClientProvider.company_id == company_id, - ClientProvider.client_or_provider == "C" + ClientProvider.client_or_provider == "client" ) .offset(skip) .limit(limit) @@ -76,7 +76,7 @@ async def get_providers_only( db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): - """Get only providers (client_or_provider = 'P')""" + """Get only providers (client_or_provider = 'provider')""" tenant_id = validate_access_to_resource(db, company_id, current_user) providers = ( @@ -84,7 +84,7 @@ async def get_providers_only( .filter( ClientProvider.tenant_id == tenant_id, ClientProvider.company_id == company_id, - ClientProvider.client_or_provider == "P" + ClientProvider.client_or_provider == "provider" ) .offset(skip) .limit(limit) @@ -116,7 +116,7 @@ async def search_by_rfc( @router.patch("/{client_id}/toggle-status", response_model=ClientProviderResponseDTO) -async def toggle_client_provider_status( +async def toggle_clients_and_providers_status( client_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), @@ -142,7 +142,7 @@ async def toggle_client_provider_status( @router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO) -async def get_client_provider_basic_info( +async def get_clients_and_providers_basic_info( client_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), diff --git a/backend/api/v1/modules/a76/client_and_provider/service.py b/backend/api/v1/modules/a76/clients_and_providers/service.py similarity index 94% rename from backend/api/v1/modules/a76/client_and_provider/service.py rename to backend/api/v1/modules/a76/clients_and_providers/service.py index e10789cf..c9bc22e3 100644 --- a/backend/api/v1/modules/a76/client_and_provider/service.py +++ b/backend/api/v1/modules/a76/clients_and_providers/service.py @@ -112,18 +112,20 @@ class ClientProviderService: # Create address if provided if client_data.address: - db_address = ClientProviderAddress( + db_address = ClientProviderAddress( + tenant_id=tenant_id, + company_id=company_id, client_id=db_client.id, - tenant_id=tenant_id, **client_data.address.model_dump(exclude_unset=True), ) db.add(db_address) # Create programs if provided if client_data.programs: - db_programs = ClientProviderPrograms( - client_id=db_client.id, + db_programs = ClientProviderPrograms( tenant_id=tenant_id, + company_id=company_id, + client_id=db_client.id, **client_data.programs.model_dump(exclude_unset=True), ) db.add(db_programs) @@ -229,8 +231,8 @@ class ClientProviderService: # Legacy methods for custom endpoints - def create_client_provider_legacy( - self, client_data: ClientProviderCreateDTO + def create_clients_and_providers_legacy( + self, company_id: int, client_data: ClientProviderCreateDTO ) -> ClientProviderResponseDTO: """Legacy method for creating client/provider""" try: @@ -243,7 +245,8 @@ class ClientProviderService: # Create address if provided if client_data.address: db_address = ClientProviderAddress( - client_id=db_client.client_id, + company_id=company_id, + client_id=db_client.id, **client_data.address.model_dump(exclude_unset=True), ) self.db.add(db_address) @@ -251,14 +254,15 @@ class ClientProviderService: # Create programs if provided if client_data.programs: db_programs = ClientProviderPrograms( - client_id=db_client.client_id, + company_id=company_id, + client_id=db_client.id, **client_data.programs.model_dump(exclude_unset=True), ) self.db.add(db_programs) self.db.commit() - return self._get_client_with_relations(db_client.client_id) + return self._get_client_with_relations(db_client.id) except IntegrityError as e: self.db.rollback() @@ -275,7 +279,7 @@ class ClientProviderService: status_code=500, detail="Error creating client/provider" ) - def get_client_provider( + def get_clients_and_providers( self, client_id: str ) -> Optional[ClientProviderResponseDTO]: """ @@ -321,7 +325,7 @@ class ClientProviderService: skip: Número de registros a omitir limit: Número máximo de registros a retornar search: Texto de búsqueda (nombre, RFC, ID) - client_or_provider: Filtrar por tipo (C=Cliente, P=Proveedor) + client_or_provider: Filtrar por tipo (client=Cliente, provider=Proveedor) enabled_only: Si True, solo retorna activos Returns: @@ -367,7 +371,7 @@ class ClientProviderService: size=len(client_dtos), ) - def update_client_provider( + def update_clients_and_providers( self, client_id: str, client_data: ClientProviderUpdateDTO ) -> Optional[ClientProviderResponseDTO]: """ @@ -447,7 +451,7 @@ class ClientProviderService: status_code=500, detail="Error updating client/provider" ) - def delete_client_provider(self, client_id: str) -> bool: + def delete_clients_and_providers(self, client_id: str) -> bool: """ Elimina un cliente/proveedor @@ -479,9 +483,9 @@ class ClientProviderService: def get_clients_only( self, skip: int = 0, limit: int = 100 ) -> List[ClientProviderBasicDTO]: - """Obtiene solo clientes (C)""" + """Obtiene solo clientes (client)""" query = self.db.query(ClientProvider).filter( - ClientProvider.client_or_provider == "C" + ClientProvider.client_or_provider == "client" ) clients = query.offset(skip).limit(limit).all() return [ClientProviderBasicDTO.model_validate(client) for client in clients] @@ -489,9 +493,9 @@ class ClientProviderService: def get_providers_only( self, skip: int = 0, limit: int = 100 ) -> List[ClientProviderBasicDTO]: - """Obtiene solo proveedores (P)""" + """Obtiene solo proveedores (provider)""" query = self.db.query(ClientProvider).filter( - ClientProvider.client_or_provider == "P" + ClientProvider.client_or_provider == "provider" ) providers = query.offset(skip).limit(limit).all() return [ diff --git a/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py b/backend/api/v1/modules/a76/clients_and_providers/test_client_and_provider.py similarity index 100% rename from backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py rename to backend/api/v1/modules/a76/clients_and_providers/test_client_and_provider.py diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py index 0805d5f4..7a2b5e99 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -72,7 +72,7 @@ class Pedimentos(Base, TenantScopedMixin, TimestampMixin): ["company_id"], ["a76.company.id"], name="fk_pedimentos_company" ), ForeignKeyConstraint( - ["client_id"], ["a76.client_provider.id"], name="fk_pedimentos_client" + ["client_id"], ["a76.clients_and_providers.id"], name="fk_pedimentos_client" ), ForeignKeyConstraint( ["regime"], ["public.pedimento_regimens.code"], name="fk_pedimentos_regime" diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 908fe20f..47083ec0 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -10,7 +10,7 @@ from .customs_brokers.routes import router as customs_broker_router # Importar routers de módulos from .auth import router as auth_router from .classes import router as classes_router -from .client_and_provider import router as client_and_provider_router +from .clients_and_providers import router as client_and_provider_router from .company import router as company_router from .country_rule_oct.routes import router as country_rule_oct_router from .drivers.routes import router as drivers_router diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cc983c94..6e4332ff 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,11 +4,12 @@ 1. [Visión General](#visión-general) 2. [Stack Tecnológico](#stack-tecnológico) 3. [Arquitectura del Sistema](#arquitectura-del-sistema) -4. [Estructura del Proyecto](#estructura-del-proyecto) -5. [Flujos Principales](#flujos-principales) -6. [Seguridad](#seguridad) -7. [Base de Datos](#base-de-datos) -8. [API Reference](#api-reference) +4. [Arquitectura de Schemas y Módulos](#arquitectura-de-schemas-y-módulos) +5. [Estructura del Proyecto](#estructura-del-proyecto) +6. [Flujos Principales](#flujos-principales) +7. [Seguridad](#seguridad) +8. [Base de Datos](#base-de-datos) +9. [API Reference](#api-reference) --- @@ -78,12 +79,12 @@ Anexo76 es una aplicación SaaS multi-tenant para gestión de comercio exterior │ ┌───────▼──────────────────────────┐ │ DATABASE LAYER (Multi-tenant) │ -│ │ -│ ┌──────────┐ ┌──────────────┐ │ -│ │ Core DB │ │ Tenant 1 DB │ │ -│ │ (shared) │ │ (dedicated) │ │ -│ └──────────┘ └──────────────┘ │ -└───────────────────────────────────┘ +│ │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ Core DB │ │ Tenant 1 DB │ │ +│ │ (shared) │ │ (dedicated) │ │ +│ └──────────┘ └──────────────┘ │ +└──────────────────────────────────┘ ``` ### Estructura Modular (por módulo) @@ -123,6 +124,104 @@ modules/{module_name}/ --- +## Arquitectura de Schemas y Módulos + +### Estructura de Schemas en Base de Datos + +La aplicación utiliza una arquitectura de schemas para organizar lógicamente las tablas según su funcionalidad y alcance: + +#### **Schema `a24` (Anexo 24)** +Contiene todas las tablas relacionadas con el **Anexo 24 del SAT** (control de inventarios para empresas IMMEX): +- Gestión de inventarios +- Control de entradas y salidas de mercancías +- Reportes de existencias +- Cumplimiento de obligaciones fiscales del Anexo 24 + +#### **Schema `a76` (Anexo 76)** +Contiene todas las tablas relacionadas con el **Anexo 76 del SAT** (comercio exterior): +- Pedimentos aduanales +- Facturas de importación/exportación +- Documentación de comercio exterior +- Cumplimiento normativo de comercio exterior + +#### **Schema `public` (Catálogos Fijos)** +Contiene **catálogos compartidos** y datos de referencia que no cambian frecuentemente: +- Catálogos del SAT (tipos de material, unidades de medida, etc.) +- Códigos de país +- Catálogos de aduanas +- Tipos de documento +- Datos maestros compartidos entre módulos + +### Convención de Prefijos de Tablas + +Para mantener claridad y trazabilidad, las tablas utilizan prefijos que identifican su módulo funcional: + +#### **Prefijo `inv_` (Inventarios)** +Tablas relacionadas con el **control de inventarios**: +- `inv_products`: Productos en inventario +- `inv_movements`: Movimientos de entrada/salida +- `inv_warehouses`: Almacenes +- `inv_balances`: Saldos de inventario + +**Nota histórica**: Anteriormente se utilizaba el prefijo `s` (SCAII - Sistema de aduanas e Inventarios). + +#### **Prefijo `fa_` (Fixed Assets / Activos Fijos)** +Tablas relacionadas con la **gestión de activos fijos**: +- `fa_assets`: Registro de activos fijos +- `fa_depreciation`: Depreciación de activos +- `fa_maintenance`: Mantenimiento de activos +- `fa_transfers`: Transferencias de activos + +**Nota histórica**: Anteriormente se utilizaba el prefijo `q` (SCAF - Sistema de Control de Activos Fijos). + +### Diagrama de Arquitectura de Schemas + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DATABASE: anexo76_db │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │ +│ │ Schema: a24 │ │ Schema: a76 │ │Schema: public │ │ +│ │ (Anexo 24) │ │ (Anexo 76) │ │ (Catálogos) │ │ +│ ├────────────────┤ ├────────────────┤ ├───────────────┤ │ +│ │ │ │ │ │ │ │ +│ │ inv_products │ │ pedimentos │ │ material_types│ │ +│ │ inv_movements │ │ facturas │ │ uom_codes │ │ +│ │ inv_warehouses │ │ customs_docs │ │ countries │ │ +│ │ inv_balances │ │ export_ops │ │ customs_list │ │ +│ │ │ │ │ │ document_types│ │ +│ │ fa_assets │ │ │ │ │ │ +│ │ fa_depreciation│ │ │ │ │ │ +│ │ fa_maintenance │ │ │ │ │ │ +│ │ fa_transfers │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +│ └────────────────┘ └────────────────┘ └───────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Ventajas de esta Arquitectura + +1. **Separación Lógica**: Cada schema representa un dominio específico del negocio +2. **Escalabilidad**: Facilita la adición de nuevos módulos sin afectar los existentes +3. **Seguridad**: Permite aplicar permisos a nivel de schema +4. **Mantenibilidad**: Código y migraciones organizados por dominio +5. **Claridad**: Los prefijos hacen evidente la funcionalidad de cada tabla +6. **Migración Gradual**: Permite actualizar sistemas legados (SCAII/SCAF) sin interrupciones + +### Mapeo de Sistemas Legados + +| Sistema Legacy | Prefijo Antiguo | Sistema Nuevo | Prefijo Nuevo | Schema | +|----------------------|-----------------|-------------------|---------------|----------| +| SCAII (Inventarios) | `s` | Inventarios | `inv_` | `a24` | +| SCAF (Activos Fijos) | `q` | Fixed Assets | `fa_` | `a24` | +| Winsaii (Pedimentos) | `w` | - | - | `a22` | +| - | `g` | Comercio Exterior | - | `a76` | +| - | `g` | Catálogos SAT | - | `public` | + +--- + ## Estructura del Proyecto ``` @@ -143,11 +242,25 @@ anexo76/ │ └── api/ │ └── v1/ │ ├── router.py # Router principal v1 -│ └── modules/ # Módulos de negocio -│ ├── auth/ # Autenticación -│ ├── tenants/ # Gestión de tenants -│ ├── licenses/ # Control de licencias -│ └── ... # Futuros módulos +│ ├── common/ # Utilidades compartidas +│ │ ├── base_models.py +│ │ ├── crud_routes.py +│ │ ├── dto_mixins.py +│ │ └── tenant_crud_routes.py +│ │ +│ └── modules/ # Módulos de negocio por schema +│ ├── a24/ # Módulo Anexo 24 (Inventarios) +│ │ ├── inventarios/ +│ │ └── activos_fijos/ +│ │ +│ ├── a76/ # Módulo Anexo 76 (Comercio Exterior) +│ │ ├── pedimentos/ +│ │ └── facturas/ +│ │ +│ └── public/ # Catálogos compartidos +│ ├── material_types/ +│ ├── uom_codes/ +│ └── countries/ │ ├── frontend/ │ ├── src/ diff --git a/docs/KEYCLOAK_SETUP.md b/docs/KEYCLOAK_SETUP.md index d1ba4aec..d08d7451 100644 --- a/docs/KEYCLOAK_SETUP.md +++ b/docs/KEYCLOAK_SETUP.md @@ -2,6 +2,15 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. +# Script auto initialize + +Te genera toda la configruracion inicial de keycloack que se ve en este documento, +aparte de esto te genera un primer usuario configurado con su tenant y una company + +``` +scripts/init_first_time.sh +``` + ## 1. Acceder a Keycloak Admin Console 1. Abrir http://localhost:8080 @@ -11,6 +20,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. ## 2. Configurar Cliente Backend ### Crear Cliente Backend + 1. En el menú izquierdo, ir a **Clients** 2. Clic en **Create client** 3. Configurar: @@ -29,6 +39,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. - Clic en **Save** ### Obtener Client Secret + 1. Ir a la pestaña **Credentials** 2. Copiar el **Client secret** 3. Agregar al archivo `backend/.env`: @@ -39,6 +50,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. ## 3. Configurar Cliente Frontend ### Crear Cliente Frontend + 1. En **Clients**, clic en **Create client** 2. Configurar: - **Client ID**: `anexo76-frontend` @@ -51,13 +63,13 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. - Clic en **Next** 4. En "Login settings": - **Root URL**: `http://localhost:5173` - - **Valid redirect URIs**: + - **Valid redirect URIs**: - `http://localhost:5173/*` - `http://localhost:3000/*` - - **Valid post logout redirect URIs**: + - **Valid post logout redirect URIs**: - `http://localhost:5173/*` - `http://localhost:3000/*` - - **Web origins**: + - **Web origins**: - `http://localhost:5173` - `http://localhost:3000` - Clic en **Save** @@ -65,6 +77,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. ## 4. Crear Usuario de Prueba ### Crear Usuario + 1. En el menú izquierdo, ir a **Users** 2. Clic en **Add user** 3. Configurar: @@ -76,6 +89,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. - Clic en **Create** ### Establecer Contraseña + 1. Ir a la pestaña **Credentials** 2. Clic en **Set password** 3. Configurar: @@ -85,6 +99,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. 4. Clic en **Save** ### Agregar Atributo tenant_id + 1. En el mismo usuario, ir a la pestaña **Attributes** 2. Clic en **Add an attribute** 3. Configurar: @@ -93,6 +108,7 @@ Esta guía te ayudará a configurar Keycloak para usar con Anexo76. 4. Clic en **Save** ### Asignar Roles + 1. Ir a la pestaña **Role mappings** 2. En "Available roles", buscar y asignar: - `admin` (si existe) @@ -126,6 +142,7 @@ Repetir para el cliente `anexo76-frontend` si es necesario. ## 6. Verificar Configuración ### Probar desde el Frontend + 1. Abrir http://localhost:5173 2. Hacer clic en "Iniciar Sesión" 3. Ingresar credenciales: @@ -134,6 +151,7 @@ Repetir para el cliente `anexo76-frontend` si es necesario. 4. Deberías ver el dashboard con información del usuario y licencia ### Probar desde el API + ```bash # Obtener token curl -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \ @@ -152,11 +170,13 @@ curl -X GET http://localhost:8000/v1/auth/me \ ## 7. Configuración Adicional (Opcional) ### Personalizar Tema de Login + 1. Ir a **Realm settings** → **Themes** 2. Seleccionar tema de login deseado 3. Guardar cambios ### Configurar Timeout de Sesión + 1. Ir a **Realm settings** → **Sessions** 2. Ajustar: - **SSO Session Idle**: Tiempo de inactividad antes de expirar (ej: 30 minutos) @@ -164,6 +184,7 @@ curl -X GET http://localhost:8000/v1/auth/me \ 3. Guardar cambios ### Habilitar Registro de Usuarios (Opcional) + 1. Ir a **Realm settings** → **Login** 2. Activar **User registration** 3. Guardar cambios @@ -171,20 +192,24 @@ curl -X GET http://localhost:8000/v1/auth/me \ ## Troubleshooting ### Error: "Invalid redirect URI" + - Verificar que las URIs en el cliente coincidan exactamente - Incluir el protocolo (http:// o https://) - Incluir el puerto si es necesario ### Error: "Client not found" + - Verificar que el Client ID sea exacto - Verificar que el realm sea correcto ### Token no incluye tenant_id + - Verificar que el usuario tenga el atributo configurado - Verificar que el mapper esté configurado correctamente - Probar obteniendo un nuevo token ### Usuario no puede hacer login + - Verificar que el usuario esté habilitado (User enabled: ON) - Verificar que el email esté verificado (Email verified: ON) - Verificar que la contraseña no sea temporal diff --git a/docs/MODULOS_A76_IMPLEMENTADOS.md b/docs/MODULOS_A76_IMPLEMENTADOS.md deleted file mode 100644 index 2c63b62e..00000000 --- a/docs/MODULOS_A76_IMPLEMENTADOS.md +++ /dev/null @@ -1,174 +0,0 @@ -# Módulos A76 Implementados - Anexo 76 - -**Fecha de implementación:** 4 de noviembre de 2025 - ---- - -## ✨ Nuevas Funcionalidades - -### Módulo de Empresa (Company) -- Gestión de empresa única con información comercial completa -- Manejo de datos fiscales y operativos centralizados - -### Módulo de Clientes y Proveedores (Client & Provider) -- Gestión integral de clientes y proveedores -- Relaciones con direcciones y programas asociados -- Capacidad de diferenciar entre clientes y proveedores - -### Módulo de Partes (GParts) -- Gestión de partes/componentes para los sistemas SCAII, SCAF y WINSAAI -- Control de inventario y clasificación arancelaria -- Información regulatoria y de cumplimiento - -### Módulo de Clases (Class) -- Clasificaciones para sistemas SCAII y SCAF -- Información arancelaria detallada -- Gestión de fracciones arancelarias y materiales - ---- - -## 🔗 Relaciones de Base de Datos - -### Relaciones Principales -- **Part ↔ Class**: Relación de clave compuesta (client_id, part_class ↔ class_code) -- **Part → Country**: Clave foránea a public.countries (country_of_origin) -- **Part → CurrencyType**: Clave foránea a public.currency_types (currency_key) -- **Class → MaterialType**: Clave foránea a public.material_types (material_key) - -### Esquema de Relaciones -``` -Part (Partes) -├── País de origen → Country -├── Tipo de moneda → CurrencyType -└── Información de clase → Class - └── Tipo de material → MaterialType -``` - ---- - -## 📊 Endpoints de API Agregados - -### Módulo Empresa (`/company`) -| Método | Endpoint | Descripción | -|--------|----------|-------------| -| POST | `/` | Crear empresa | -| GET | `/` | Obtener información de la empresa | - -### Módulo Clientes y Proveedores (`/clients-providers`) -| Método | Endpoint | Descripción | -|--------|----------|-------------| -| POST | `/` | Crear cliente/proveedor | -| GET | `/` | Listar todos con paginación | -| GET | `/clients` | Listar solo clientes | -| GET | `/providers` | Listar solo proveedores | -| GET | `/search/rfc/{rfc}` | Buscar por RFC | -| GET | `/{client_id}` | Obtener por ID | -| PUT | `/{client_id}` | Actualizar cliente/proveedor | -| DELETE | `/{client_id}` | Eliminar cliente/proveedor | -| PATCH | `/{client_id}/toggle-status` | Cambiar estatus | -| GET | `/{client_id}/address` | Obtener información de dirección | -| GET | `/{client_id}/programs` | Obtener información de programas | -| GET | `/{client_id}/basic` | Obtener información básica | - -### Módulo Partes (`/parts`) -| Método | Endpoint | Descripción | -|--------|----------|-------------| -| POST | `/` | Crear parte | -| GET | `/` | Listar todas con paginación y filtros | -| GET | `/client/{client_id}` | Obtener partes por cliente | -| GET | `/search/fraction/{fraction}` | Buscar por fracción arancelaria | -| GET | `/search/supplier/{supplier}` | Buscar por proveedor | -| GET | `/search/country/{country_code}` | Buscar por país | -| GET | `/statistics` | Obtener estadísticas de partes | -| GET | `/{client_id}/{part_number}` | Obtener parte específica | -| PUT | `/{client_id}/{part_number}` | Actualizar parte | -| DELETE | `/{client_id}/{part_number}` | Eliminar parte | -| PATCH | `/{client_id}/{part_number}/toggle-status` | Cambiar estatus | -| GET | `/{client_id}/{part_number}/basic` | Obtener información básica | -| GET | `/{client_id}/{part_number}/regulatory` | Obtener información regulatoria | - -### Módulo Clases (`/classes`) -| Método | Endpoint | Descripción | -|--------|----------|-------------| -| POST | `/` | Crear clase | -| GET | `/` | Listar todas con paginación y filtros | -| GET | `/client/{client_id}` | Obtener clases por cliente | -| GET | `/search/fraction/{fraction}` | Buscar por fracción arancelaria | -| GET | `/search/material/{material_key}` | Buscar por material | -| GET | `/search/unit-measure/{unit_of_measure}` | Buscar por unidad de medida | -| GET | `/search/physical-review/{physical_review}` | Buscar por revisión física | -| GET | `/statistics` | Obtener estadísticas de clases | -| GET | `/{client_id}/{class_code}` | Obtener clase específica | -| PUT | `/{client_id}/{class_code}` | Actualizar clase | -| DELETE | `/{client_id}/{class_code}` | Eliminar clase | -| GET | `/{client_id}/{class_code}/basic` | Obtener información básica | -| GET | `/{client_id}/{class_code}/tariff` | Obtener información arancelaria | - ---- - -## 🏗️ Arquitectura Implementada - -### Diseño Modular -- **Modelos**: Definición de entidades ORM con SQLAlchemy -- **DTOs**: Objetos de transferencia de datos con validación Pydantic -- **Servicios**: Lógica de negocio y operaciones de base de datos -- **Rutas**: Endpoints REST API con documentación automática - -### Características Técnicas -- **Nombres de campos en inglés** para consistencia internacional -- **Claves primarias compuestas** donde es aplicable -- **Operaciones CRUD completas** con endpoints de búsqueda especializados -- **Relaciones SQLAlchemy** con restricciones de clave foránea apropiadas -- **DTOs type-safe** con validación Pydantic - -### Patrones de Desarrollo -- Estructura consistente en todos los módulos para facilitar mantenimiento -- Separación clara de responsabilidades (models, DTOs, services, routes) -- Validación de datos en múltiples capas -- Manejo de errores estandarizado -- Documentación automática con FastAPI/OpenAPI - ---- - -## 📝 Documentación - -### Archivos de Documentación -- **RELATIONSHIPS.md**: Documentación completa de relaciones de base de datos -- **Type hints detallados** en todos los métodos de servicio -- **Comentarios explicativos** en modelos y funciones complejas - -### Estándares de Código -- Consistencia en patrones de desarrollo entre módulos -- Nomenclatura estandarizada para endpoints y funciones -- Validación robusta de datos de entrada y salida -- Manejo de excepciones centralizado - ---- - -## 📈 Resumen de Implementación - -### Números Totales -- **4 módulos completos** implementados -- **42+ endpoints** REST API disponibles -- **23 archivos nuevos** agregados al proyecto -- **2,798+ líneas de código** implementadas - -### Estado del Proyecto -- ✅ Modelos de base de datos implementados -- ✅ Relaciones entre entidades establecidas -- ✅ DTOs con validación completa -- ✅ Servicios con lógica de negocio -- ✅ Endpoints REST API funcionales -- ✅ Integración en router principal -- ⏳ Migraciones de base de datos (pendiente) - -### Próximos Pasos -1. Crear migraciones de Alembic para las nuevas tablas -2. Implementar tests unitarios para cada módulo -3. Agregar documentación de API con ejemplos -4. Implementar autenticación y autorización -5. Optimizar consultas de base de datos - ---- - -*Documento generado automáticamente el 4 de noviembre de 2025* \ No newline at end of file diff --git a/docs/RELATIONSHIPS.md b/docs/RELATIONSHIPS.md deleted file mode 100644 index 4e2e35c2..00000000 --- a/docs/RELATIONSHIPS.md +++ /dev/null @@ -1,107 +0,0 @@ -# Relaciones entre Modelos A76 - -## Resumen de Relaciones Establecidas - -### Part (Tabla: parts) -El modelo `Part` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI. - -#### Relaciones: - -1. **Con Country (public.countries)** - - Campo: `country_of_origin` → `countries.m3_key` - - Relación: Many-to-One - - Propósito: País de origen de la parte - -2. **Con CurrencyType (public.currency_types)** - - Campo: `currency_key` → `currency_types.code` - - Relación: Many-to-One - - Propósito: Tipo de moneda para el costo unitario - -3. **Con Class (classes)** - - Campos: `(client_id, part_class)` → `(client_id, class_code)` - - Relación: Many-to-One (usando primaryjoin complejo) - - Propósito: Clasificación de la parte - - Atributo: `part_class_info` - -### Class (Tabla: classes) -El modelo `Class` representa las clases de clasificación en sistemas SCAII y SCAF. - -#### Relaciones: - -1. **Con MaterialType (public.material_types)** - - Campo: `material_key` → `material_types.key` - - Relación: Many-to-One - - Propósito: Tipo de material de la clase - -2. **Con Part (parts)** - - Campos: `(client_id, class_code)` → `(client_id, part_class)` - - Relación: One-to-Many (inversa de la relación en Part) - - Propósito: Partes que pertenecen a esta clase - - Atributo: `parts` - -## Esquema de Relaciones - -``` -Part -├── country (Country) # País de origen -├── currency (CurrencyType) # Tipo de moneda -└── part_class_info (Class) # Información de clasificación - └── material_type (MaterialType) # Tipo de material - -Class -├── material_type (MaterialType) # Tipo de material -└── parts (List[Part]) # Partes que usan esta clase -``` - -## Uso de las Relaciones - -### En consultas: -```python -# Obtener una parte con su información completa -part = session.query(Part).options( - joinedload(Part.country), - joinedload(Part.currency), - joinedload(Part.part_class_info).joinedload(Class.material_type) -).filter( - Part.client_id == 1, - Part.part_number == "PART001" -).first() - -# Acceder a los datos relacionados -print(f"País: {part.country.description_es}") -print(f"Moneda: {part.currency.currency_name}") -print(f"Clase: {part.part_class_info.description_spanish}") -print(f"Material: {part.part_class_info.material_type.description}") -``` - -### En DTOs: -Los DTOs pueden incluir información relacionada: -```python -class PartDetailResponseDTO(BaseModel): - client_id: int - part_number: str - description_spanish: Optional[str] - country_name: Optional[str] = None - currency_name: Optional[str] = None - class_description: Optional[str] = None - material_type: Optional[str] = None -``` - -## Consideraciones Técnicas - -1. **Composite Foreign Keys**: La relación entre `Part` y `Class` usa claves foráneas compuestas que requieren `primaryjoin` personalizado. - -2. **Viewonly Relationships**: Algunas relaciones están marcadas como `viewonly=True` para evitar problemas de escritura accidental. - -3. **Lazy Loading**: Por defecto, las relaciones usan lazy loading. Para consultas que necesiten datos relacionados, usar `joinedload` o `selectinload`. - -4. **Type Hints**: Se usan `TYPE_CHECKING` imports para evitar import circulares mientras se mantienen los type hints. - -## Futuras Relaciones - -Potenciales relaciones adicionales que se pueden agregar: - -1. **Con Sectors** (public.sectors) - para clasificación sectorial -2. **Con Transport Types** (public.transport_types) - para modo de transporte -3. **Con Customs Sections** (public.customs_sections) - para sección aduanera -4. **Relaciones con tablas subsidiarias** como `SPartes`, `QPartes`, etc. \ No newline at end of file diff --git a/docs/SCHEMA_A76_UPDATE.md b/docs/SCHEMA_A76_UPDATE.md deleted file mode 100644 index b0b6ca65..00000000 --- a/docs/SCHEMA_A76_UPDATE.md +++ /dev/null @@ -1,126 +0,0 @@ -# Actualización de Schemas A76 - -**Fecha de actualización:** 5 de noviembre de 2025 - ---- - -## ✅ Modelos Actualizados al Schema A76 - -Se han actualizado todos los modelos en `api/v1/modules/a76/` para usar el schema `a76` en PostgreSQL. - -### 📋 Tablas Configuradas - -| Módulo | Tabla | Schema | Estado | -|--------|-------|---------|---------| -| **Company** | `company` | `a76` | ✅ Actualizada | -| **Client & Provider** | `client_provider` | `a76` | ✅ Actualizada | -| **Client & Provider** | `client_provider_address` | `a76` | ✅ Actualizada | -| **Client & Provider** | `client_provider_programs` | `a76` | ✅ Actualizada | -| **GParts** | `parts` | `a76` | ✅ Actualizada | -| **Class** | `classes` | `a76` | ✅ Actualizada | -| **Licenses** | `licenses` | `a76` | ✅ Ya estaba | -| **Licenses** | `license_usage` | `a76` | ✅ Ya estaba | -| **Tenants** | `tenants` | `a76` | ✅ Ya estaba | - -### 🔄 Cambios Realizados - -#### 1. Configuración de Schema -```python -# ANTES -class Company(Base): - __tablename__ = "company" - -# DESPUÉS -class Company(Base): - __tablename__ = "company" - __table_args__ = {"schema": "a76"} -``` - -#### 2. Foreign Keys Actualizadas -```python -# ANTES -client_id = Column(String(8), ForeignKey('client_provider.client_id'), ...) - -# DESPUÉS -client_id = Column(String(8), ForeignKey('a76.client_provider.client_id'), ...) -``` - -### 🏗️ Estructura de Schemas - -``` -PostgreSQL Database -├── Schema: public -│ ├── countries -│ ├── currency_types -│ ├── material_types -│ └── ... (reference data) -│ -└── Schema: a76 - ├── tenants - ├── licenses - ├── license_usage - ├── company - ├── client_provider - ├── client_provider_address - ├── client_provider_programs - ├── parts - └── classes -``` - -### 🔗 Relaciones Mantenidas - -Las relaciones entre schemas funcionan correctamente: - -- **A76 → Public**: Los modelos A76 pueden referenciar datos de referencia en `public` -- **A76 → A76**: Las relaciones internas del schema A76 están actualizadas -- **Composite Keys**: Las relaciones con claves compuestas funcionan correctamente - -#### Ejemplos de Relaciones Cross-Schema: -```python -# Part (a76) → Country (public) -country_of_origin = Column(String(3), ForeignKey('public.countries.m3_key')) - -# Part (a76) → CurrencyType (public) -currency_key = Column(String(3), ForeignKey('public.currency_types.code')) - -# Class (a76) → MaterialType (public) -material_key = Column(String(10), ForeignKey('public.material_types.key')) -``` - -### 🎯 Beneficios de la Separación - -1. **Organización**: Datos de negocio separados de datos de referencia -2. **Seguridad**: Permisos granulares por schema -3. **Mantenimiento**: Facilita respaldos y migraciones selectivas -4. **Escalabilidad**: Permite distribuir schemas en el futuro -5. **Claridad**: Separación lógica de responsabilidades - -### ⚠️ Consideraciones Importantes - -1. **Migraciones**: Las nuevas migraciones deben especificar el schema `a76` -2. **Permisos DB**: El usuario de base de datos necesita permisos en ambos schemas -3. **Testing**: Los tests deben considerar la estructura de schemas -4. **Backup**: Configurar respaldos para incluir ambos schemas - -### 📝 Próximos Pasos - -1. **Crear migraciones de Alembic** con el schema correcto -2. **Verificar permisos** de base de datos para el usuario de aplicación -3. **Actualizar tests** para considerar la estructura de schemas -4. **Documentar convenciones** de naming para futuros modelos - ---- - -### 🔧 Comando de Verificación - -Para verificar que todos los modelos tienen el schema correcto: - -```bash -grep -r "__table_args__ = {\"schema\": \"a76\"}" backend/api/v1/modules/a76/*/models.py -``` - -**Resultado esperado:** 8 coincidencias (una por cada modelo A76) - ---- - -*Actualización completada el 5 de noviembre de 2025* \ No newline at end of file diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f474d318..2e0e9afd 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -72,6 +72,8 @@ "customs_sections": "Customs Sections", "anexo_22_app_31": "Anexo 22 App 3" }, + "clients_and_providers": "Clients and Providers", + "customs_brokers": "Customs Brokers", "nav_user": { "profile": "Profile", "settings": "Settings", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index e879f1fe..ae2806e5 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -71,6 +71,12 @@ "payment_methods": "Formas de Pago", "customs_sections": "Secciones Aduaneras", "anexo_22_app_31": "Anexo 22 App 3" + }, + "clients_and_providers": "Clientes y Proveedores", + "customs_brokers": "Agentes Aduanales", + "nav_user": { + "profile": "Perfil", + "settings": "Configuración" } } } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 02dc6bfc..79332865 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,6 +55,7 @@ "vitest-browser-svelte": "^1.1.0" }, "dependencies": { - "keycloak-js": "^26.2.1" + "keycloak-js": "^26.2.1", + "lucide-svelte": "^0.553.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b514b82d..ad22d6ef 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: keycloak-js: specifier: ^26.2.1 version: 26.2.1 + lucide-svelte: + specifier: ^0.553.0 + version: 0.553.0(svelte@5.40.2) devDependencies: '@eslint/compat': specifier: ^1.4.0 @@ -1414,6 +1417,11 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lucide-svelte@0.553.0: + resolution: {integrity: sha512-pOqzFX+RfcNyvjF0+nGVnSmprd+4NQ6mvpLOLEmhTyZGOad8+OtCl65822E7Rx9qE7rfKw84ODKI2v318JZ/7g==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -3181,6 +3189,10 @@ snapshots: loupe@3.2.1: {} + lucide-svelte@0.553.0(svelte@5.40.2): + dependencies: + svelte: 5.40.2 + lz-string@1.5.0: {} magic-string@0.30.19: diff --git a/frontend/src/lib/api/dashboard/a76/clients-providers.ts b/frontend/src/lib/api/dashboard/a76/clients-providers.ts new file mode 100644 index 00000000..40faa07d --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/clients-providers.ts @@ -0,0 +1,202 @@ +/** + * API Client para Clientes y Proveedores + * Gestiona las operaciones CRUD para clientes y proveedores + */ +import { api } from '$lib/api'; + +export interface ClientProviderAddress { + id?: number; + street?: string | null; + neighborhood?: string | null; + city?: string | null; + state?: string | null; + country?: string | null; + zip_code?: string | null; + client_id?: number; +} + +export interface ClientProviderPrograms { + id?: number; + program_code?: string | null; + authorization_date?: string | null; + client_id?: number; +} + +export interface ClientProvider { + id: number; + rfc: string; + name: string; + curp?: string | null; + residence_country?: string | null; + domicile_fiscal?: string | null; + foreign_tax_id?: string | null; + client_or_provider?: string | null; + enabled_disabled?: number; + tenant_id: number; + address?: ClientProviderAddress | null; + programs?: ClientProviderPrograms | null; +} + +export interface ClientProviderBasic { + id: number; + rfc: string; + name: string; + curp?: string | null; + residence_country?: string | null; + domicile_fiscal?: string | null; + foreign_tax_id?: string | null; + client_or_provider?: string | null; + enabled_disabled?: number; + tenant_id: number; +} + +export interface ClientProviderListResponse { + items: ClientProvider[]; + total: number; + page: number; + page_size: number; +} + +export interface CreateClientProviderData { + rfc: string; + name: string; + curp?: string | null; + residence_country?: string | null; + domicile_fiscal?: string | null; + foreign_tax_id?: string | null; + client_or_provider?: string | null; + enabled_disabled?: number; + address?: Omit | null; + programs?: Omit | null; +} + +export interface UpdateClientProviderData { + rfc?: string; + name?: string; + curp?: string | null; + residence_country?: string | null; + domicile_fiscal?: string | null; + foreign_tax_id?: string | null; + client_or_provider?: string | null; + enabled_disabled?: number; + address?: Partial | null; + programs?: Partial | null; +} + +/** + * API para Clientes y Proveedores + */ +export const clientsProvidersApi = { + /** + * Lista todos los clientes y proveedores con paginación + * @param companyId - ID de la compañía + * @param page - Número de página (por defecto 1) + * @param pageSize - Tamaño de página (por defecto 50) + * @param filters - Filtros opcionales + */ + list: (companyId: number, page = 1, pageSize = 50, filters?: Record) => { + const params = new URLSearchParams({ + company_id: companyId.toString(), + page: page.toString(), + page_size: pageSize.toString() + }); + + if (filters) { + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, value.toString()); + } + }); + } + + return api.get( + `/v1/a76/clients-providers?${params.toString()}` + ); + }, + + /** + * Lista solo clientes (client_or_provider = 'client') + * @param companyId - ID de la compañía + * @param skip - Número de registros a saltar + * @param limit - Límite de registros + */ + listClients: (companyId: number, skip = 0, limit = 100) => + api.get( + `/v1/a76/clients-providers/clients?company_id=${companyId}&skip=${skip}&limit=${limit}` + ), + + /** + * Lista solo proveedores (client_or_provider = 'provider') + * @param companyId - ID de la compañía + * @param skip - Número de registros a saltar + * @param limit - Límite de registros + */ + listProviders: (companyId: number, skip = 0, limit = 100) => + api.get( + `/v1/a76/clients-providers/providers?company_id=${companyId}&skip=${skip}&limit=${limit}` + ), + + /** + * Busca clientes/proveedores por RFC + * @param companyId - ID de la compañía + * @param rfc - RFC a buscar + */ + searchByRfc: (companyId: number, rfc: string) => + api.get( + `/v1/a76/clients-providers/search/rfc/${rfc}?company_id=${companyId}` + ), + + /** + * Obtiene un cliente/proveedor por ID + * @param id - ID del cliente/proveedor + * @param companyId - ID de la compañía + */ + get: (id: number, companyId: number) => + api.get(`/v1/a76/clients-providers/${id}?company_id=${companyId}`), + + /** + * Obtiene información básica de un cliente/proveedor + * @param id - ID del cliente/proveedor + * @param companyId - ID de la compañía + */ + getBasic: (id: number, companyId: number) => + api.get( + `/v1/a76/clients-providers/${id}/basic?company_id=${companyId}` + ), + + /** + * Crea un nuevo cliente/proveedor + * @param companyId - ID de la compañía + * @param data - Datos del cliente/proveedor a crear + */ + create: (companyId: number, data: CreateClientProviderData) => + api.post(`/v1/a76/clients-providers?company_id=${companyId}`, data), + + /** + * Actualiza un cliente/proveedor existente + * @param id - ID del cliente/proveedor a actualizar + * @param companyId - ID de la compañía + * @param data - Datos a actualizar + */ + update: (id: number, companyId: number, data: UpdateClientProviderData) => + api.put(`/v1/a76/clients-providers/${id}?company_id=${companyId}`, data), + + /** + * Alterna el estado activo/inactivo de un cliente/proveedor + * @param id - ID del cliente/proveedor + * @param companyId - ID de la compañía + */ + toggleStatus: (id: number, companyId: number) => + api.put( + `/v1/a76/clients-providers/${id}/toggle-status?company_id=${companyId}`, + {} + ), + + /** + * Elimina un cliente/proveedor + * @param id - ID del cliente/proveedor a eliminar + * @param companyId - ID de la compañía + */ + delete: (id: number, companyId: number) => + api.delete(`/v1/a76/clients-providers/${id}?company_id=${companyId}`) +}; diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts b/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts new file mode 100644 index 00000000..cfa37a35 --- /dev/null +++ b/frontend/src/lib/components/dashboard/clients_and_providers/columns.ts @@ -0,0 +1,111 @@ +import type { ColumnDef } from "@tanstack/table-core"; +import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import DataTableActions from "./data-table-actions.svelte"; +import type { ClientProvider } from "$lib/api/dashboard/a76/clients-providers"; + +export type { ClientProvider }; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: "id", + header: "ID", + cell: ({ row }) => { + const idSnippet = createRawSnippet<[{ id: number }]>((getId) => { + const { id } = getId(); + return { + render: () => + `${id}` + }; + }); + return renderSnippet(idSnippet, { id: row.original.id }); + } + }, + { + accessorKey: "rfc", + header: "RFC", + cell: ({ row }) => { + const rfcSnippet = createRawSnippet<[{ rfc: string }]>((getRfc) => { + const { rfc } = getRfc(); + return { + render: () => + `${rfc}` + }; + }); + return renderSnippet(rfcSnippet, { rfc: row.original.rfc }); + } + }, + { + accessorKey: "name", + header: "Nombre", + cell: ({ row }) => { + const nameSnippet = createRawSnippet<[{ name: string }]>((getName) => { + const { name } = getName(); + return { + render: () => `
${name}
` + }; + }); + return renderSnippet(nameSnippet, { name: row.original.name }); + } + }, + { + accessorKey: "client_or_provider", + header: "Tipo", + cell: ({ row }) => { + const typeSnippet = createRawSnippet<[{ type: string | null | undefined }]>((getType) => { + const { type } = getType(); + const displayType = type === 'client' ? 'Cliente' : type === 'provider' ? 'Proveedor' : type === 'both' ? 'Ambos' : 'N/A'; + const colorClass = type === 'client' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300' + : type === 'provider' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' + : type === 'both' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300' + : 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'; + return { + render: () => `${displayType}` + }; + }); + return renderSnippet(typeSnippet, { type: row.original.client_or_provider }); + } + }, + { + accessorKey: "residence_country", + header: "País", + cell: ({ row }) => { + const countrySnippet = createRawSnippet<[{ country: string | null | undefined }]>((getCountry) => { + const { country } = getCountry(); + return { + render: () => `
${country || '-'}
` + }; + }); + return renderSnippet(countrySnippet, { country: row.original.residence_country }); + } + }, + { + accessorKey: "enabled_disabled", + header: "Estado", + cell: ({ row }) => { + const statusSnippet = createRawSnippet<[{ status: number | undefined }]>((getStatus) => { + const { status } = getStatus(); + const isEnabled = status === 1; + const statusText = isEnabled ? 'Activo' : 'Inactivo'; + const colorClass = isEnabled + ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' + : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + return { + render: () => `${statusText}` + }; + }); + return renderSnippet(statusSnippet, { status: row.original.enabled_disabled }); + } + }, + { + id: "actions", + cell: ({ row }) => { + return renderComponent(DataTableActions, { item: row.original, onSuccess }); + } + } + ]; +} + +// Mantener compatibilidad hacia atrás +export const columns = createColumns(); diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/create-edit-dialog.svelte new file mode 100644 index 00000000..2a85404a --- /dev/null +++ b/frontend/src/lib/components/dashboard/clients_and_providers/create-edit-dialog.svelte @@ -0,0 +1,461 @@ + + + + + + + {isEditing ? "Editar" : "Nuevo"} Cliente/Proveedor + + + {isEditing + ? "Modifica los datos del cliente o proveedor." + : "Completa los datos para crear un nuevo cliente o proveedor."} + + + +
+ {#if error} +
+ {error} +
+ {/if} + + +
+

Información Básica

+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+

Información Fiscal

+ +
+ + +
+ +
+ + +
+
+ + +
+

Dirección

+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + +
+

Programas

+ +
+
+ + +
+ +
+ + +
+
+
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte new file mode 100644 index 00000000..982cc4dc --- /dev/null +++ b/frontend/src/lib/components/dashboard/clients_and_providers/data-table-actions.svelte @@ -0,0 +1,103 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + + Acciones + + Copiar ID + + + Copiar RFC + + + + Ver detalles + Editar + + {isToggling ? 'Cambiando...' : item.enabled_disabled === 1 ? 'Desactivar' : 'Activar'} + + + Eliminar + + + + + + + diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte new file mode 100644 index 00000000..ef98de23 --- /dev/null +++ b/frontend/src/lib/components/dashboard/clients_and_providers/data-table.svelte @@ -0,0 +1,123 @@ + + +
+
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {#if !header.isPlaceholder} + + {/if} + + {/each} + + {/each} + + + {#each table.getRowModel().rows as row (row.id)} + + {#each row.getVisibleCells() as cell (cell.id)} + + + + {/each} + + {:else} + + + No hay resultados. + + + {/each} + + + {#if hasMore} + + +
+ {#if loading} +
+
+ Cargando más... +
+ {:else} +
+ Desplázate para cargar más +
+ {/if} +
+
+
+ {/if} +
+
+
+
diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/delete-dialog.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/delete-dialog.svelte new file mode 100644 index 00000000..2d0963be --- /dev/null +++ b/frontend/src/lib/components/dashboard/clients_and_providers/delete-dialog.svelte @@ -0,0 +1,125 @@ + + + + + + ¿Estás seguro? + +

Esta acción no se puede deshacer. Se eliminará permanentemente este cliente/proveedor:

+ {#if item} +
+
+ ID: + {item.id} +
+
+ RFC: + {item.rfc} +
+
+ Nombre: + {item.name} +
+
+ Tipo: + + {item.client_or_provider === 'client' ? 'Cliente' : + item.client_or_provider === 'provider' ? 'Proveedor' : + item.client_or_provider === 'both' ? 'Ambos' : 'N/A'} + +
+
+ {/if} + {#if error} +
+ {error} +
+ {/if} +
+
+ + Cancelar + + {#if loading} + + + + + {/if} + Eliminar + + +
+
diff --git a/frontend/src/lib/components/dashboard/clients_and_providers/details-dialog.svelte b/frontend/src/lib/components/dashboard/clients_and_providers/details-dialog.svelte new file mode 100644 index 00000000..592a85e7 --- /dev/null +++ b/frontend/src/lib/components/dashboard/clients_and_providers/details-dialog.svelte @@ -0,0 +1,217 @@ + + + + + + Detalles del Cliente/Proveedor + + Información completa del cliente o proveedor + + + + {#if item} +
+ +
+

Información Básica

+ +
+
+ ID + {item.id} +
+ +
+ RFC + {item.rfc} +
+
+ +
+ Nombre + {item.name} +
+ + {#if item.curp} +
+ CURP + {item.curp} +
+ {/if} + + +
+ + +
+

Clasificación

+ +
+
+ Tipo + {#if item.client_or_provider === 'client'} + + Cliente + + {:else if item.client_or_provider === 'provider'} + + Proveedor + + {:else if item.client_or_provider === 'both'} + + Ambos + + {:else} + No especificado + {/if} +
+ +
+ Estado + {#if item.enabled_disabled === 1} + + Activo + + {:else} + + Inactivo + + {/if} +
+
+ + +
+ + +
+

Información Fiscal

+ + {#if item.residence_country} +
+ País de Residencia + {item.residence_country} +
+ {/if} + + {#if item.domicile_fiscal} +
+ Domicilio Fiscal + {item.domicile_fiscal} +
+ {/if} + + {#if item.foreign_tax_id} +
+ ID Fiscal Extranjero + {item.foreign_tax_id} +
+ {/if} + + +
+ + + {#if item.address} +
+

Dirección

+ +
+ {#if item.address.street} +
+ Calle + {item.address.street} +
+ {/if} + +
+ {#if item.address.neighborhood} +
+ Colonia + {item.address.neighborhood} +
+ {/if} + + {#if item.address.zip_code} +
+ Código Postal + {item.address.zip_code} +
+ {/if} +
+ +
+ {#if item.address.city} +
+ Ciudad + {item.address.city} +
+ {/if} + + {#if item.address.state} +
+ Estado + {item.address.state} +
+ {/if} +
+ + {#if item.address.country} +
+ País + {item.address.country} +
+ {/if} +
+ + +
+ {/if} + + + {#if item.programs} +
+

Programas

+ + {#if item.programs.program_code} +
+ Código de Programa + {item.programs.program_code} +
+ {/if} + + {#if item.programs.authorization_date} +
+ Fecha de Autorización + {new Date(item.programs.authorization_date).toLocaleDateString()} +
+ {/if} +
+ {/if} +
+ {/if} + + + + +
+
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 77a9cf17..2291da7c 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -1,13 +1,15 @@ -import AudioWaveformIcon from "@lucide/svelte/icons/audio-waveform"; -import BookOpenIcon from "@lucide/svelte/icons/book-open"; -import BotIcon from "@lucide/svelte/icons/bot"; -import ChartPieIcon from "@lucide/svelte/icons/chart-pie"; -import CommandIcon from "@lucide/svelte/icons/command"; -import FrameIcon from "@lucide/svelte/icons/frame"; -import GalleryVerticalEndIcon from "@lucide/svelte/icons/gallery-vertical-end"; -import MapIcon from "@lucide/svelte/icons/map"; -import Settings2Icon from "@lucide/svelte/icons/settings-2"; -import SquareTerminalIcon from "@lucide/svelte/icons/square-terminal"; +import { + BadgeCheck, + ChartPie, + Database, + FileText, + Frame, + GalleryVerticalEnd, + LayoutDashboard, + Package, + Settings2, + Users, +} from 'lucide-svelte'; import * as m from "$lib/paraglide/messages.js"; export interface NavItem { @@ -59,15 +61,21 @@ export function getSidebarData(): SidebarData { teams: [ { name: "Anexo76", - logo: GalleryVerticalEndIcon, + logo: GalleryVerticalEnd, plan: "Enterprise", }, ], navMain: [ + { + title: "Dashboard", + url: "/dashboard", + icon: LayoutDashboard, + items: [], + }, { title: m["sidebar.reference_data.title"](), url: "/dashboard", - icon: SquareTerminalIcon, + icon: Database, items: [ { title: m["sidebar.reference_data.codes_pedimento_regimen"](), @@ -143,7 +151,7 @@ export function getSidebarData(): SidebarData { { title: m["sidebar.general_catalogs.title"](), url: "#", - icon: BotIcon, + icon: Package, items: [ { title: m["sidebar.general_catalogs.company_information"](), @@ -282,7 +290,7 @@ export function getSidebarData(): SidebarData { { title: m["sidebar.pedimentos.title"](), url: "#", - icon: BookOpenIcon, + icon: FileText, items: [ { title: m["sidebar.pedimentos.pedimento_management"](), @@ -310,10 +318,22 @@ export function getSidebarData(): SidebarData { }, ], }, + { + title: m["sidebar.clients_and_providers"](), + url: "/dashboard/clients_and_providers", + icon: Users, + items: [], + }, + { + title: m["sidebar.customs_brokers"](), + url: "/dashboard/customs_brokers", + icon: BadgeCheck, + items: [], + }, { title: m["sidebar.reference_data.configuracion"](), url: "#", - icon: Settings2Icon, + icon: Settings2, items: [ { title: m["sidebar.reference_data.general"](), @@ -334,12 +354,12 @@ export function getSidebarData(): SidebarData { { name: m["sidebar.reference_data.usuarios"](), url: "#", - icon: ChartPieIcon, + icon: ChartPie, }, { name: m["sidebar.reference_data.ayuda"](), url: "#", - icon: FrameIcon, + icon: Frame, }, ], }; diff --git a/frontend/src/lib/components/sidebar/nav-main.svelte b/frontend/src/lib/components/sidebar/nav-main.svelte index bd491e96..809ab499 100644 --- a/frontend/src/lib/components/sidebar/nav-main.svelte +++ b/frontend/src/lib/components/sidebar/nav-main.svelte @@ -22,43 +22,60 @@ - Platform + Anexo-76 {#each items as item (item.title)} - - {#snippet child({ props })} - - - {#snippet child({ props })} - - {#if item.icon} - - {/if} - {item.title} - - - {/snippet} - - - - {#each item.items ?? [] as subItem (subItem.title)} - - - {#snippet child({ props })} - - {subItem.title} - - {/snippet} - - - {/each} - - - - {/snippet} - + {#if item.items && item.items.length > 0} + + + {#snippet child({ props })} + + + {#snippet child({ props })} + + {#if item.icon} + + {/if} + {item.title} + + + {/snippet} + + + + {#each item.items as subItem (subItem.title)} + + + {#snippet child({ props })} + + {subItem.title} + + {/snippet} + + + {/each} + + + + {/snippet} + + {:else} + + + + {#snippet child({ props })} + + {#if item.icon} + + {/if} + {item.title} + + {/snippet} + + + {/if} {/each} diff --git a/frontend/src/routes/dashboard/clients_and_providers/+page.server.ts b/frontend/src/routes/dashboard/clients_and_providers/+page.server.ts new file mode 100644 index 00000000..4fd142ce --- /dev/null +++ b/frontend/src/routes/dashboard/clients_and_providers/+page.server.ts @@ -0,0 +1,92 @@ +import type { PageServerLoad } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; + +export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { + // Esperar a que el layout padre valide/refresque el token + const parentData = await parent(); + + const { accessToken } = getAuthTokens(cookies); + + if (!accessToken) { + return { + error: 'No authenticated', + items: [], + total: 0, + page: 1, + page_size: 50, + companies: parentData.companies || [] + }; + } + + try { + // Obtener parámetros de paginación de la URL + const page = parseInt(url.searchParams.get('page') || '1'); + const pageSize = parseInt(url.searchParams.get('page_size') || '50'); + + // Obtener company_id de la URL o de las companies del usuario + const companyIdParam = url.searchParams.get('company_id'); + const companyId = companyIdParam + ? parseInt(companyIdParam) + : parentData.companies?.[0]?.id; // Usar la primera compañía por defecto + + if (!companyId) { + return { + error: 'No company selected', + items: [], + total: 0, + page: page, + page_size: pageSize, + companies: parentData.companies || [] + }; + } + + // Usar authenticatedFetch para manejar automáticamente el refresh de tokens + const response = await authenticatedFetch( + `v1/a76/clients-providers?company_id=${companyId}&page=${page}&page_size=${pageSize}`, + {}, + cookies, + fetch + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('📊 [Clients&Providers] API Error:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + return { + error: `Error ${response.status}: ${response.statusText}`, + items: [], + total: 0, + page: page, + page_size: pageSize, + companies: parentData.companies || [], + currentCompanyId: companyId + }; + } + + const data = await response.json(); + + return { + items: data.items || [], + total: data.total || 0, + page: data.page || page, + page_size: data.page_size || pageSize, + error: null, + companies: parentData.companies || [], + currentCompanyId: companyId + }; + } catch (error) { + console.error('📊 [Clients&Providers] Load error:', error); + return { + error: 'Error loading data', + items: [], + total: 0, + page: 1, + page_size: 50, + companies: parentData.companies || [] + }; + } +}; diff --git a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte new file mode 100644 index 00000000..6a348f28 --- /dev/null +++ b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte @@ -0,0 +1,204 @@ + + +
+ +
+
+

Clientes y Proveedores

+

+ Gestiona el catálogo de clientes y proveedores de tu empresa +

+
+ +
+ + + {#if error} + + + Error + {error} + + + {/if} + + + + +
+
+ Listado de Clientes y Proveedores + + Mostrando {allItems.length} de {totalItems} registros + {#if companyStore.activeCompany} + - Compañía: {companyStore.activeCompany.name} + {/if} + +
+ +
+
+ + + + +
+
+ + + diff --git a/package.json b/package.json deleted file mode 100644 index 24a8af56..00000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "lucide-svelte": "^0.552.0" - } -}