diff --git a/.gitignore b/.gitignore index 60db8bdd..f9b99e83 100644 --- a/.gitignore +++ b/.gitignore @@ -49,13 +49,15 @@ logs/ *.sqlite3 # Testing +backend/app_data/ +.mypy_cache/ .pytest_cache/ .coverage htmlcov/ -backend/app_data/ # Node (para frontend) -node_modules/ +**/node_modules/ +**/.svelte-kit/ .npm .yarn diff --git a/backend/api/v1/common/tenant_crud_routes.py b/backend/api/v1/common/tenant_crud_routes.py index 91b23321..98f4b184 100644 --- a/backend/api/v1/common/tenant_crud_routes.py +++ b/backend/api/v1/common/tenant_crud_routes.py @@ -3,7 +3,7 @@ import logging from core.database import get_core_db from core.security import get_current_user, validate_access_to_resource -from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query +from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request from pydantic import BaseModel from sqlalchemy.orm import Session @@ -137,6 +137,7 @@ class TenantCRUDRoutes( description=f"Get paginated list of {self.resource_name}s with optional filters", ) async def list_resources( + request: Request, company_id: int = Query(..., description="Company ID"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query( @@ -145,13 +146,6 @@ class TenantCRUDRoutes( le=self.max_page_size, description="Page size", ), - status: Optional[str] = Query(None, description="Filter by status"), - operation_type: Optional[str] = Query( - None, description="Filter by operation type" - ), - invoice_type: Optional[str] = Query( - None, description="Filter by invoice type" - ), db: Session = Depends(self.db_dependency), current_user: Dict[str, Any] = Depends(self.auth_dependency), ): @@ -164,13 +158,15 @@ class TenantCRUDRoutes( ) skip = (page - 1) * page_size - filters = {} - if status: - filters["status"] = status - if operation_type: - filters["operation_type"] = operation_type - if invoice_type: - filters["invoice_type"] = invoice_type + + # Extraer todos los parámetros de búsqueda dinámicamente + # Excluimos los parámetros estándar de paginación y control + standard_params = {"company_id", "page", "page_size"} + filters = { + k: v + for k, v in request.query_params.items() + if k not in standard_params and v is not None and v != "" + } items, total = self.service.get_all( db, tenant_id, company_id, skip, page_size, filters diff --git a/backend/api/v1/modules/a76/transportation/trailers/services.py b/backend/api/v1/modules/a76/transportation/trailers/services.py index 43332b64..4ffb7c30 100644 --- a/backend/api/v1/modules/a76/transportation/trailers/services.py +++ b/backend/api/v1/modules/a76/transportation/trailers/services.py @@ -25,6 +25,10 @@ class TrailerService: # Apply filters if provided if filters: + if filters.get("trailer_number"): + query = query.filter( + models.Trailer.trailer_number.ilike(f"%{filters['trailer_number']}%") + ) if filters.get("plate_number"): query = query.filter( models.Trailer.plate_number.ilike(f"%{filters['plate_number']}%") @@ -75,8 +79,8 @@ class TrailerService: db: Session, trailer_number: str, tenant_id: int, - company_id: int, trailer_data: dto.TrailerUpdateDTO, + company_id: int, ) -> Optional[models.Trailer]: """Update a trailer""" trailer = TrailerService.get_by_id(db, trailer_number, tenant_id, company_id) diff --git a/backend/api/v1/modules/a76/transportation/transporters/models.py b/backend/api/v1/modules/a76/transportation/transporters/models.py index cc355dbf..9f555c79 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/models.py +++ b/backend/api/v1/modules/a76/transportation/transporters/models.py @@ -9,7 +9,7 @@ class Transporter(Base, TenantScopedMixin, TimestampMixin): {"schema": "a76"}, ) - transporter_key = Column(String(5), primary_key=True, nullable=False) + transporter_key = Column(String(23), primary_key=True, nullable=False) name = Column(String(256), nullable=True) short_name = Column(String(10), nullable=True) responsible = Column(String(100), nullable=True) @@ -27,4 +27,4 @@ class Transporter(Base, TenantScopedMixin, TimestampMixin): ftp_user = Column(String(200), nullable=True) ftp_password = Column(String(100), nullable=True) ftp_directory = Column(String(1000), nullable=True) - filler_code = Column(String(4), nullable=True) + filler_code = Column(String(20), nullable=True) diff --git a/backend/api/v1/modules/a76/transportation/transporters/services.py b/backend/api/v1/modules/a76/transportation/transporters/services.py index 1af393e5..bbf76e69 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/services.py +++ b/backend/api/v1/modules/a76/transportation/transporters/services.py @@ -25,6 +25,10 @@ class TransporterService: # Apply filters if provided if filters: + if filters.get("transporter_key"): + query = query.filter( + models.Transporter.transporter_key.ilike(f"%{filters['transporter_key']}%") + ) if filters.get("name"): query = query.filter( models.Transporter.name.ilike(f"%{filters['name']}%") @@ -75,8 +79,8 @@ class TransporterService: db: Session, transporter_key: str, tenant_id: int, - company_id: int, transporter_data: dto.TransporterUpdateDTO, + company_id: int, ) -> Optional[models.Transporter]: """Update a transporter""" transporter = TransporterService.get_by_id( diff --git a/backend/api/v1/modules/a76/transportation/vehicles/services.py b/backend/api/v1/modules/a76/transportation/vehicles/services.py index d21bd36f..2b9801ac 100644 --- a/backend/api/v1/modules/a76/transportation/vehicles/services.py +++ b/backend/api/v1/modules/a76/transportation/vehicles/services.py @@ -25,6 +25,10 @@ class VehicleService: # Apply filters if provided if filters: + if filters.get("vehicle_key"): + query = query.filter( + models.Vehicle.vehicle_key.ilike(f"%{filters['vehicle_key']}%") + ) if filters.get("plate_number"): query = query.filter( models.Vehicle.plate_number.ilike(f"%{filters['plate_number']}%") @@ -75,8 +79,8 @@ class VehicleService: db: Session, vehicle_key: str, tenant_id: int, - company_id: int, vehicle_data: dto.VehicleUpdateDTO, + company_id: int, ) -> Optional[models.Vehicle]: """Update a vehicle""" vehicle = VehicleService.get_by_id(db, vehicle_key, tenant_id, company_id) diff --git a/backend/main.py b/backend/main.py index b1740f07..d383b62c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -58,6 +58,11 @@ from api.v1.modules.a76.clients_and_providers.models import ClientProvider from api.v1.modules.a76.customs_brokers.models import CustomsBroker from api.v1.modules.a76.general_catalogs.company.models import Company +# Transportation Modules +from api.v1.modules.a76.transportation.trailers.models import Trailer +from api.v1.modules.a76.transportation.transporters.models import Transporter +from api.v1.modules.a76.transportation.vehicles.models import Vehicle + # Core Modules & Transactional Models from api.v1.modules.a76.items.models import LineItem from api.v1.modules.a76.items.series.models import Serie @@ -145,6 +150,137 @@ def run_migrations(): subprocess.run(["alembic", "upgrade", "head"], check=True) +def create_transportation_tables(): + """Crea las tablas de transporte directamente si no existen. + Se usa en lugar de una migración Alembic para evitar gestionar versiones. + """ + from sqlalchemy import text + from core.database import core_engine + + ddl_statements = [ + """ + CREATE TABLE IF NOT EXISTS a76.transporter ( + transporter_key VARCHAR(23) PRIMARY KEY, + name VARCHAR(256), + short_name VARCHAR(10), + responsible VARCHAR(100), + rfc VARCHAR(30), + streets VARCHAR(100), + postal_code VARCHAR(15), + city VARCHAR(30), + state VARCHAR(30), + country VARCHAR(3), + loader_code VARCHAR(9), + caat_code VARCHAR(49), + transport_code VARCHAR(8), + transport_interface_type VARCHAR(20), + ftp_server VARCHAR(200), + ftp_user VARCHAR(200), + ftp_password VARCHAR(100), + ftp_directory VARCHAR(1000), + filler_code VARCHAR(20), + tenant_id INTEGER NOT NULL REFERENCES core.tenants(id), + company_id INTEGER NOT NULL REFERENCES a76.company(id), + created_at TIMESTAMP NOT NULL DEFAULT now(), + updated_at TIMESTAMP NOT NULL DEFAULT now(), + deleted_at TIMESTAMP + ); + """, + """ + CREATE TABLE IF NOT EXISTS a76.trailer ( + trailer_number VARCHAR(20) PRIMARY KEY, + ace_trailer_number VARCHAR(10), + trailer_type_key VARCHAR(2), + seal VARCHAR(15), + entity_code VARCHAR(1), + plate_number VARCHAR(17), + state VARCHAR(30), + country VARCHAR(3), + container_key VARCHAR(3), + tenant_id INTEGER NOT NULL REFERENCES core.tenants(id), + company_id INTEGER NOT NULL REFERENCES a76.company(id), + created_at TIMESTAMP NOT NULL DEFAULT now(), + updated_at TIMESTAMP NOT NULL DEFAULT now(), + deleted_at TIMESTAMP + ); + """, + """ + CREATE TABLE IF NOT EXISTS a76.vehicle ( + vehicle_key VARCHAR(14) PRIMARY KEY, + ace_vehicle_key VARCHAR(10), + transporter_key VARCHAR(23), + transport_identifier VARCHAR(30), + transport_type VARCHAR(2), + entity_code VARCHAR(1), + transponder_number VARCHAR(16), + dot_number VARCHAR(8), + plate_number VARCHAR(17), + city VARCHAR(30), + state VARCHAR(30), + country VARCHAR(3), + seal VARCHAR(49), + insurance_company_name VARCHAR(30), + insurance_number VARCHAR(20), + insurance_amount NUMERIC(13, 2), + insurance_date INTEGER, + box_number VARCHAR(300), + brand VARCHAR(20), + year VARCHAR(4), + series VARCHAR(30), + description VARCHAR(100), + engine_number VARCHAR(50), + sct_permission VARCHAR(40), + color VARCHAR(20), + container_key VARCHAR(3), + tenant_id INTEGER NOT NULL REFERENCES core.tenants(id), + company_id INTEGER NOT NULL REFERENCES a76.company(id), + created_at TIMESTAMP NOT NULL DEFAULT now(), + updated_at TIMESTAMP NOT NULL DEFAULT now(), + deleted_at TIMESTAMP + ); + """, + ] + + with core_engine.connect() as conn: + for stmt in ddl_statements: + conn.execute(text(stmt)) + # Ampliar columnas que pudieron haberse creado con tamaño incorrecto + conn.execute(text(""" + DO $$ + BEGIN + -- Fix transporter_key si fue creada como VARCHAR(5) + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='a76' AND table_name='transporter' + AND column_name='transporter_key' + AND character_maximum_length < 23 + ) THEN + ALTER TABLE a76.transporter ALTER COLUMN transporter_key TYPE VARCHAR(23); + END IF; + -- Fix filler_code si fue creada como VARCHAR(4) + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema='a76' AND table_name='transporter' + AND column_name='filler_code' + AND character_maximum_length < 20 + ) THEN + ALTER TABLE a76.transporter ALTER COLUMN filler_code TYPE VARCHAR(20); + END IF; + + -- Eliminar constraint de trailer_type_key en trailer si existe + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name='trailer_trailer_type_key_fkey' + AND table_schema='a76' AND table_name='trailer' + ) THEN + ALTER TABLE a76.trailer DROP CONSTRAINT trailer_trailer_type_key_fkey; + END IF; + END$$; + """)) + conn.commit() + logger.info("Tablas de transporte verificadas/creadas correctamente.") + + # Inicializar la base de datos @app.on_event("startup") async def on_startup(): @@ -152,6 +288,7 @@ async def on_startup(): logger.info("Iniciando la aplicación Anexo76...") init_db() run_migrations() + create_transportation_tables() logger.info("Base de datos inicializada correctamente.") @@ -265,6 +402,10 @@ def register_audit(): CustomsBroker, Part, Company, + # Transportation Modules + Trailer, + Transporter, + Vehicle, # Reference Data Country, CurrencyType, diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 7e328ea8..9cffaf74 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -13,4 +13,4 @@ declare global { } } -export {}; +export { }; diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts index 2b10b734..28e77fcd 100644 --- a/frontend/src/lib/api/dashboard/a76/items.ts +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -18,7 +18,7 @@ export interface LineCustoms { destination_country?: string; advalorem?: string; advalorem_numeric?: number; - advalorem_american?: number; + advalorem_american?: number; advalorem_tlcan?: number; rate?: string; depreciation_rate?: number; @@ -33,7 +33,7 @@ export interface LineFinancials { unit_cost_mxn?: number; unit_cost_capture?: number; unit_cost_commercial_usd?: number; - + // Values value_mc?: number; value_usd?: number; @@ -49,7 +49,7 @@ export interface LineQuantities { line_item_id?: number; quantity?: number; unit_of_measure?: string; - + // Special quantities quantity_temp_export?: number; quantity_returned?: number; @@ -92,7 +92,7 @@ export interface FaLineItem { id?: number; tenant_id?: number; company_id?: number; - + // Asset information (SCAF specific) asset_number?: string; asset_photo?: string; @@ -101,44 +101,46 @@ export interface FaLineItem { return_import_invoice?: string; return_import_date?: number; movement_type_import?: string; - + // Cross-references for import repair search_invoice?: string; search_line?: number; - + // Search type search_type?: string; - - // Subitems - is_subitem?: boolean; - contains_subitems?: boolean; - subitem_number?: number; + + // Subitems + is_subitem?: boolean; + contains_subitems?: boolean; + subitem_number?: number; // Special flags download?: boolean; own_equipment?: boolean; - omit_annex31?: boolean; - + omit_annex31?: boolean; + // Timestamps created_at?: string; updated_at?: string; } export interface Item { - id?: number; - invoice_id: number; + id?: number; + invoice_id: number; line_number: number; - + // Identification part_number?: string; + part_number_id?: number; component_part_number?: string; + component_part_number_id?: number; class_id?: number; identifier?: string; // Unit of Measure unit_of_measure?: number; alternate_unit?: number; - + // Permits permit_number?: string; page_line?: string; @@ -151,29 +153,29 @@ export interface Item { includes_subitems?: boolean; tax_payment?: boolean; is_military_mcia?: boolean; - + // Payment payment_method?: string; igi_amount?: number; - + // Additional notes wildcard_field?: string; // Computed fields from class_info relation class_code?: string; class_description?: string; - + // Computed field from unit_of_measure_info relation - unit_of_measure_code?: string; - reference_number?: string; - order?: string; - guide_number?: string; - depreciation_date?: number; - rectification?: number; - warehouse?: string; - location?: string; - created_at?: string; - updated_at?: string; + unit_of_measure_code?: string; + reference_number?: string; + order?: string; + guide_number?: string; + depreciation_date?: number; + rectification?: number; + warehouse?: string; + location?: string; + created_at?: string; + updated_at?: string; // Nested relations (Singular names to match backend Pydantic models) customs?: LineCustoms; @@ -185,86 +187,86 @@ export interface Item { } export interface ItemListResponse { - items: Item[]; - total: number; - skip: number; - limit: number; + items: Item[]; + total: number; + skip: number; + limit: number; } export interface CreateItemData extends Omit { - invoice_id: number; + invoice_id: number; } -export interface UpdateItemData extends Partial> {} +export interface UpdateItemData extends Partial> { } /** * API para Items */ export const itemsApi = { - /** - * Lista todos los items con paginación - */ - list: (companyId: number, skip = 0, limit = 100, invoiceId?: number) => { - const params = new URLSearchParams({ - company_id: companyId.toString(), - skip: skip.toString(), - limit: limit.toString() - }); + /** + * Lista todos los items con paginación + */ + list: (companyId: number, skip = 0, limit = 100, invoiceId?: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString(), + skip: skip.toString(), + limit: limit.toString() + }); - if (invoiceId) { - params.append('invoice_id', invoiceId.toString()); - } + if (invoiceId) { + params.append('invoice_id', invoiceId.toString()); + } - return api.get(`/v1/a76/items/?${params.toString()}`); - }, + return api.get(`/v1/a76/items/?${params.toString()}`); + }, - /** - * Lista items por invoice ID - */ - listByInvoice: (invoiceId: number, companyId: number) => { - const params = new URLSearchParams({ - company_id: companyId.toString() - }); - return api.get(`/v1/a76/items/invoice/${invoiceId}/items/?${params.toString()}`); - }, + /** + * Lista items por invoice ID + */ + listByInvoice: (invoiceId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`/v1/a76/items/invoice/${invoiceId}/items/?${params.toString()}`); + }, - /** - * Obtiene un item por ID - */ - get: (itemId: number, companyId: number) => { - const params = new URLSearchParams({ - company_id: companyId.toString() - }); - return api.get(`/v1/a76/items/${itemId}/?${params.toString()}`); - }, + /** + * Obtiene un item por ID + */ + get: (itemId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`/v1/a76/items/${itemId}/?${params.toString()}`); + }, - /** - * Crea un nuevo item - */ - create: (companyId: number, data: CreateItemData) => { - const params = new URLSearchParams({ - company_id: companyId.toString() - }); - return api.post(`/v1/a76/items/?${params.toString()}`, data); - }, + /** + * Crea un nuevo item + */ + create: (companyId: number, data: CreateItemData) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`/v1/a76/items/?${params.toString()}`, data); + }, - /** - * Actualiza un item existente - */ - update: (itemId: number, companyId: number, data: UpdateItemData) => { - const params = new URLSearchParams({ - company_id: companyId.toString() - }); - return api.put(`/v1/a76/items/${itemId}/?${params.toString()}`, data); - }, + /** + * Actualiza un item existente + */ + update: (itemId: number, companyId: number, data: UpdateItemData) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put(`/v1/a76/items/${itemId}/?${params.toString()}`, data); + }, - /** - * Elimina un item - */ - delete: (itemId: number, companyId: number) => { - const params = new URLSearchParams({ - company_id: companyId.toString() - }); - return api.delete(`/v1/a76/items/${itemId}/?${params.toString()}`); - } + /** + * Elimina un item + */ + delete: (itemId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`/v1/a76/items/${itemId}/?${params.toString()}`); + } }; diff --git a/frontend/src/lib/api/dashboard/a76/pedimentos.ts b/frontend/src/lib/api/dashboard/a76/pedimentos.ts index 582e913d..f36ccc30 100644 --- a/frontend/src/lib/api/dashboard/a76/pedimentos.ts +++ b/frontend/src/lib/api/dashboard/a76/pedimentos.ts @@ -33,12 +33,19 @@ export interface PedimentoPayments { } export interface PedimentoTransportMeans { + id?: number; destination?: number | null; entry_exit?: string | null; arrival?: string | null; departure?: string | null; } +export interface PedimentoCustomsOffices { + id?: number; + dispatch_customs?: string | null; + entry_exit_customs?: string | null; +} + export interface PedimentoValidation { validator?: string | null; validation_ack?: string | null; @@ -180,6 +187,7 @@ export interface Pedimento { pedimento_incrementables?: PedimentoIncrementables | null; pedimento_decrementables?: PedimentoDecrementables | null; pedimento_indexes?: PedimentoIndexes | null; + pedimento_customs_offices?: PedimentoCustomsOffices | null; pedimento_config_additional?: PedimentoConfigAdditional | null; pedimento_config_calculations?: PedimentoConfigCalculations | null; pedimento_config_surcharges?: PedimentoConfigSurcharges | null; @@ -220,6 +228,7 @@ export interface CreatePedimentoData { pedimento_incrementables?: PedimentoIncrementables | null; pedimento_decrementables?: PedimentoDecrementables | null; pedimento_indexes?: PedimentoIndexes | null; + pedimento_customs_offices?: PedimentoCustomsOffices | null; pedimento_config_additional?: PedimentoConfigAdditional | null; pedimento_config_calculations?: PedimentoConfigCalculations | null; pedimento_config_surcharges?: PedimentoConfigSurcharges | null; @@ -253,6 +262,7 @@ export interface UpdatePedimentoData { pedimento_incrementables?: PedimentoIncrementables | null; pedimento_decrementables?: PedimentoDecrementables | null; pedimento_indexes?: PedimentoIndexes | null; + pedimento_customs_offices?: PedimentoCustomsOffices | null; pedimento_config_additional?: PedimentoConfigAdditional | null; pedimento_config_calculations?: PedimentoConfigCalculations | null; pedimento_config_surcharges?: PedimentoConfigSurcharges | null; diff --git a/frontend/src/lib/api/dashboard/a76/trailers.ts b/frontend/src/lib/api/dashboard/a76/trailers.ts index df739414..5d74aa2d 100644 --- a/frontend/src/lib/api/dashboard/a76/trailers.ts +++ b/frontend/src/lib/api/dashboard/a76/trailers.ts @@ -2,10 +2,17 @@ import { api, type ApiResponse } from '$lib/api'; export interface Trailer { trailer_number: string; - plate_number?: string; + ace_trailer_number?: string; trailer_type_key?: string; - is_active: boolean; - tenant_id?: string; + seal?: string; + entity_code?: string; + plate_number?: string; + state?: string; + country?: string; + container_key?: string; + is_active?: boolean; + company_id?: number | string; + tenant_id?: number | string; } export interface TrailerResponse { @@ -16,7 +23,7 @@ export interface TrailerResponse { } class TrailersApi { - private baseUrl = '/v1/a76/trailers'; + private baseUrl = '/v1/a76/transportation/trailers'; async list( companyId: string | number, @@ -35,6 +42,27 @@ class TrailersApi { }); return api.get(`${this.baseUrl}/${id}?${queryParams.toString()}`); } + + async create(data: Trailer, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`${this.baseUrl}?${queryParams.toString()}`, data); + } + + async update(id: string, data: Trailer, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put(`${this.baseUrl}/${id}/?${queryParams.toString()}`, data); + } + + async delete(id: string, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`${this.baseUrl}/${id}?${queryParams.toString()}`); + } } export const trailersApi = new TrailersApi(); diff --git a/frontend/src/lib/api/dashboard/a76/transporters.ts b/frontend/src/lib/api/dashboard/a76/transporters.ts index 6383828b..70ecbcdd 100644 --- a/frontend/src/lib/api/dashboard/a76/transporters.ts +++ b/frontend/src/lib/api/dashboard/a76/transporters.ts @@ -2,10 +2,26 @@ import { api, type ApiResponse } from '$lib/api'; export interface Transporter { transporter_key: string; - name: string; + name?: string; + short_name?: string; + responsible?: string; rfc?: string; - is_active: boolean; - tenant_id?: string; + streets?: string; + postal_code?: string; + city?: string; + state?: string; + country?: string; + loader_code?: string; + caat_code?: string; + transport_code?: string; + transport_interface_type?: string; + ftp_server?: string; + ftp_user?: string; + ftp_password?: string; + ftp_directory?: string; + filler_code?: string; + company_id?: number | string; + tenant_id?: number | string; } export interface TransporterResponse { @@ -35,6 +51,27 @@ class TransportersApi { }); return api.get(`${this.baseUrl}/${id}?${queryParams.toString()}`); } + + async create(data: Transporter, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`${this.baseUrl}?${queryParams.toString()}`, data); + } + + async update(id: string, data: Transporter, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put(`${this.baseUrl}/${id}/?${queryParams.toString()}`, data); + } + + async delete(id: string, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`${this.baseUrl}/${id}?${queryParams.toString()}`); + } } export const transportersApi = new TransportersApi(); diff --git a/frontend/src/lib/api/dashboard/a76/vehicles.ts b/frontend/src/lib/api/dashboard/a76/vehicles.ts index a0c864e4..3fd7d67a 100644 --- a/frontend/src/lib/api/dashboard/a76/vehicles.ts +++ b/frontend/src/lib/api/dashboard/a76/vehicles.ts @@ -1,32 +1,84 @@ -import { api } from '$lib/api'; +import { api, type ApiResponse } from '$lib/api'; export interface Vehicle { vehicle_key: string; - brand?: string; - plate_number?: string; - description?: string; + ace_vehicle_key?: string; + transporter_key?: string; + transport_identifier?: string; transport_type?: string; + entity_code?: string; + transponder_number?: string; + dot_number?: string; + plate_number?: string; + city?: string; + state?: string; + country?: string; + seal?: string; + insurance_company_name?: string; + insurance_number?: string; + insurance_amount?: number; + insurance_date?: number; + box_number?: string; + brand?: string; year?: string; + series?: string; + description?: string; + engine_number?: string; + sct_permission?: string; + color?: string; + container_key?: string; + company_id?: number | string; + tenant_id?: number | string; } -export interface VehicleListResponse { +export interface VehicleResponse { items: Vehicle[]; total: number; + page: number; + page_size: number; } -/** - * API para Vehículos - */ -export const vehiclesApi = { - list: (companyId: string, page = 1, pageSize = 50) => { - return api.get( - `/v1/a76/transportation/vehicles?company_id=${companyId}&page=${page}&page_size=${pageSize}` - ); - }, +class VehiclesApi { + private baseUrl = '/v1/a76/transportation/vehicles'; - get: (vehicleKey: string, companyId: string) => { - return api.get( - `/v1/a76/transportation/vehicles/${vehicleKey}?company_id=${companyId}` - ); + async list( + companyId: string | number, + params?: Record + ): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString(), + ...params + }); + return api.get(`${this.baseUrl}?${queryParams.toString()}`); } -}; + + async get(id: string, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`${this.baseUrl}/${id}?${queryParams.toString()}`); + } + + async create(data: Vehicle, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`${this.baseUrl}?${queryParams.toString()}`, data); + } + + async update(id: string, data: Vehicle, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put(`${this.baseUrl}/${id}/?${queryParams.toString()}`, data); + } + + async delete(id: string, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`${this.baseUrl}/${id}?${queryParams.toString()}`); + } +} + +export const vehiclesApi = new VehiclesApi(); diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte index 372198ff..8bc9b75a 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -20,7 +20,7 @@ } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; - import { itemsApi, type Item } from '$lib/api/dashboard/a76/items'; + import { itemsApi, type Item as InvoiceItem } from '$lib/api/dashboard/a76/items'; import { companyStore } from '$lib/stores/company.svelte'; import ItemSheetFa from './fa/item-sheet-fa.svelte'; import ItemSheetInv from './inv/item-sheet-inv.svelte'; @@ -43,7 +43,7 @@ } = $props(); // 1. Core State - let items = $state([]); + let items = $state([]); let displayedItems = $state([]); let imported = $state(0); let net_weight = $state(0); @@ -129,9 +129,9 @@ let showItemSheet = $state(false); let isEditMode = $state(false); let showDeleteDialog = $state(false); - let selectedItem = $state(null); - let originalItemData = $state | null>(null); - let editingItem = $state>({ + let selectedItem = $state(null); + let originalItemData = $state | null>(null); + let editingItem = $state>({ invoice_id: undefined, reference_number: '', order: '', @@ -400,7 +400,7 @@ return cleanLineData({ ...rest }); } - function cloneItemForPreset(item: Item) { + function cloneItemForPreset(item: InvoiceItem) { const { id, tenant_id, company_id, created_at, updated_at, temp_id, ...rest } = item as any; return { ...sanitizeLineForPreset(rest), @@ -502,7 +502,7 @@ isSavingPreset = true; try { // We group everything as items for the template - const lines = builderItems.map((item: Item, idx: number) => { + const lines = builderItems.map((item: InvoiceItem, idx: number) => { return { ...cleanLineData(item), line_number: item.line_number || idx + 1, // Ensure line_number is present @@ -552,7 +552,7 @@ } // Enrich item with descriptive data for display - async function enrichItemData(item: Partial) { + async function enrichItemData(item: Partial) { if (!item || !activeCompanyId) return; // Load class data @@ -697,7 +697,7 @@ } // Normalize numeric values from strings to numbers - function normalizeItemData(item: Partial): Partial { + function normalizeItemData(item: Partial): Partial { if (item) { const normalizedItem = { ...item }; diff --git a/frontend/src/lib/components/dashboard/transportation/trailers/columns.ts b/frontend/src/lib/components/dashboard/transportation/trailers/columns.ts new file mode 100644 index 00000000..e00011c6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/trailers/columns.ts @@ -0,0 +1,64 @@ +/** + * Definición de columnas para la tabla de Trailers + */ +import type { Trailer } from '$lib/api/dashboard/a76/trailers'; +import type { ColumnDef } from '@tanstack/table-core'; +import { renderComponent } from '$lib/components/ui/data-table'; +import DataTableActions from './data-table-actions.svelte'; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: 'trailer_number', + header: 'Número de Trailer', + cell: ({ row }) => { + return row.original.trailer_number; + } + }, + { + accessorKey: 'plate_number', + header: 'Placas', + cell: ({ row }) => { + return row.original.plate_number || '-'; + } + }, + { + accessorKey: 'trailer_type_key', + header: 'Tipo de Trailer', + cell: ({ row }) => { + return row.original.trailer_type_key || '-'; + } + }, + { + accessorKey: 'container_key', + header: 'Contenedor', + cell: ({ row }) => { + return row.original.container_key || '-'; + } + }, + { + accessorKey: 'state', + header: 'Estado', + cell: ({ row }) => { + return row.original.state || '-'; + } + }, + { + accessorKey: 'country', + header: 'País', + cell: ({ row }) => { + return row.original.country || '-'; + } + }, + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => { + return renderComponent(DataTableActions, { + item: row.original, + onSuccess + }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte new file mode 100644 index 00000000..6959aed2 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte @@ -0,0 +1,207 @@ + + + + + + {title} + + {isEdit + ? 'Modifica los datos del trailer' + : 'Completa los datos para crear un nuevo trailer'} + + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="space-y-6" + > + {#if error} +
+ {error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/transportation/trailers/data-table-actions.svelte b/frontend/src/lib/components/dashboard/transportation/trailers/data-table-actions.svelte new file mode 100644 index 00000000..6dce1ab9 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/trailers/data-table-actions.svelte @@ -0,0 +1,111 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Acciones + + + + Editar + + + + {#if loading} + + {:else} + + {/if} + Eliminar + + + + + diff --git a/frontend/src/lib/components/dashboard/transportation/trailers/data-table.svelte b/frontend/src/lib/components/dashboard/transportation/trailers/data-table.svelte new file mode 100644 index 00000000..e0cdf7c1 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/trailers/data-table.svelte @@ -0,0 +1,99 @@ + + +
+ + + {#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} + + +
+ +
+
+ Total: {totalItems} +
+
+ + +
+
diff --git a/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte new file mode 100644 index 00000000..0cc296fd --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte @@ -0,0 +1,306 @@ + + + + + + {title} + + {isEdit + ? 'Modifica los datos del transportista' + : 'Completa los datos para crear un nuevo transportista'} + + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="space-y-6" + > + {#if error} +
+ {error} +
+ {/if} + +
+ +
+

Información General

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

Códigos de Transporte

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

Dirección

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

Configuración FTP

+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/transportation/transporters/data-table-actions.svelte b/frontend/src/lib/components/dashboard/transportation/transporters/data-table-actions.svelte new file mode 100644 index 00000000..8715b474 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/transporters/data-table-actions.svelte @@ -0,0 +1,114 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Acciones + + + + Editar + + + + {#if loading} + + {:else} + + {/if} + Eliminar + + + + + diff --git a/frontend/src/lib/components/dashboard/transportation/transporters/data-table.svelte b/frontend/src/lib/components/dashboard/transportation/transporters/data-table.svelte new file mode 100644 index 00000000..e0cdf7c1 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/transporters/data-table.svelte @@ -0,0 +1,99 @@ + + +
+ + + {#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} + + +
+ +
+
+ Total: {totalItems} +
+
+ + +
+
diff --git a/frontend/src/lib/components/dashboard/transportation/transporters/transporter-columns.ts b/frontend/src/lib/components/dashboard/transportation/transporters/transporter-columns.ts new file mode 100644 index 00000000..32e69333 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/transporters/transporter-columns.ts @@ -0,0 +1,64 @@ +/** + * Definición de columnas para la tabla de Transportistas + */ +import type { Transporter } from '$lib/api/dashboard/a76/transporters'; +import type { ColumnDef } from '@tanstack/table-core'; +import { renderComponent } from '$lib/components/ui/data-table'; +import DataTableActions from './data-table-actions.svelte'; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: 'transporter_key', + header: 'Clave', + cell: ({ row }) => { + return row.original.transporter_key; + } + }, + { + accessorKey: 'name', + header: 'Nombre', + cell: ({ row }) => { + return row.original.name || '-'; + } + }, + { + accessorKey: 'short_name', + header: 'Nombre Corto', + cell: ({ row }) => { + return row.original.short_name || '-'; + } + }, + { + accessorKey: 'rfc', + header: 'RFC', + cell: ({ row }) => { + return row.original.rfc || '-'; + } + }, + { + accessorKey: 'caat_code', + header: 'CAAT', + cell: ({ row }) => { + return row.original.caat_code || '-'; + } + }, + { + accessorKey: 'transport_code', + header: 'Código Transporte', + cell: ({ row }) => { + return row.original.transport_code || '-'; + } + }, + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => { + return renderComponent(DataTableActions, { + item: row.original, + onSuccess + }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/transportation/vehicles/columns.ts b/frontend/src/lib/components/dashboard/transportation/vehicles/columns.ts new file mode 100644 index 00000000..2bacea83 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/vehicles/columns.ts @@ -0,0 +1,64 @@ +/** + * Definición de columnas para la tabla de Vehículos + */ +import type { Vehicle } from '$lib/api/dashboard/a76/vehicles'; +import type { ColumnDef } from '@tanstack/table-core'; +import { renderComponent } from '$lib/components/ui/data-table'; +import DataTableActions from './data-table-actions.svelte'; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: 'vehicle_key', + header: 'Clave', + cell: ({ row }) => { + return row.original.vehicle_key; + } + }, + { + accessorKey: 'brand', + header: 'Marca', + cell: ({ row }) => { + return row.original.brand || '-'; + } + }, + { + accessorKey: 'year', + header: 'Año', + cell: ({ row }) => { + return row.original.year || '-'; + } + }, + { + accessorKey: 'plate_number', + header: 'Placas', + cell: ({ row }) => { + return row.original.plate_number || '-'; + } + }, + { + accessorKey: 'transporter_key', + header: 'Transportista', + cell: ({ row }) => { + return row.original.transporter_key || '-'; + } + }, + { + accessorKey: 'transport_type', + header: 'Tipo Transporte', + cell: ({ row }) => { + return row.original.transport_type || '-'; + } + }, + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => { + return renderComponent(DataTableActions, { + item: row.original, + onSuccess + }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte new file mode 100644 index 00000000..313dd45b --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte @@ -0,0 +1,317 @@ + + + + + + {title} + + {isEdit + ? 'Modifica los datos del vehículo' + : 'Completa los datos para crear un nuevo vehículo de transporte'} + + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="space-y-6" + > + {#if error} +
+ {error} +
+ {/if} + +
+ +
+

Identificación del Vehículo

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

Datos de Transporte

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

Seguro y Otros

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

Ubicación y Detalles

+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/transportation/vehicles/data-table-actions.svelte b/frontend/src/lib/components/dashboard/transportation/vehicles/data-table-actions.svelte new file mode 100644 index 00000000..9ec4eb00 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/vehicles/data-table-actions.svelte @@ -0,0 +1,111 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Acciones + + + + Editar + + + + {#if loading} + + {:else} + + {/if} + Eliminar + + + + + diff --git a/frontend/src/lib/components/dashboard/transportation/vehicles/data-table.svelte b/frontend/src/lib/components/dashboard/transportation/vehicles/data-table.svelte new file mode 100644 index 00000000..e0cdf7c1 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/vehicles/data-table.svelte @@ -0,0 +1,99 @@ + + +
+ + + {#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} + + +
+ +
+
+ Total: {totalItems} +
+
+ + +
+
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 5e7fe6ae..5ab973b2 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -15,6 +15,7 @@ import { Settings2, Shield, Ship, + Truck, Users, } from 'lucide-svelte'; import * as m from "$lib/paraglide/messages.js"; @@ -192,6 +193,7 @@ export function getSidebarData(): SidebarData { title: m["sidebar.general_catalogs.identifiers"](), url: "/dashboard/general_catalogs/identifiers", }, + // ------------------------------------- { title: m["sidebar.general_catalogs.incoterms"](), url: "/dashboard/reference_data/incoterms", @@ -333,6 +335,25 @@ export function getSidebarData(): SidebarData { }, ], }, + { + title: "Transportes", + url: "#", + icon: Truck, + items: [ + { + title: "Transportistas", + url: "/dashboard/general_catalogs/transporters", + }, + { + title: "Trailers", + url: "/dashboard/general_catalogs/trailers", + }, + { + title: "Vehículos", + url: "/dashboard/general_catalogs/vehicles", + }, + ], + }, { title: m["sidebar.goods.title"](), url: "#", @@ -522,4 +543,4 @@ export function getSidebarData(): SidebarData { } // Exportar también como constante para compatibilidad (deprecado) -export const sidebarData: SidebarData = getSidebarData(); +export const sidebarData: SidebarData = getSidebarData(); \ No newline at end of file diff --git a/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte new file mode 100644 index 00000000..596c3d13 --- /dev/null +++ b/frontend/src/routes/dashboard/general_catalogs/trailers/+page.svelte @@ -0,0 +1,117 @@ + + +
+
+
+

Trailers

+

Gestión del catálogo de trailers de la compañía

+
+ +
+ +
+ + +
+ + {#if loading && data.length === 0} +
+ Cargando trailers... +
+ {:else} + + {/if} + + +
diff --git a/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte new file mode 100644 index 00000000..150e006b --- /dev/null +++ b/frontend/src/routes/dashboard/general_catalogs/transporters/+page.svelte @@ -0,0 +1,117 @@ + + +
+
+
+

Transportistas

+

Gestión del catálogo de líneas transportistas

+
+ +
+ +
+ + +
+ + {#if loading && data.length === 0} +
+ Cargando transportistas... +
+ {:else} + + {/if} + + +
diff --git a/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte new file mode 100644 index 00000000..1bcc8916 --- /dev/null +++ b/frontend/src/routes/dashboard/general_catalogs/vehicles/+page.svelte @@ -0,0 +1,119 @@ + + +
+
+
+

Vehículos (Transporte)

+

+ Gestión del catálogo de camiones y vehículos de transporte +

+
+ +
+ +
+ + +
+ + {#if loading && data.length === 0} +
+ Cargando vehículos... +
+ {:else} + + {/if} + + +
diff --git a/frontend/src/svelte-shims.d.ts b/frontend/src/svelte-shims.d.ts new file mode 100644 index 00000000..185afbcb --- /dev/null +++ b/frontend/src/svelte-shims.d.ts @@ -0,0 +1,7 @@ +// Ambient type declarations for .svelte files +// This must be a script (no top-level import/export) to be globally ambient +declare module "*.svelte" { + import type { Component } from "svelte"; + const component: Component; + export default component; +}