feature/catalogo-driver

This commit is contained in:
hreyes
2026-03-06 09:52:20 -07:00
parent 8ecc926ed1
commit 3175383442
11 changed files with 874 additions and 53 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<ApiResponse<DriverResponse>> {
const params = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString()
});
async list(
companyId: string | number,
params?: Record<string, string | number | undefined>
): Promise<ApiResponse<DriverResponse>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
...(params as Record<string, string>)
});
return api.get<DriverResponse>(`${this.baseUrl}?${queryParams.toString()}`);
}
return api.get<DriverResponse>(`${this.baseUrl}?${params.toString()}`);
}
async get(
transporterKey: string,
line: number,
companyId: string | number
): Promise<ApiResponse<Driver>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Driver>(
`${this.baseUrl}/${transporterKey}/${line}?${params.toString()}`
);
}
async get(
transporterKey: string,
line: number,
companyId: string | number
): Promise<ApiResponse<Driver>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<Driver>(`${this.baseUrl}/${transporterKey}/${line}?${params.toString()}`);
}
async create(
data: Omit<Driver, 'company_id' | 'tenant_id'> & { company_id: number; tenant_id: number },
companyId: string | number
): Promise<ApiResponse<Driver>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.post<Driver>(`${this.baseUrl}?${params.toString()}`, data);
}
async update(
transporterKey: string,
line: number,
data: Partial<Driver>,
companyId: string | number
): Promise<ApiResponse<Driver>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.put<Driver>(
`${this.baseUrl}/${transporterKey}/${line}?${params.toString()}`,
data
);
}
async delete(
transporterKey: string,
line: number,
companyId: string | number
): Promise<ApiResponse<void>> {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.delete<void>(
`${this.baseUrl}/${transporterKey}/${line}?${params.toString()}`
);
}
}
export const driversApi = new DriversApi();

View File

@@ -34,16 +34,22 @@ export interface TransporterResponse {
class TransportersApi {
private baseUrl = '/v1/a76/transporters';
async list(
companyId: string | number,
params?: Record<string, any>
): Promise<ApiResponse<TransporterResponse>> {
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
...params
});
return api.get<TransporterResponse>(`${this.baseUrl}?${queryParams.toString()}`);
}
async list(
companyId: string | number,
params?: Record<string, string | number | undefined>
): Promise<ApiResponse<TransporterResponse>> {
const raw: Record<string, string | number> = {
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<TransporterResponse>(`${this.baseUrl}?${queryParams.toString()}`);
}
async get(id: string, companyId: string | number): Promise<ApiResponse<Transporter>> {
const queryParams = new URLSearchParams({

View File

@@ -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<Driver>[] {
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
});
}
}
];
}

View File

