diff --git a/backend/api/v1/modules/a76/transportation/drivers/dto.py b/backend/api/v1/modules/a76/transportation/drivers/dto.py index bfb3d8fc..02420ea8 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/dto.py +++ b/backend/api/v1/modules/a76/transportation/drivers/dto.py @@ -36,6 +36,33 @@ class DriverCreateDTO(DriverBaseDTO): pass +class DriverUpdateDTO(BaseModel): + """All fields optional; transporter_key, line, company_id, tenant_id are not updated.""" + + driver_name: Optional[str] = None + license_number: Optional[str] = None + express_line_id: Optional[str] = None + ace_id: Optional[str] = None + birth_date: Optional[int] = None + gender: Optional[str] = None + birth_country: Optional[str] = None + hazardous_material_auth: Optional[str] = None + hazardous_material_state: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + id_key1: Optional[str] = None + id_number1: Optional[str] = None + id_state1: Optional[str] = None + id_country1: Optional[str] = None + id_key2: Optional[str] = None + id_number2: Optional[str] = None + id_state2: Optional[str] = None + id_country2: Optional[str] = None + badge_number: Optional[str] = None + class_type: Optional[str] = None + unique_badge_number: Optional[str] = None + + class DriverResponseDTO(DriverBaseDTO): class Config: from_attributes = True diff --git a/backend/api/v1/modules/a76/transportation/drivers/routes.py b/backend/api/v1/modules/a76/transportation/drivers/routes.py index eb0d61a7..f449adab 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/routes.py +++ b/backend/api/v1/modules/a76/transportation/drivers/routes.py @@ -5,7 +5,7 @@ from core.security import get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session -from .dto import DriverCreateDTO, DriverResponseDTO +from .dto import DriverCreateDTO, DriverResponseDTO, DriverUpdateDTO from .models import Driver from .services import DriverService from api.v1.modules.a76.layouts_csv.drivers.routes import router as imports_router @@ -66,6 +66,29 @@ async def create_driver( return DriverService.create_driver(db, driver_data) +@router.put("/{transporter_key}/{line}", response_model=DriverResponseDTO) +async def update_driver( + transporter_key: str, + line: int, + driver_data: DriverUpdateDTO, + company_id: int = Query(..., description="Company ID for filtering"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + driver = DriverService.update_driver( + db, + transporter_key, + line, + str(company_id), + tenant_id, + driver_data, + ) + if not driver: + raise HTTPException(status_code=404, detail="Driver not found") + return driver + + @router.delete("/{transporter_key}/{line}", status_code=status.HTTP_204_NO_CONTENT) async def delete_driver( transporter_key: str, diff --git a/backend/api/v1/modules/a76/transportation/drivers/services.py b/backend/api/v1/modules/a76/transportation/drivers/services.py index c22a49c0..d9d9d261 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/services.py +++ b/backend/api/v1/modules/a76/transportation/drivers/services.py @@ -40,6 +40,27 @@ class DriverService: db.refresh(new_driver) return new_driver + @staticmethod + def update_driver( + db: Session, + transporter_key: str, + line: int, + company_id: str, + tenant_id: Optional[str], + data: dto.DriverUpdateDTO, + ) -> Optional[models.Driver]: + driver = DriverService.get_driver_by_key_and_line( + db, transporter_key, line, company_id, tenant_id + ) + if not driver: + return None + update_data = data.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(driver, key, value) + db.commit() + db.refresh(driver) + return driver + @staticmethod def delete_driver( db: Session, diff --git a/frontend/src/lib/api/dashboard/a76/drivers.ts b/frontend/src/lib/api/dashboard/a76/drivers.ts index cb0725b1..3ebc7354 100644 --- a/frontend/src/lib/api/dashboard/a76/drivers.ts +++ b/frontend/src/lib/api/dashboard/a76/drivers.ts @@ -1,57 +1,105 @@ import { api, type ApiResponse } from '$lib/api'; export interface Driver { - transporter_key: string; - line: number; - driver_name?: string; - license_number?: string; - express_line_id?: string; - ace_id?: string; - birth_date?: number; - gender?: string; - birth_country?: string; - hazardous_material_auth?: string; - hazardous_material_state?: string; - first_name?: string; - last_name?: string; - badge_number?: string; - class_type?: string; + transporter_key: string; + line: number; + driver_name?: string; + license_number?: string; + express_line_id?: string; + ace_id?: string; + birth_date?: number; + gender?: string; + birth_country?: string; + hazardous_material_auth?: string; + hazardous_material_state?: string; + first_name?: string; + last_name?: string; + id_key1?: string; + id_number1?: string; + id_state1?: string; + id_country1?: string; + id_key2?: string; + id_number2?: string; + id_state2?: string; + id_country2?: string; + badge_number?: string; + class_type?: string; + unique_badge_number?: string; + company_id?: number; + tenant_id?: number; } export interface DriverResponse { - items: Driver[]; - total: number; - page: number; - page_size: number; + items: Driver[]; + total: number; + page: number; + page_size: number; } class DriversApi { - private baseUrl = '/v1/a76/drivers'; + private baseUrl = '/v1/a76/drivers'; - async list( - companyId: string | number, - page: number = 1, - pageSize: number = 50 - ): Promise> { - const params = new URLSearchParams({ - company_id: companyId.toString(), - page: page.toString(), - page_size: pageSize.toString() - }); + async list( + companyId: string | number, + params?: Record + ): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString(), + ...(params as Record) + }); + return api.get(`${this.baseUrl}?${queryParams.toString()}`); + } - return api.get(`${this.baseUrl}?${params.toString()}`); - } + async get( + transporterKey: string, + line: number, + companyId: string | number + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get( + `${this.baseUrl}/${transporterKey}/${line}?${params.toString()}` + ); + } - async get( - transporterKey: string, - line: number, - companyId: string | number - ): Promise> { - const params = new URLSearchParams({ - company_id: companyId.toString() - }); - return api.get(`${this.baseUrl}/${transporterKey}/${line}?${params.toString()}`); - } + async create( + data: Omit & { company_id: number; tenant_id: number }, + companyId: string | number + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`${this.baseUrl}?${params.toString()}`, data); + } + + async update( + transporterKey: string, + line: number, + data: Partial, + companyId: string | number + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put( + `${this.baseUrl}/${transporterKey}/${line}?${params.toString()}`, + data + ); + } + + async delete( + transporterKey: string, + line: number, + companyId: string | number + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete( + `${this.baseUrl}/${transporterKey}/${line}?${params.toString()}` + ); + } } export const driversApi = new DriversApi(); diff --git a/frontend/src/lib/api/dashboard/a76/transporters.ts b/frontend/src/lib/api/dashboard/a76/transporters.ts index 70ecbcdd..fa585465 100644 --- a/frontend/src/lib/api/dashboard/a76/transporters.ts +++ b/frontend/src/lib/api/dashboard/a76/transporters.ts @@ -34,16 +34,22 @@ export interface TransporterResponse { class TransportersApi { private baseUrl = '/v1/a76/transporters'; - 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 list( + companyId: string | number, + params?: Record + ): Promise> { + const raw: Record = { + company_id: companyId.toString(), + ...params + }; + const queryParams = new URLSearchParams(); + for (const [k, v] of Object.entries(raw)) { + if (v != null && v !== '') { + queryParams.set(k, String(v)); + } + } + return api.get(`${this.baseUrl}?${queryParams.toString()}`); + } async get(id: string, companyId: string | number): Promise> { const queryParams = new URLSearchParams({ diff --git a/frontend/src/lib/components/dashboard/transportation/drivers/columns.ts b/frontend/src/lib/components/dashboard/transportation/drivers/columns.ts new file mode 100644 index 00000000..35bc7c79 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/drivers/columns.ts @@ -0,0 +1,52 @@ +/** + * Definición de columnas para la tabla de Conductores + */ +import type { Driver } from '$lib/api/dashboard/a76/drivers'; +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 Transportista', + cell: ({ row }) => row.original.transporter_key + }, + { + accessorKey: 'line', + header: 'Línea', + cell: ({ row }) => row.original.line + }, + { + accessorKey: 'driver_name', + header: 'Nombre del Conductor', + cell: ({ row }) => row.original.driver_name || '-' + }, + { + accessorKey: 'license_number', + header: 'Número de Licencia', + cell: ({ row }) => row.original.license_number || '-' + }, + { + accessorKey: 'first_name', + header: 'Nombre', + cell: ({ row }) => row.original.first_name || '-' + }, + { + accessorKey: 'last_name', + header: 'Apellido', + cell: ({ row }) => row.original.last_name || '-' + }, + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => { + return renderComponent(DataTableActions, { + item: row.original, + onSuccess + }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/transportation/drivers/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/drivers/create-edit-dialog.svelte new file mode 100644 index 00000000..10304064 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/drivers/create-edit-dialog.svelte @@ -0,0 +1,313 @@ + + + + + + {title} + + {isEdit + ? 'Modifica los datos del conductor' + : 'Completa los datos para crear un nuevo conductor'} + + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="space-y-6" + > + {#if error} +
+ {error} +
+ {/if} + +
+
+ + {#if isEdit} + + {:else} + + + {transportersLoading + ? 'Cargando transportistas...' + : transporters.length === 0 + ? 'No hay transportistas' + : transporters.find((t) => t.transporter_key === formData.transporter_key) + ? `${formData.transporter_key} - ${transporters.find((t) => t.transporter_key === formData.transporter_key)?.name || ''}` + : 'Seleccionar transportista'} + + + {#each transporters as t} + + {t.transporter_key} — {t.name || t.short_name || 'Sin nombre'} + + {/each} + {#if !transportersLoading && transporters.length === 0} +
+ No hay transportistas. Crea uno en el catálogo Transportistas. +
+ {/if} +
+
+ {/if} +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/transportation/drivers/data-table-actions.svelte b/frontend/src/lib/components/dashboard/transportation/drivers/data-table-actions.svelte new file mode 100644 index 00000000..442c66c6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/drivers/data-table-actions.svelte @@ -0,0 +1,116 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Acciones + + + + Editar + + + + {#if loading} + + {:else} + + {/if} + Eliminar + + + + + diff --git a/frontend/src/lib/components/dashboard/transportation/drivers/data-table.svelte b/frontend/src/lib/components/dashboard/transportation/drivers/data-table.svelte new file mode 100644 index 00000000..e0cdf7c1 --- /dev/null +++ b/frontend/src/lib/components/dashboard/transportation/drivers/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 812103f5..0566199d 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -344,6 +344,10 @@ export function getSidebarData(): SidebarData { title: "Transportistas", url: "/dashboard/general_catalogs/transporters", }, + { + title: "Conductores", + url: "/dashboard/general_catalogs/drivers", + }, { title: "Trailers", url: "/dashboard/general_catalogs/trailers", diff --git a/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte new file mode 100644 index 00000000..03a29c1c --- /dev/null +++ b/frontend/src/routes/dashboard/general_catalogs/drivers/+page.svelte @@ -0,0 +1,112 @@ + + +
+
+
+

Conductores

+

Gestión del catálogo de conductores

+
+ +
+ +
+ + +
+ + {#if loading && data.length === 0} +
+ Cargando conductores... +
+ {:else} + + {/if} + + +