From 78adde9737a291b8996bc1b7b1244d56ff55ff9c Mon Sep 17 00:00:00 2001 From: hreyes Date: Tue, 3 Feb 2026 12:03:41 -0600 Subject: [PATCH] Feature/Integracion de nasvegacion free mouse para goods --- .../keyboard/KeyboardManager.svelte | 95 ++++ .../keyboard/ShortcutsHelpModal.svelte | 111 +++++ frontend/src/lib/config/shortcuts.ts | 29 ++ frontend/src/lib/hooks/use-shortcuts.ts | 17 + frontend/src/lib/stores/shortcut-store.ts | 49 ++ frontend/src/routes/+layout.svelte | 6 + .../goods/fixed-asset-classes/+page.svelte | 25 + .../routes/dashboard/goods/parts/+page.svelte | 461 ++++++++++-------- start.sh | 2 +- 9 files changed, 588 insertions(+), 207 deletions(-) create mode 100644 frontend/src/lib/components/keyboard/KeyboardManager.svelte create mode 100644 frontend/src/lib/components/keyboard/ShortcutsHelpModal.svelte create mode 100644 frontend/src/lib/config/shortcuts.ts create mode 100644 frontend/src/lib/hooks/use-shortcuts.ts create mode 100644 frontend/src/lib/stores/shortcut-store.ts diff --git a/frontend/src/lib/components/keyboard/KeyboardManager.svelte b/frontend/src/lib/components/keyboard/KeyboardManager.svelte new file mode 100644 index 00000000..2d1c6eb5 --- /dev/null +++ b/frontend/src/lib/components/keyboard/KeyboardManager.svelte @@ -0,0 +1,95 @@ + + + + + (showHelp = false)} /> diff --git a/frontend/src/lib/components/keyboard/ShortcutsHelpModal.svelte b/frontend/src/lib/components/keyboard/ShortcutsHelpModal.svelte new file mode 100644 index 00000000..3cb117be --- /dev/null +++ b/frontend/src/lib/components/keyboard/ShortcutsHelpModal.svelte @@ -0,0 +1,111 @@ + + +{#if open} + +{/if} diff --git a/frontend/src/lib/config/shortcuts.ts b/frontend/src/lib/config/shortcuts.ts new file mode 100644 index 00000000..6829f7fa --- /dev/null +++ b/frontend/src/lib/config/shortcuts.ts @@ -0,0 +1,29 @@ +/** + * Central Configuration for Keyboard Shortcuts + * + * Standards: + * - Alt + Key: Global Navigation (Route switching) + * - Ctrl + Key: Local Actions (Context specific) + */ + +export const GLOBAL_NAV = { + // Goods / Merchandise + 'g': '/dashboard/goods/parts', + 'c': '/dashboard/goods/fixed-asset-classes', + + // Common Actions (Navigation intents) + 'b': 'SEARCH_FOCUS', // Special case for generic focus + 'h': '/', // Home +} as const; + +export const STANDARD_ACTIONS = { + 's': 'SAVE', + 'n': 'NEW', + 'e': 'EXPORT', + 'd': 'DELETE', + 'f': 'FILTER', + 'Escape': 'CANCEL' // Special case +} as const; + +export type GlobalNavKey = keyof typeof GLOBAL_NAV; +export type ActionKey = keyof typeof STANDARD_ACTIONS; diff --git a/frontend/src/lib/hooks/use-shortcuts.ts b/frontend/src/lib/hooks/use-shortcuts.ts new file mode 100644 index 00000000..bd0cc204 --- /dev/null +++ b/frontend/src/lib/hooks/use-shortcuts.ts @@ -0,0 +1,17 @@ +import { onMount, onDestroy } from 'svelte'; +import { shortcutStore, type ShortcutDef } from '$lib/stores/shortcut-store'; + +/** + * Hook to register shortcuts for a component lifecycle. + * @param context Name of the context (e.g., 'Goods List') + * @param shortcuts Array of shortcut definitions + */ +export function useShortcuts(context: string, shortcuts: ShortcutDef[]) { + onMount(() => { + shortcutStore.register(context, shortcuts); + }); + + onDestroy(() => { + shortcutStore.clear(context); + }); +} diff --git a/frontend/src/lib/stores/shortcut-store.ts b/frontend/src/lib/stores/shortcut-store.ts new file mode 100644 index 00000000..6569814e --- /dev/null +++ b/frontend/src/lib/stores/shortcut-store.ts @@ -0,0 +1,49 @@ +import { writable, derived } from 'svelte/store'; + +export interface ShortcutDef { + key: string; // e.g., 'Ctrl+S' + description: string; + action: () => void; + group?: string; +} + +interface ShortcutState { + context: string; + shortcuts: ShortcutDef[]; +} + +function createShortcutStore() { + const { subscribe, set, update } = writable({ + context: 'Global', + shortcuts: [] + }); + + return { + subscribe, + /** + * Register local shortcuts for the current view. + * Call this on mount (or $effect). + */ + register: (context: string, shortcuts: ShortcutDef[]) => { + set({ context, shortcuts }); + }, + /** + * Clear shortcuts (on unmount) + * Only clear if the context matches (prevent clearing new page's shortcuts during transition) + */ + clear: (contextToClear?: string) => { + update(state => { + // If specific context is provided, only clear if it matches current + if (contextToClear && state.context !== contextToClear) { + return state; + } + return { context: 'Global', shortcuts: [] }; + }); + } + }; +} + +export const shortcutStore = createShortcutStore(); + +// Derived store to help UI display active shortcuts +export const activeShortcuts = derived(shortcutStore, ($state) => $state); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index f69244ba..69b575c0 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -4,6 +4,8 @@ import { Toaster } from 'svelte-sonner'; import { page } from '$app/stores'; import { handleApiError } from '$lib/utils/error-handler'; + import KeyboardManager from '$lib/components/keyboard/KeyboardManager.svelte'; + let { children } = $props(); @@ -21,4 +23,8 @@ + + + console.log('Global Search Focused')} /> + {@render children?.()} diff --git a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte index f6d5f3a2..04a35476 100644 --- a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte +++ b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte @@ -419,6 +419,31 @@ toast.error('Error al eliminar la clase'); } } + + // Keyboard Shortcuts + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + + useShortcuts('Goods / Classes', [ + { + key: 'Alt+Shift+N', + description: 'New Class', + action: () => { + handleNew(); + validationError = ''; + showInsertDialog = true; + } + }, + { + key: 'Alt+Shift+R', + description: 'Refresh', + action: handleRefresh + }, + { + key: 'Alt+Shift+D', + description: 'Delete', + action: handleDelete + } + ]);
diff --git a/frontend/src/routes/dashboard/goods/parts/+page.svelte b/frontend/src/routes/dashboard/goods/parts/+page.svelte index ea3f4bb0..ce9533e6 100644 --- a/frontend/src/routes/dashboard/goods/parts/+page.svelte +++ b/frontend/src/routes/dashboard/goods/parts/+page.svelte @@ -5,12 +5,14 @@ import { Plus, RefreshCw, Package } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { partsApi, type Part } from '$lib/api/dashboard/a76/parts'; - import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers'; + import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers'; import { companyStore } from '$lib/stores/company.svelte'; + import { useShortcuts } from '$lib/hooks/use-shortcuts'; + import { goto } from '$app/navigation'; // Estado de la lista de partes let parts = $state([]); - let clientsMap = $state>({}); // Mapa ID -> Nombre + let clientsMap = $state>({}); // Mapa ID -> Nombre let selectedPart = $state(null); let isLoading = $state(false); let searchPartNumber = $state(''); @@ -22,24 +24,26 @@ const filteredParts = $derived( parts.filter((p) => { // Filtro por número de parte - const matchesPartNumber = !searchPartNumber || - p.part_number.toLowerCase().includes(searchPartNumber.toLowerCase()); - + const matchesPartNumber = + !searchPartNumber || p.part_number.toLowerCase().includes(searchPartNumber.toLowerCase()); + // Filtro por descripción (español o inglés) - const matchesDescription = !searchDescription || + const matchesDescription = + !searchDescription || (p.description_spanish?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false) || (p.description_english?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false); - + // Filtro por cliente (Busca en nombre o ID) - const clientName = clientsMap[p.client_id] || ''; - const matchesClient = !searchClient || + const clientName = clientsMap[p.client_id] || ''; + const matchesClient = + !searchClient || (p.client_id?.toString().includes(searchClient) ?? false) || - clientName.toLowerCase().includes(searchClient.toLowerCase()); - + clientName.toLowerCase().includes(searchClient.toLowerCase()); + // Filtro por clase - const matchesClass = !searchClass || - (p.part_class?.toLowerCase().includes(searchClass.toLowerCase()) ?? false); - + const matchesClass = + !searchClass || (p.part_class?.toLowerCase().includes(searchClass.toLowerCase()) ?? false); + return matchesPartNumber && matchesDescription && matchesClient && matchesClass; }) ); @@ -53,43 +57,38 @@ }); async function loadData() { - await Promise.all([loadParts(), loadClients()]); - } + await Promise.all([loadParts(), loadClients()]); + } - async function loadClients() { - const companyId = companyStore.activeCompany?.id; - if (!companyId) return; + async function loadClients() { + const companyId = companyStore.activeCompany?.id; + if (!companyId) return; - try { - // Fetch all clients/providers to ensure we map "both" types as well - const response = await clientsProvidersApi.list( - companyId, - 1, - 1000 - ); - - const data = (response as any).data || response; - const items = data.items || []; - - const map: Record = {}; - items.forEach((c: any) => { - map[c.id] = c.name; - }); - clientsMap = map; + try { + // Fetch all clients/providers to ensure we map "both" types as well + const response = await clientsProvidersApi.list(companyId, 1, 1000); - } catch (e) { - console.error("Error cargando clientes:", e); - } - } + const data = (response as any).data || response; + const items = data.items || []; + + const map: Record = {}; + items.forEach((c: any) => { + map[c.id] = c.name; + }); + clientsMap = map; + } catch (e) { + console.error('Error cargando clientes:', e); + } + } async function loadParts() { const companyId = companyStore.activeCompany?.id; - if (!companyId) { + if (!companyId) { return; } isLoading = true; - try { + try { const response = await partsApi.list({ company_id: companyId, page: 1, @@ -116,32 +115,52 @@ toast.success('Partes actualizadas'); } - async function handleDelete() { if (!selectedPart) { toast.error('Selecciona una parte para borrar'); return; } - const confirmed = window.confirm(`¿Estás seguro de que deseas eliminar la parte ${selectedPart.part_number}? Esta acción no se puede deshacer.`); - if (!confirmed) return; + const confirmed = window.confirm( + `¿Estás seguro de que deseas eliminar la parte ${selectedPart.part_number}? Esta acción no se puede deshacer.` + ); + if (!confirmed) return; - isLoading = true; - try { - const companyId = companyStore.activeCompany?.id; - if (!companyId) return; + isLoading = true; + try { + const companyId = companyStore.activeCompany?.id; + if (!companyId) return; - await partsApi.delete(selectedPart.id, companyId); - toast.success('Parte eliminada exitosamente'); - selectedPart = null; - await loadData(); - } catch (e) { - console.error("Error al eliminar:", e); - toast.error('Error al eliminar la parte'); - } finally { - isLoading = false; - } + await partsApi.delete(selectedPart.id, companyId); + toast.success('Parte eliminada exitosamente'); + selectedPart = null; + await loadData(); + } catch (e) { + console.error('Error al eliminar:', e); + toast.error('Error al eliminar la parte'); + } finally { + isLoading = false; + } } + + // Keyboard Shortcuts + useShortcuts('Goods / Parts', [ + { + key: 'Alt+Shift+N', + description: 'New Part', + action: () => goto('/dashboard/goods/parts/edit') + }, + { + key: 'Alt+Shift+R', + description: 'Refresh List', + action: handleRefresh + }, + { + key: 'Alt+Shift+D', + description: 'Delete Selected', + action: handleDelete + } + ]);
@@ -169,11 +188,7 @@
- +
@@ -185,19 +200,11 @@
- +
- +
@@ -218,155 +225,190 @@
- -
- - - - - - - - - - - - - - {#if isLoading} + +
+
- - Número de ParteDescripciónClienteClaseU.M.Fracción
+ - + + + + + + + - {:else if filteredParts.length === 0} - - - - {:else} - {#each filteredParts as part (part.id)} - selectPart(part)} - > - - - - - - - + + + {#if isLoading} + + - {/each} - {/if} - -
Cargando... + + Número de ParteDescripciónClienteClaseU.M.Fracción
- No hay partes registradas -
- - - - {part.part_number} - - {part.description_spanish || ''} -
- {clientsMap[part.client_id] || 'Cargando...'} -
-
- {#if part.part_class} - - {part.part_class} - - {:else} - - - {/if} - {part.unit_of_measure || '-'}{part.fraction || '-'}
Cargando...
+ {:else if filteredParts.length === 0} + + + No hay partes registradas + + + {:else} + {#each filteredParts as part (part.id)} + selectPart(part)} + > + + + + + + {part.part_number} + + + {part.description_spanish || ''} + +
+ {clientsMap[part.client_id] || 'Cargando...'} +
+ + + {#if part.part_class} + + {part.part_class} + + {:else} + - + {/if} + + {part.unit_of_measure || '-'} + {part.fraction || '-'} + + {/each} + {/if} + + +
- -
-
-

Número de Parte

-

- {selectedPart?.part_number || '---'} -

-
+
+
+

+ Número de Parte +

+

+ {selectedPart?.part_number || '---'} +

+
-
- {#if selectedPart} -
-
- -

{selectedPart.description_spanish || 'Sin descripción'}

-
-
- -

{selectedPart.description_english || 'No translation available'}

-
-
- -
-
- -
- - {clientsMap[selectedPart.client_id] || selectedPart.client_id} -
-
-
- - {selectedPart.part_class || '-'} -
-
- -
-
- - {selectedPart.unit_of_measure || '-'} -
-
- - {selectedPart.unit_weight || '-'} -
-
- -
- -

- {selectedPart.fraction || '0000.00.00'} +

+ {#if selectedPart} +
+
+ +

+ {selectedPart.description_spanish || 'Sin descripción'}

+
+ +

+ {selectedPart.description_english || 'No translation available'} +

+
+
- {#if selectedPart.unit_cost} -
- -

- ${selectedPart.unit_cost} {selectedPart.currency_key || 'USD'} -

+
+
+ +
+ + {clientsMap[selectedPart.client_id] || selectedPart.client_id}
- {/if} - {:else} -
- -

Selecciona una parte para ver sus detalles

+
+
+ + {selectedPart.part_class || '-'} +
+
+ +
+
+ + {selectedPart.unit_of_measure || '-'} +
+
+ + {selectedPart.unit_weight || '-'} +
+
+ +
+ +

+ {selectedPart.fraction || '0000.00.00'} +

+
+ + {#if selectedPart.unit_cost} +
+ +

+ ${selectedPart.unit_cost} + {selectedPart.currency_key || 'USD'} +

{/if} -
+ {:else} +
+ +

Selecciona una parte para ver sus detalles

+
+ {/if}
+
-
+
@@ -374,8 +416,15 @@ Nueva Parte - - + +
-
\ No newline at end of file +
diff --git a/start.sh b/start.sh index c5c0b66a..f3e1a213 100755 --- a/start.sh +++ b/start.sh @@ -81,7 +81,7 @@ DEBUG=True ENVIRONMENT=development NODE_ENV=development VITE_API_URL=http://localhost:8000/api -VITE_KEYCLOAK_URL=http://localhost:8080 +VITE_KEYCLOAK_URL=http://localhost:8080/kcauth EOF echo -e "${GREEN}✓ Archivo .env creado con valores por defecto${NC}" fi