@@ -0,0 +1,313 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { driversApi, type Driver } from '$lib/api/dashboard/a76/drivers';
import { transportersApi, type Transporter } from '$lib/api/dashboard/a76/transporters';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
item = null,
onSuccess
}: {
open: boolean;
item?: Driver | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? 'Editar Conductor' : 'Nuevo Conductor');
let transporters = $state<Transporter[]>([]);
let transportersLoading = $state(false);
let formData = $state<Driver & { lineStr?: string }>({
transporter_key: '',
line: 0,
driver_name: '',
license_number: '',
first_name: '',
last_name: '',
badge_number: '',
express_line_id: '',
ace_id: '',
birth_country: '',
hazardous_material_auth: '',
hazardous_material_state: '',
class_type: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
// Cargar transportistas al abrir el diálogo en modo creación (backend max page_size=100)
$effect(() => {
if (open && !item && companyStore.activeCompany) {
transportersLoading = true;
transportersApi
.list(companyStore.activeCompany.id, { page: 1, page_size: 100 })
.then((res) => {
if (res.data?.items) transporters = res.data.items;
else transporters = [];
})
.catch(() => (transporters = []))
.finally(() => (transportersLoading = false));
}
});
$effect(() => {
if (item) {
formData = {
...item,
lineStr: String(item.line)
};
} else {
formData = {
transporter_key: '',
line: 0,
lineStr: '',
driver_name: '',
license_number: '',
first_name: '',
last_name: '',
badge_number: '',
express_line_id: '',
ace_id: '',
birth_country: '',
hazardous_material_auth: '',
hazardous_material_state: '',
class_type: ''
};
}
});
async function handleSubmit() {
error = null;
loading = true;
try {
const company = companyStore.activeCompany;
if (!company) {
throw new Error('No hay una compañía seleccionada');
}
if (!formData.transporter_key?.trim()) {
throw new Error('Selecciona un transportista de la lista');
}
const lineNum = isEdit ? item!.line : parseInt(String(formData.lineStr ?? formData.line), 10);
if (!isEdit && (Number.isNaN(lineNum) || lineNum < 1)) {
throw new Error('La línea debe ser un número mayor a 0');
}
if (isEdit && item) {
const response = await driversApi.update(
item.transporter_key,
item.line,
{
driver_name: formData.driver_name || undefined,
license_number: formData.license_number || undefined,
first_name: formData.first_name || undefined,
last_name: formData.last_name || undefined,
badge_number: formData.badge_number || undefined,
express_line_id: formData.express_line_id || undefined,
ace_id: formData.ace_id || undefined,
birth_country: formData.birth_country || undefined,
hazardous_material_auth: formData.hazardous_material_auth || undefined,
hazardous_material_state: formData.hazardous_material_state || undefined,
class_type: formData.class_type || undefined
},
company.id
);
if (response.error) throw new Error(response.error);
} else {
const payload = {
transporter_key: String(formData.transporter_key).trim(),
line: lineNum,
driver_name: formData.driver_name || undefined,
license_number: formData.license_number || undefined,
first_name: formData.first_name || undefined,
last_name: formData.last_name || undefined,
badge_number: formData.badge_number || undefined,
express_line_id: formData.express_line_id || undefined,
ace_id: formData.ace_id || undefined,
birth_country: formData.birth_country || undefined,
hazardous_material_auth: formData.hazardous_material_auth || undefined,
hazardous_material_state: formData.hazardous_material_state || undefined,
class_type: formData.class_type || undefined,
company_id: company.id,
tenant_id: company.tenant_id
};
const response = await driversApi.create(payload, company.id);
if (response.error) throw new Error(response.error);
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
if (e && typeof e === 'object' && 'message' in e) {
error = (e as { message: string }).message;
} else {
error = 'Error al guardar el conductor';
}
} finally {
loading = false;
}
}
function handleCancel() {
open = false;
error = null;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>
{isEdit
? 'Modifica los datos del conductor'
: 'Completa los datos para crear un nuevo conductor'}
</Dialog.Description>
</Dialog.Header>
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
class="space-y-6"
>
{#if error}
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
{error}
</div>
{/if}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="transporter_key"
>Transportista <span class="text-destructive">*</span></Label
>
{#if isEdit}
<Input
id="transporter_key"
value={formData.transporter_key}
disabled
class="bg-muted"
/>
{:else}
<Select.Root
type="single"
bind:value={formData.transporter_key}
disabled={transportersLoading}
>
<Select.Trigger class="w-full">
{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'}
</Select.Trigger>
<Select.Content>
{#each transporters as t}
<Select.Item value={t.transporter_key} label={t.transporter_key}>
{t.transporter_key}{t.name || t.short_name || 'Sin nombre'}
</Select.Item>
{/each}
{#if !transportersLoading && transporters.length === 0}
<div class="px-2 py-3 text-sm text-muted-foreground">
No hay transportistas. Crea uno en el catálogo Transportistas.
</div>
{/if}
</Select.Content>
</Select.Root>
{/if}
</div>
<div class="grid gap-2">
<Label for="line">Línea <span class="text-destructive">*</span></Label>
<Input
id="line"
type="number"
min="1"
bind:value={formData.lineStr}
disabled={isEdit}
required
placeholder="Ej: 1"
/>
</div>
<div class="grid gap-2 md:col-span-2">
<Label for="driver_name">Nombre del Conductor</Label>
<Input id="driver_name" bind:value={formData.driver_name} maxlength={80} />
</div>
<div class="grid gap-2">
<Label for="first_name">Nombre</Label>
<Input id="first_name" bind:value={formData.first_name} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="last_name">Apellido</Label>
<Input id="last_name" bind:value={formData.last_name} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="license_number">Número de Licencia</Label>
<Input id="license_number" bind:value={formData.license_number} maxlength={29} />
</div>
<div class="grid gap-2">
<Label for="badge_number">Número de Placa/Insignia</Label>
<Input id="badge_number" bind:value={formData.badge_number} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="express_line_id">Express Line ID</Label>
<Input id="express_line_id" bind:value={formData.express_line_id} maxlength={17} />
</div>
<div class="grid gap-2">
<Label for="ace_id">ACE ID</Label>
<Input id="ace_id" bind:value={formData.ace_id} maxlength={20} />
</div>
<div class="grid gap-2">
<Label for="birth_country">País de Nacimiento</Label>
<Input id="birth_country" bind:value={formData.birth_country} maxlength={3} placeholder="MEX / USA" />
</div>
<div class="grid gap-2">
<Label for="hazardous_material_auth">Auth. Material Peligroso</Label>
<Input id="hazardous_material_auth" bind:value={formData.hazardous_material_auth} maxlength={2} />
</div>
<div class="grid gap-2">
<Label for="hazardous_material_state">Estado Material Peligroso</Label>
<Input id="hazardous_material_state" bind:value={formData.hazardous_material_state} maxlength={30} />
</div>
<div class="grid gap-2">
<Label for="class_type">Tipo de Clase</Label>
<Input id="class_type" bind:value={formData.class_type} maxlength={1} />
</div>
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={handleCancel} disabled={loading}>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { driversApi, type Driver } from '$lib/api/dashboard/a76/drivers';
import { companyStore } from '$lib/stores/company.svelte';
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
let {
item,
onSuccess
}: {
item: Driver;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
let selectedItem = $state<Driver | null>(null);
async function handleDelete() {
if (
!confirm(
`¿Estás seguro de eliminar el conductor "${item.driver_name || item.transporter_key + '-' + item.line}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`
)
) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await driversApi.delete(
item.transporter_key,
item.line,
companyStore.activeCompany.id
);
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = response.error;
alert(`❌ Error al eliminar:\n\n${response.error}`);
}
return;
}
alert(
`✅ Conductor "${item.driver_name || item.transporter_key + '-' + item.line}" eliminado correctamente`
);
if (onSuccess) {
onSuccess();
}
} catch (e) {
error = e instanceof Error ? e.message : 'Error al eliminar';
alert(`❌ Error: ${error}`);
console.error('Error deleting:', e);
} finally {
loading = false;
}
}
function handleEdit() {
selectedItem = item;
dialogOpen = true;
}
function handleDialogSuccess() {
dialogOpen = false;
selectedItem = null;
if (onSuccess) {
onSuccess();
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical size={16} />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog bind:open={dialogOpen} item={selectedItem} onSuccess={handleDialogSuccess} />

View File

@@ -0,0 +1,99 @@
<script lang="ts" generics="TData, TValue">
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import { Button } from '$lib/components/ui/button';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let { data, columns, pageCount, totalItems }: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
get columns() {
return columns;
},
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
get pageCount() {
return pageCount;
}
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
</script>
<div class="rounded-md border bg-card">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && 'selected'}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -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",

View File

@@ -0,0 +1,112 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import DataTable from '$lib/components/dashboard/transportation/drivers/data-table.svelte';
import CreateEditDialog from '$lib/components/dashboard/transportation/drivers/create-edit-dialog.svelte';
import { createColumns } from '$lib/components/dashboard/transportation/drivers/columns';
import { driversApi, type Driver } from '$lib/api/dashboard/a76/drivers';
import { companyStore } from '$lib/stores/company.svelte';
import { browser } from '$app/environment';
let data = $state<Driver[]>([]);
let totalItems = $state(0);
let pageCount = $state(0);
let loading = $state(false);
let createDialogOpen = $state(false);
let currentPage = $derived(Number($page.url.searchParams.get('page')) || 1);
let pageSize = 10;
let searchTransporterKey = $state($page.url.searchParams.get('transporter_key') || '');
let searchDriverName = $state($page.url.searchParams.get('driver_name') || '');
let searchTimeout: NodeJS.Timeout;
async function loadData() {
if (!companyStore.activeCompany) return;
loading = true;
try {
const params: Record<string, string | number> = {
page: currentPage,
page_size: pageSize
};
// Filtros: el backend aún no los soporta; se mantienen en URL para futura implementación
// if (searchTransporterKey) params.transporter_key = searchTransporterKey;
// if (searchDriverName) params.driver_name = searchDriverName;
const response = await driversApi.list(companyStore.activeCompany.id, params);
if (response.data) {
data = response.data.items;
totalItems = response.data.total;
pageCount = Math.ceil(response.data.total / response.data.page_size) || 1;
}
} catch (error) {
console.error('Error loading drivers:', error);
} finally {
loading = false;
}
}
function handleSearch() {
if (!browser) return;
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const url = new URL($page.url);
url.searchParams.set('page', '1');
if (searchTransporterKey) url.searchParams.set('transporter_key', searchTransporterKey);
else url.searchParams.delete('transporter_key');
if (searchDriverName) url.searchParams.set('driver_name', searchDriverName);
else url.searchParams.delete('driver_name');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
$effect(() => {
const _ = { p: $page.url.href, c: companyStore.activeCompany?.id };
loadData();
});
const columns = createColumns(loadData);
</script>
<div class="flex h-full flex-col space-y-6 p-8">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Conductores</h2>
<p class="text-muted-foreground">Gestión del catálogo de conductores</p>
</div>
<Button onclick={() => (createDialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" /> Nuevo Conductor
</Button>
</div>
<div class="flex items-center space-x-2">
<Input
placeholder="Buscar por clave transportista..."
class="h-8 w-[250px] bg-card"
bind:value={searchTransporterKey}
oninput={handleSearch}
/>
<Input
placeholder="Buscar por nombre..."
class="h-8 w-[250px] bg-card"
bind:value={searchDriverName}
oninput={handleSearch}
/>
</div>
{#if loading && data.length === 0}
<div class="flex h-64 items-center justify-center text-muted-foreground">
Cargando conductores...
</div>
{:else}
<DataTable {data} {columns} {pageCount} {totalItems} />
{/if}
<CreateEditDialog bind:open={createDialogOpen} onSuccess={loadData} />
</div>