diff --git a/backend/api/v1/modules/a76/parts/dto.py b/backend/api/v1/modules/a76/parts/dto.py index e8c891fa..dfcbca5b 100644 --- a/backend/api/v1/modules/a76/parts/dto.py +++ b/backend/api/v1/modules/a76/parts/dto.py @@ -3,27 +3,45 @@ from decimal import Decimal from typing import List, Optional from pydantic import BaseModel, Field +# --- DTO DE CREACIÓN --- class PartCreateDTO(BaseModel): client_id: int part_number: str = Field(..., max_length=50) + + # Campos Generales description_spanish: Optional[str] = None description_english: Optional[str] = None part_class: Optional[str] = None unit_of_measure: Optional[str] = "PZ" commercial_part_number: Optional[str] = None country_of_origin: Optional[str] = "MEX" + + # Costos y Pesos unit_cost: Optional[Decimal] = Decimal("0.0") currency_key: Optional[str] = "USD" unit_weight: Optional[Decimal] = Decimal("0.0") weight_type: Optional[str] = "KG" + + # --- LOS QUE FALTABAN Y AHORA SE GUARDARÁN --- + added_value: Optional[Decimal] = None + part_photo: Optional[str] = None + alternate_unit_measure: Optional[str] = None + license_code: Optional[str] = None + export_code: Optional[str] = None + exclusion_symbol: Optional[str] = None + + # Regulatorios fraction: Optional[str] = None us_fraction: Optional[str] = None supplier: Optional[str] = None fda_key: Optional[str] = None fcc_key: Optional[str] = None eccn: Optional[str] = None + + # Estatus is_active: Optional[bool] = True +# --- DTO DE ACTUALIZACIÓN --- class PartUpdateDTO(BaseModel): description_spanish: Optional[str] = None description_english: Optional[str] = None @@ -31,10 +49,18 @@ class PartUpdateDTO(BaseModel): unit_of_measure: Optional[str] = None commercial_part_number: Optional[str] = None country_of_origin: Optional[str] = None + unit_cost: Optional[Decimal] = None currency_key: Optional[str] = None unit_weight: Optional[Decimal] = None weight_type: Optional[str] = None + added_value: Optional[Decimal] = None + part_photo: Optional[str] = None + alternate_unit_measure: Optional[str] = None + license_code: Optional[str] = None + export_code: Optional[str] = None + exclusion_symbol: Optional[str] = None + fraction: Optional[str] = None us_fraction: Optional[str] = None supplier: Optional[str] = None @@ -43,6 +69,7 @@ class PartUpdateDTO(BaseModel): eccn: Optional[str] = None is_active: Optional[bool] = None + class PartResponseDTO(PartCreateDTO): id: int tenant_id: int diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 6129f616..e938f689 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -41,6 +41,7 @@ from .general_catalogs.electronic_notices.routes import router as electronic_not from .transportation.trailers.routes import router as trailers_router from .transportation.transporters.routes import router as transporters_router from .transportation.vehicles.routes import router as vehicles_router +from api.v1.modules.public.reference_data.material_types.routes import router as material_types_router # Router principal router = APIRouter() @@ -94,3 +95,10 @@ router.include_router(error_catalogs_router, prefix="/a76") router.include_router(doda_router, prefix="/a76") router.include_router(prevalidators_router, prefix="/a76") router.include_router(electronic_notices_router, prefix="/a76") + +# Registrar router de tipos de material públicos +router.include_router( + material_types_router, + prefix="/public/reference-data", + tags=["Reference Data"] +) diff --git a/frontend/src/lib/api/dashboard/a76/classes.ts b/frontend/src/lib/api/dashboard/a76/classes.ts index a08c2832..e9ba24d3 100644 --- a/frontend/src/lib/api/dashboard/a76/classes.ts +++ b/frontend/src/lib/api/dashboard/a76/classes.ts @@ -1,107 +1,52 @@ -/** - * API para gestión de Classes (Clases A76) - */ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface A76Class { - id: number; - tenant_id: number; - company_id: number; - client_id: number; - class_code: string; - description_es: string | null; - description_en: string | null; - material_key: string | null; - unit_of_measure: string; - fraction: string; - us_fraction: string; - sub_key: string; - physical_review: number; - iva_exempt_fraction: string; - created_at: string; - updated_at: string; -} - -export interface A76ClassCreate { - company_id: number; - client_id: number; - class_code: string; - description_es?: string | null; - description_en?: string | null; - material_key?: string | null; - unit_of_measure: string; - fraction: string; - us_fraction: string; - sub_key: string; - physical_review?: number; - iva_exempt_fraction: string; -} - -export interface A76ClassUpdate { - client_id?: number; - class_code?: string; - description_es?: string | null; - description_en?: string | null; - material_key?: string | null; - unit_of_measure?: string; - fraction?: string; - us_fraction?: string; - sub_key?: string; - physical_review?: number; - iva_exempt_fraction?: string; + id: number; + tenant_id: number; + company_id: number; + client_id: number; + class_code: string; + description_es: string | null; + description_en: string | null; + material_key: string | null; + unit_of_measure: string; + fraction: string; + us_fraction: string; + sub_key: string; + physical_review: number; + iva_exempt_fraction: string; + created_at: string; + updated_at: string; } export interface A76ClassListResponse { - items: A76Class[]; - total: number; - page: number; - page_size: number; + items: A76Class[]; + classes?: A76Class[]; + total: number; + page: number; + page_size: number; + size?: number; } export interface A76ClassListParams { - company_id: number; - page?: number; - page_size?: number; + company_id: number; + page?: number; + page_size?: number; + class_code?: string; + description?: string; } -/** - * API de Classes - */ export const classesApi = { - /** - * Obtener lista de classes con paginación - */ - list: (params: A76ClassListParams): Promise> => { - const { company_id, page = 1, page_size = 50 } = params; - return api.get(`/v1/a76/classes/?company_id=${company_id}&page=${page}&page_size=${page_size}`); - }, + list: (params: A76ClassListParams): Promise> => { + const query = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value) query.append(key, value.toString()); + }); + return api.get(`/v1/a76/classes/?${query.toString()}`); + }, - /** - * Obtener un class por ID - */ - get: (id: number, company_id: number): Promise> => { - return api.get(`/v1/a76/classes/${id}?company_id=${company_id}`); - }, - - /** - * Crear un nuevo class - */ - create: (data: A76ClassCreate, company_id: number): Promise> => { - return api.post(`/v1/a76/classes/?company_id=${company_id}`, data); - }, - - /** - * Actualizar un class existente - */ - update: (id: number, data: A76ClassUpdate, company_id: number): Promise> => { - return api.put(`/v1/a76/classes/${id}?company_id=${company_id}`, data); - }, - - /** - * Eliminar un class - */ - delete: (id: number, company_id: number): Promise> => { - return api.delete(`/v1/a76/classes/${id}?company_id=${company_id}`); - } -}; + get: (id: number, company_id: number): Promise> => { + return api.get(`/v1/a76/classes/${id}?company_id=${company_id}`); + } +}; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/material-types.ts b/frontend/src/lib/api/dashboard/a76/material-types.ts new file mode 100644 index 00000000..e0c0e39a --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/material-types.ts @@ -0,0 +1,23 @@ +import { api } from '$lib/api'; + + +export interface MaterialType { + key: string; + type: string; + description: string; +} + + +export interface MaterialTypeListResponse { + items: MaterialType[]; + total: number; + page: number; + page_size: number; +} + +export const materialTypesApi = { + list: async (page = 1, pageSize = 100) => { + + return api.get(`/v1/public/reference-data/material-types/?page=${page}&page_size=${pageSize}`); + } +}; \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/parts/class-selector-dialog.svelte b/frontend/src/lib/components/dashboard/parts/class-selector-dialog.svelte new file mode 100644 index 00000000..5d03286a --- /dev/null +++ b/frontend/src/lib/components/dashboard/parts/class-selector-dialog.svelte @@ -0,0 +1,166 @@ + + + + + + Seleccionar Clase (Anexo 24) + + Seleccione la clasificación del material. + + + +
+ + +
+ +
+ {#if loading} +
+ +

Cargando catálogo...

+
+ {:else if filteredItems.length === 0} +
+

No se encontraron clases.

+
+ {:else} + + + + + + + + + + + {#each filteredItems as item} + + + + + + + + + + {/each} + +
ClaveDescripciónUMAcción
+ + {item.class_code} + + +
+ + + {item.description_es || 'Sin descripción'} + +
+
+
+ + {item.unit_of_measure || '-'} +
+
+ +
+ {/if} +
+ + +
+ {filteredItems.length} registros encontrados +
+ +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/parts/client-selector-dialog.svelte b/frontend/src/lib/components/dashboard/parts/client-selector-dialog.svelte index e87aee97..65399818 100644 --- a/frontend/src/lib/components/dashboard/parts/client-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/parts/client-selector-dialog.svelte @@ -2,20 +2,26 @@ import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import * as Dialog from "$lib/components/ui/dialog"; - import { Search, Loader2, User, Building2, CheckCircle2, XCircle } from "lucide-svelte"; + import { Search, Loader2, User, Building2 } from "lucide-svelte"; import { clientsProvidersApi, type ClientProvider } from "$lib/api/dashboard/a76/clients-providers"; import { companyStore } from "$lib/stores/company.svelte"; - - // Props - let { open = $bindable(false), onSelect }: { open: boolean, onSelect: (client: ClientProvider) => void } = $props(); - // Estado + // --- PROPS Y BINDING --- + let { + open = $bindable(false), + onSelect + }: { + open: boolean, + onSelect: (client: ClientProvider) => void + } = $props(); + + // --- ESTADO LOCAL --- let clients = $state([]); let loading = $state(false); let searchTerm = $state(""); - let loaded = $state(false); + let loaded = $state(false); - // Filtramos localmente para que sea instantáneo + // Filtro reactivo local let filteredClients = $derived( clients.filter(c => c.name.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -24,7 +30,7 @@ ) ); - // Cargar clientes al abrir el modal + // Efecto para cargar datos cuando se abre el modal $effect(() => { if (open && !loaded && companyStore.activeCompany?.id) { loadClients(); @@ -36,19 +42,20 @@ loading = true; try { + // Petición a la API const res = await clientsProvidersApi.list(companyStore.activeCompany.id, 1, 100, { type: 'client' }); + // Normalización de respuesta const responseData = (res as any).data || res; if (responseData && responseData.items) { clients = responseData.items; loaded = true; } else { - console.warn("La API respondió pero no trajo items:", responseData); + console.warn("La API no trajo items:", responseData); } - } catch (e) { console.error("Error cargando clientes:", e); } finally { @@ -56,9 +63,17 @@ } } + // --- FUNCIÓN DE SELECCIÓN --- + function handleSelect(client: ClientProvider) { + console.log("Seleccionando cliente:", client.name); + if (onSelect) { + onSelect(client); + } + open = false; // Cerrar el modal + } - + Seleccionar Cliente @@ -125,7 +140,13 @@ {/if} - diff --git a/frontend/src/lib/components/dashboard/parts/columns.ts b/frontend/src/lib/components/dashboard/parts/columns.ts index e33f9a40..6230f6f1 100644 --- a/frontend/src/lib/components/dashboard/parts/columns.ts +++ b/frontend/src/lib/components/dashboard/parts/columns.ts @@ -18,7 +18,7 @@ function formatCurrency(amount: number | null, currency: string | null): string export function createColumns(onSuccess?: () => void): ColumnDef[] { return [ - // 1. STATUS (Corregido a Texto) + { accessorKey: "is_active", header: "Status", @@ -35,7 +35,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { } }, - // 2. NUMERO PARTE + { accessorKey: "part_number", header: "No. Parte", @@ -51,7 +51,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { } }, - // 3. DESCRIPCION (Español) + { accessorKey: "description_spanish", header: "Descripción", @@ -67,7 +67,6 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { } }, - // 4. DESCRIPCION INGLES { accessorKey: "description_english", header: "Desc. Inglés", @@ -83,7 +82,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { } }, - // 5. CLASE + { accessorKey: "part_class", header: "Clase", @@ -98,7 +97,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { } }, - // 6. TIPO (Commercial Part Number) + { accessorKey: "commercial_part_number", header: "Tipo", diff --git a/frontend/src/lib/components/dashboard/parts/material-type-selector-dialog.svelte b/frontend/src/lib/components/dashboard/parts/material-type-selector-dialog.svelte new file mode 100644 index 00000000..5fa0974f --- /dev/null +++ b/frontend/src/lib/components/dashboard/parts/material-type-selector-dialog.svelte @@ -0,0 +1,133 @@ + + + + + + Seleccionar Tipo de Material + Catálogo general. + + +
+ + +
+ +
+ {#if loading} +
+ +

Cargando catálogo...

+
+ {:else if filteredItems.length === 0} +
+

No se encontraron resultados.

+
+ {:else} + + + + + + + + + + + {#each filteredItems as item} + + + + + + + + + + {/each} + +
ClaveDescripciónTipoAcción
{item.key}{item.description} +
+ + {item.type} +
+
+ +
+ {/if} +
+ + + +
+
\ No newline at end of file diff --git a/frontend/src/routes/dashboard/goods/parts/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/goods/parts/edit/[[id]]/+page.svelte index 9254a048..13637b32 100644 --- a/frontend/src/routes/dashboard/goods/parts/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/goods/parts/edit/[[id]]/+page.svelte @@ -12,36 +12,46 @@ import * as Card from '$lib/components/ui/card'; import * as Select from "$lib/components/ui/select"; import { Switch } from "$lib/components/ui/switch"; + // Iconos import { ArrowLeft, LoaderCircle, Save, Package, DollarSign, FileText, Settings, Image as ImageIcon, Search, - UserCheck, CheckCircle2, XCircle + UserCheck, CheckCircle2, XCircle, Tag, Layers } from 'lucide-svelte'; // Stores & APIs import { companyStore } from '$lib/stores/company.svelte'; import { partsApi, type PartCreate } from '$lib/api/dashboard/a76/parts'; import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers'; + import { classesApi } from '$lib/api/dashboard/a76/classes'; + import { materialTypesApi } from '$lib/api/dashboard/a76/material-types'; + - // COMPONENTE DEL MODAL (Ajusta la ruta si es necesario) import ClientSelectorDialog from '$lib/components/dashboard/parts/client-selector-dialog.svelte'; + import ClassSelectorDialog from '$lib/components/dashboard/parts/class-selector-dialog.svelte'; + import MaterialTypeSelectorDialog from '$lib/components/dashboard/parts/material-type-selector-dialog.svelte'; + - // --- 1. IDENTIFICACIÓN --- let id = $derived($page.params.id === 'new' ? null : Number($page.params.id)); let isEdit = $derived(!!id); let title = $derived(isEdit ? "Editar Parte" : "Nueva Parte"); - // --- 2. ESTADOS --- let loading = $state(false); let error = $state(null); - // Estado del Modal de Clientes + // Estado Modales let showClientModal = $state(false); + let showClassModal = $state(false); + let showMaterialModal = $state(false); + + // Descripciones Visuales let selectedClientName = $state(""); let selectedClientStatus = $state(true); + let selectedClassDesc = $state(""); + let selectedMaterialDesc = $state(""); - // Estado del Formulario + // Estado Formulario let formData = $state({ client_id: 0, part_number: '', @@ -49,7 +59,8 @@ // General description_spanish: '', description_english: '', - part_class: '', + part_class: '', + material_type_key: '', country_of_origin: 'MEX', unit_of_measure: 'PZ', @@ -59,7 +70,7 @@ unit_cost: 0, currency_key: 'USD', added_value: 0, - value_added_type: 'USD', // Campo visual (no en BD) + value_added_type: 'USD', us_fraction: '', // Opciones @@ -72,7 +83,7 @@ // Otros commercial_part_number: '', - fraction: '', // Fraccion MX + fraction: '', eccn: '', license_code: '', export_code: '', @@ -81,7 +92,7 @@ is_active: true }); - // --- 3. CARGA --- + // --- 3. CARGA INICIAL --- onMount(async () => { const companyId = companyStore.activeCompany?.id; if (!companyId) return; @@ -103,20 +114,23 @@ if (response.data) { const d = response.data; + // Mapeo de datos formData = { client_id: d.client_id, part_number: d.part_number, description_spanish: d.description_spanish || '', description_english: d.description_english || '', + part_class: d.part_class || '', + // OJO: Asegúrate que tu backend devuelva este campo si existe en BD + material_type_key: (d as any).material_type_key || '', + country_of_origin: d.country_of_origin || 'MEX', unit_of_measure: d.unit_of_measure || 'PZ', - fraction: d.fraction || '', us_fraction: d.us_fraction || '', unit_weight: Number(d.unit_weight) || 0, weight_type: d.weight_type || 'KG', - supplier: d.supplier || '', fda_key: d.fda_key || '', fcc_key: d.fcc_key || '', @@ -124,22 +138,20 @@ license_code: d.license_code || '', export_code: d.export_code || '', exclusion_symbol: d.exclusion_symbol || '', - unit_cost: Number(d.unit_cost) || 0, currency_key: d.currency_key || 'USD', added_value: Number(d.added_value) || 0, - value_added_type: 'USD', // Valor por defecto al cargar + value_added_type: 'USD', commercial_part_number: d.commercial_part_number || '', - alternate_unit_measure: d.alternate_unit_measure || '', part_photo: d.part_photo || '', is_active: d.is_active ?? true }; - // Cargar info visual del cliente - if (d.client_id) { - await fetchClientName(d.client_id, companyId); - } + // Cargar datos visuales + if (d.client_id) await fetchClientName(d.client_id, companyId); + if (d.part_class) await fetchClassDesc(d.part_class, companyId); + if ((d as any).material_type_key) await fetchMaterialName((d as any).material_type_key); } } catch (e) { error = "Error al cargar la parte"; @@ -149,28 +161,57 @@ } } - // Función auxiliar para obtener nombre del cliente async function fetchClientName(clientId: number, companyId: number) { try { const res = await clientsProvidersApi.get(clientId, companyId); - // Ajusta esto según cómo devuelva tu API el objeto (res o res.data) const clientData = (res as any).data || res; if (clientData) { selectedClientName = clientData.name; selectedClientStatus = clientData.is_active ?? true; } - } catch (e) { - console.log("No se pudo cargar info visual del cliente", e); - } + } catch (e) { console.log("Error visual cliente", e); } } - // Callback del Modal + async function fetchClassDesc(code: string, companyId: number) { + try { + const res = await classesApi.list({ company_id: companyId, class_code: code }); + const data = (res as any).data || res; + const list = data.items || data.classes || []; + if (list.length > 0) { + const found = list.find((i: any) => i.class_code === code) || list[0]; + selectedClassDesc = found.description_es || found.description_en || ""; + } + } catch (e) { console.log("Error visual clase", e); } + } + + async function fetchMaterialName(key: string) { + try { + const res = await materialTypesApi.list(1, 100); + const data = (res as any).data || res; + const list = data.items || []; + const found = list.find((m: any) => m.key === key); + if (found) selectedMaterialDesc = found.description; + } catch (e) { console.log("Error visual material", e); } + } + + + function handleClientSelect(client: any) { formData.client_id = client.id; selectedClientName = client.name; selectedClientStatus = client.is_active ?? true; } + function handleClassSelect(item: any) { + formData.part_class = item.class_code; + selectedClassDesc = item.description_es || item.description_en || ""; + } + + function handleMaterialSelect(item: any) { + formData.material_type_key = item.key; + selectedMaterialDesc = item.description; + } + async function handleSubmit() { error = null; const activeCompanyId = companyStore.activeCompany?.id; @@ -180,18 +221,21 @@ loading = true; try { + const commonData = { + description_spanish: formData.description_spanish || null, description_english: formData.description_english || null, - part_class: formData.part_class || null, + + part_class: formData.part_class || null, + material_type_key: formData.material_type_key || null, + country_of_origin: formData.country_of_origin || 'MEX', unit_of_measure: formData.unit_of_measure, - fraction: formData.fraction || null, us_fraction: formData.us_fraction || null, unit_weight: Number(formData.unit_weight) || 0, weight_type: formData.weight_type || 'KG', - supplier: formData.supplier || null, fda_key: formData.fda_key || null, fcc_key: formData.fcc_key || null, @@ -199,21 +243,21 @@ license_code: formData.license_code || null, export_code: formData.export_code || null, exclusion_symbol: formData.exclusion_symbol || null, - - unit_cost: Number(formData.unit_cost) || 0, - currency_key: formData.currency_key || 'USD', - added_value: Number(formData.added_value) || 0, - commercial_part_number: formData.commercial_part_number || null, - alternate_unit_measure: formData.alternate_unit_measure || null, part_photo: formData.part_photo || null, + added_value: Number(formData.added_value) || 0, + unit_cost: Number(formData.unit_cost) || 0, + currency_key: formData.currency_key || 'USD', + commercial_part_number: formData.commercial_part_number || null, is_active: formData.is_active }; if (isEdit && id) { + const response = await partsApi.update(id, commonData, activeCompanyId); if (response.error) throw new Error(response.error); } else { + const createData: PartCreate = { ...commonData, company_id: activeCompanyId, @@ -228,13 +272,7 @@ } catch (e: any) { console.error("Error en el guardado:", e); - if (e.message?.includes('already exists')) { - error = `El número de parte ${formData.part_number} ya existe para este cliente.`; - } else if (e.message?.includes('foreign key')) { - error = `Error: Uno de los catálogos (País, Moneda, UM) no es válido.`; - } else { - error = e.message || 'Error inesperado al guardar'; - } + error = e.message || 'Error inesperado al guardar'; } finally { loading = false; } @@ -289,51 +327,76 @@
+
- - + +
+
+
+ +
+ showClassModal = true} + /> +
+ +
+ {#if selectedClassDesc} +
+ + {selectedClassDesc} +
+ {/if}
+
+ +
+
+
+ +
+ showMaterialModal = true} + /> +
+ +
+ {#if selectedMaterialDesc} +
+ + {selectedMaterialDesc} +
+ {/if} +

Opcional: Clasificación adicional por tipo de material.

+
+ +
-
-

- Tipos de material y unidades de medida -

-
- -
- - -

Campo informativo (Visual)

-
- -
- - - - {formData.unit_of_measure || "Seleccione"} - - - Pieza (PZ) - Kilogramo (KG) - Elemento (EA) - Litro (L) - Metro (M) - - -
-
-
-

Costos, valores y peso unitario

-
@@ -346,7 +409,6 @@
-
@@ -354,7 +416,6 @@
-
@@ -373,7 +434,6 @@
-
@@ -388,7 +448,6 @@
-
{#if formData.value_added_type === 'PERCENT'} @@ -396,15 +455,7 @@ {:else} $ {/if} - - +
@@ -417,47 +468,38 @@ -

Configuración General

-
-
-
-
-
-

FDA (Food and Drug Administration)

+

FDA

-
-
-
@@ -471,41 +513,31 @@ onclick={() => showClientModal = true} />
- -
- {#if selectedClientName} -
+
{selectedClientName} - {#if selectedClientStatus} Activo {:else} - Baja / Inactivo + Inactivo {/if}
{/if} -

El cliente propietario de este número de parte.

-
-
-

Factor Conversión

-
- - -
+ +
-
-

Datos Regulatorios Adicionales

+

Datos Regulatorios

@@ -529,49 +561,28 @@
-
-
- -

Habilitar o deshabilitar

-
+
- -
- - - - - - - - + + + + -
- +
@@ -584,6 +595,16 @@ onSelect={handleClientSelect} /> + + + +