diff --git a/backend/api/v1/modules/core/help_center/services.py b/backend/api/v1/modules/core/help_center/services.py index 06b0ccff..e31921bc 100644 --- a/backend/api/v1/modules/core/help_center/services.py +++ b/backend/api/v1/modules/core/help_center/services.py @@ -54,7 +54,10 @@ class HelpCenterService: } # Only append if there's something meaningful beyond "article" - if metadata["content_type"] != "article" or metadata["file_url"]: + if (metadata["content_type"] != "article" or + metadata["file_url"] or + metadata["context_path"] or + metadata["tags"]): content += f"\n\n" return content diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 3b1e30d2..5414a941 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -3,6 +3,7 @@ "hello_world": "Hello, {name} from en!", "sidebar": { "dashboard": "Dashboard", + "help_center": "System Manuals", "reference_data": { "title": "Fixed Catalogs", "codes_pedimento_regimen": "Pedimento and Regime Codes", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 6ecb0021..d17d7d5f 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -3,6 +3,7 @@ "hello_world": "Hello, {name} from es!", "sidebar": { "dashboard": "Dashboard", + "help_center": "Manuales del Sistema", "reference_data": { "title": "Catálogos Fijos", "codes_pedimento_regimen": "Códigos de Pedimento y Régimen", diff --git a/frontend/src/lib/components/help/HelpDrawer.svelte b/frontend/src/lib/components/help/HelpDrawer.svelte index ad3f6a84..0c7d8f14 100644 --- a/frontend/src/lib/components/help/HelpDrawer.svelte +++ b/frontend/src/lib/components/help/HelpDrawer.svelte @@ -14,6 +14,7 @@ Search, Sparkles, FileCode, + FileText, Upload } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; @@ -34,18 +35,109 @@ let searchTerm = $state(''); const isAdmin = $derived($currentUser?.roles?.includes('admin') || false); - const currentPath = $derived(page.url.pathname); + const currentPath = $derived(page.url.pathname + page.url.search); - // Filtrar artículos contextuales basados en la ruta actual + const synonyms: Record = { + 'users': ['usuario', 'colaborador', 'acceso', 'perfil', 'permiso', 'roles'], + 'company': ['empresa', 'compania', 'negocio', 'fiscal', 'emisor', 'razon social', 'rfc'], + 'invoices': ['factura', 'cobro', 'gasto', 'cxc', 'cxp', 'comprobante', 'imp', 'exp', 'facturacion'], + 'packages': ['bulto', 'embalaje', 'empaque', 'pallet', 'contenedor', 'packing'], + 'audit': ['auditoria', 'bitacora', 'log', 'historial', 'evento', 'monitoreo'], + 'pedimentos': ['pedimento', 'aduana', 'valida', 'despacho', 'pedimentacion'], + 'parts': ['parte', 'mercancia', 'producto', 'fraccion', 'item', 'articulo', 'numero de parte'], + 'goods': ['mercancia', 'producto', 'bienes', 'parte', 'item'], + 'manifest': ['manifiesto', 'salida', 'embarque', 'transporte', 'carga'], + 'clients': ['cliente', 'proveedor', 'vendor', 'provider', 'comercial', 'socio', 'proveedores', 'clientes'], + 'transporters': ['transportista', 'fletera', 'chofer', 'conductor', 'camion', 'vehiculo', 'transporte'], + 'settings': ['configuracion', 'ajuste', 'preferencia', 'perfil', 'cuenta'], + 'digitalizacion': ['documento', 'archivo', 'digital', 'expediente', 'pdf', 'xml', 'e-document'], + 'reports': ['reporte', 'estadistica', 'grafica', 'consulta', 'descarga', 'excel', 'kpi'], + 'fractions': ['fraccion', 'arancel', 'nico', 'tarifa', 'impuesto', 'tigie'], + 'reference': ['catalogo', 'fijo', 'referencia', 'maestro', 'base'] + }; + + function getKeywords(str: string): string[] { + const isEdit = str.toLowerCase().includes('/edit') || str.toLowerCase().includes('/editor'); + const isCreate = str.toLowerCase().includes('/create') || str.toLowerCase().includes('/new'); + + const baseKeywords = str + .toLowerCase() + .normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Quitar acentos + .replace(/[/_-]/g, ' ') + .split(/\s+/) + .filter(w => w.length > 2 && !['dashboard', 'general', 'catalogs', 'information', 'management'].includes(w)); + + if (isEdit) baseKeywords.push('edicion', 'editar', 'modificar', 'actualizar'); + if (isCreate) baseKeywords.push('crear', 'nuevo', 'registro', 'alta'); + + // Expandir con sinónimos + const expanded = [...baseKeywords]; + baseKeywords.forEach(kw => { + if (synonyms[kw]) expanded.push(...synonyms[kw]); + // Buscar si la palabra clave es un sinónimo de alguna categoría + Object.entries(synonyms).forEach(([key, values]) => { + if (values.includes(kw)) expanded.push(key); + }); + }); + return [...new Set(expanded)]; + } + + $inspect('HELP_DEBUG_PATH', currentPath); + $inspect('HELP_DEBUG_KEYWORDS', getKeywords(currentPath)); + + // Filtrar artículos contextuales basados en la ruta actual o coincidencias inteligentes const contextualArticles = $derived( articles.filter((a) => { - if (!a.context_path) return false; - try { - const regex = new RegExp(a.context_path); - return regex.test(currentPath); - } catch { - return currentPath.includes(a.context_path); + // 1. Prioridad: Ruta explícita (si existe) + if (a.context_path) { + try { + if (a.context_path.startsWith('/')) { + if (currentPath === a.context_path || currentPath.startsWith(a.context_path + '/')) return true; + } else { + const regex = new RegExp(a.context_path); + if (regex.test(currentPath)) return true; + } + } catch { + if (currentPath.includes(a.context_path)) return true; + } } + + // 2. Inteligencia de Coincidencia (Fuzzy match por palabras clave) + const editIntents = ['edicion', 'editar', 'modificar', 'actualizar', 'corregir', 'edit', 'editor']; + const createIntents = ['crear', 'nuevo', 'registro', 'alta', 'create', 'new']; + const intentionWords = [...editIntents, ...createIntents, 'baja', 'cambio']; + + const pathKeywords = getKeywords(currentPath); + const moduleKeywords = pathKeywords.filter(pk => !intentionWords.includes(pk)); + const pathIntentKeywords = pathKeywords.filter(pk => intentionWords.includes(pk)); + + const titleKeywords = getKeywords(a.title); + const contentKeywords = getKeywords(a.content.substring(0, 100)); + + // Si estamos en un módulo específico (ej. Pedimentos), DEBE coincidir el módulo + const matchesModule = moduleKeywords.length === 0 || moduleKeywords.some(pk => + titleKeywords.some(tk => tk.includes(pk) || pk.includes(tk)) || + contentKeywords.some(ck => ck.includes(pk) || pk.includes(ck)) + ); + + if (!matchesModule) return false; + + // Si hay una intención clara en la URL (crear/editar), filtramos los artículos + // que sean explícitamente de la intención OPUESTA. + if (pathIntentKeywords.length > 0) { + const pathIsEdit = pathIntentKeywords.some(pk => editIntents.includes(pk)); + const pathIsCreate = pathIntentKeywords.some(pk => createIntents.includes(pk)); + + const articleIsEdit = titleKeywords.some(tk => editIntents.includes(tk)); + const articleIsCreate = titleKeywords.some(tk => createIntents.includes(tk)); + + // Bloqueo cruzado: Si estoy creando, no me des manuales que son SOLO de editar. + // (Si el manual sirve para ambos o es general, pasará) + if (pathIsCreate && articleIsEdit && !articleIsCreate) return false; + if (pathIsEdit && articleIsCreate && !articleIsEdit) return false; + } + + return true; }) ); @@ -87,7 +179,7 @@ function startCreate() { editContent = '# Nuevo Artículo\nEscribe el contenido aquí...'; - editTitle = ''; + editTitle = 'Nueva Guía de Ayuda'; isEditing = true; isCreating = true; selectedArticle = { @@ -107,6 +199,7 @@ const newArticle = await helpApi.createArticle({ title: editTitle, content: editContent, + context_path: currentPath, // Captura automática de la ruta real last_editor: $currentUser?.username || 'unknown' }); selectedArticle = newArticle; @@ -144,6 +237,13 @@ .replace(/\*(.*)\*/gim, '$1') .replace(/\n/gim, '
'); } + + function highlightText(text: string, term: string) { + if (!term || term.length < 2) return text; + const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapedTerm})`, 'gi'); + return text.replace(regex, '$1'); + } @@ -176,20 +276,26 @@ /> - + {#if contextualArticles.length > 0 && !searchTerm}
- Recomendado para esta pantalla +
+ Recomendado para ti +
{#each contextualArticles as article} + {/if}
{:else} -
- {#each filteredArticles as article} - +
+ {:else} +
+ {#each displayList as article} +
- {article.category || 'Gral'} - - {/each} -
+ + + {/each} + + {/if} {/if} @@ -311,10 +445,20 @@
- {#if browser} - {@html renderMarkdown(selectedArticle.content)} + {#if selectedArticle.content_type === 'pdf' && selectedArticle.file_url} +
+ +
{:else} -
{selectedArticle.content}
+ {#if browser} + {@html renderMarkdown(selectedArticle.content)} + {:else} +
{selectedArticle.content}
+ {/if} {/if}
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index d040c051..6cadef43 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -572,7 +572,7 @@ export function getSidebarData(): SidebarData { icon: Settings2, }, { - name: "Manuales del Sistema", + name: m["sidebar.help_center"](), url: "/dashboard/help-center", icon: Book, }, diff --git a/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte b/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte index f807b354..d0a3b428 100644 --- a/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte +++ b/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte @@ -74,7 +74,11 @@ const uuid = $page.params.uuid as string; if (uuid === 'new') { loading = false; - // Defaults for new article + // Pre-rellenar context_path si viene en la URL + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('context_path')) { + context_path = urlParams.get('context_path') || ''; + } return; } @@ -286,6 +290,11 @@ +
+ + +

Se autogenera del título si se deja vacío.

+
-
- - -
-
- - -
-
- - -

Expresión regular para activar este artículo en pantallas específicas.

-
-
- - -

Separadas por comas. Ayudan a la búsqueda y contextualización.

-
+ +
+ + Opciones Avanzadas + + +
+
+ + +
+
+ + +
+
+ + +

URL donde aparecerá este artículo.

+
+
+ + +
+
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5346ce8c..c9ac8ce9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -51,7 +51,7 @@ export default defineConfig({ // 'otro-host.com' si necesitas más ], proxy: { - '/api/uploads': { + '/api': { target: 'http://backend:8000', changeOrigin: true }