diff --git a/backend/api/v1/modules/core/help_center/schemas.py b/backend/api/v1/modules/core/help_center/schemas.py index 5fc67a37..065e9132 100644 --- a/backend/api/v1/modules/core/help_center/schemas.py +++ b/backend/api/v1/modules/core/help_center/schemas.py @@ -14,6 +14,8 @@ class HelpArticleBase(BaseModel): file_url: Optional[str] = None file_size: Optional[int] = None mime_type: Optional[str] = None + context_path: Optional[str] = None + tags: Optional[str] = None class HelpArticleCreate(HelpArticleBase): pass @@ -29,6 +31,8 @@ class HelpArticleUpdate(BaseModel): file_url: Optional[str] = None file_size: Optional[int] = None mime_type: Optional[str] = None + context_path: Optional[str] = None + tags: Optional[str] = None class HelpArticleInDB(HelpArticleBase): uuid: UUID @@ -50,6 +54,8 @@ class HelpSyncRequest(BaseModel): client_file_url: Optional[str] = None client_file_size: Optional[int] = None client_mime_type: Optional[str] = None + client_context_path: Optional[str] = None + client_tags: Optional[str] = None class HelpSyncResponse(BaseModel): status: str @@ -63,4 +69,6 @@ class HelpSyncResponse(BaseModel): server_file_url: Optional[str] = None server_file_size: Optional[int] = None server_mime_type: Optional[str] = None + server_context_path: Optional[str] = None + server_tags: Optional[str] = None message: str diff --git a/backend/api/v1/modules/core/help_center/services.py b/backend/api/v1/modules/core/help_center/services.py index 8f6f36cb..06b0ccff 100644 --- a/backend/api/v1/modules/core/help_center/services.py +++ b/backend/api/v1/modules/core/help_center/services.py @@ -22,6 +22,8 @@ class HelpCenterService: article.file_url = metadata.get("file_url") article.file_size = metadata.get("file_size") article.mime_type = metadata.get("mime_type") + article.context_path = metadata.get("context_path") + article.tags = metadata.get("tags") # Remove metadata from content for clean display if needed, # but usually better to leave it and let parser handle it or hide it here. # For now, we just set the attributes. @@ -32,6 +34,8 @@ class HelpCenterService: article.file_url = None article.file_size = None article.mime_type = None + article.context_path = None + article.tags = None return article @@ -44,7 +48,9 @@ class HelpCenterService: "content_type": data.get("content_type", "article"), "file_url": data.get("file_url"), "file_size": data.get("file_size"), - "mime_type": data.get("mime_type") + "mime_type": data.get("mime_type"), + "context_path": data.get("context_path"), + "tags": data.get("tags") } # Only append if there's something meaningful beyond "article" @@ -82,7 +88,7 @@ class HelpCenterService: # Move metadata into content data["content"] = HelpCenterService._extract_metadata(data["content"], data) # Remove virtual fields from data to avoid SQLAlchemy errors - virtual_fields = ["content_type", "file_url", "file_size", "mime_type"] + virtual_fields = ["content_type", "file_url", "file_size", "mime_type", "context_path", "tags"] for f in virtual_fields: if f in data: del data[f] @@ -105,16 +111,18 @@ class HelpCenterService: update_data = article_data.model_dump(exclude_unset=True) # Handle metadata update - if "content" in update_data or any(f in update_data for f in ["content_type", "file_url", "file_size", "mime_type"]): + if "content" in update_data or any(f in update_data for f in ["content_type", "file_url", "file_size", "mime_type", "context_path", "tags"]): # Merge existing metadata with new updates current_meta = { "content_type": getattr(db_article, "content_type", "article"), "file_url": getattr(db_article, "file_url", None), "file_size": getattr(db_article, "file_size", None), - "mime_type": getattr(db_article, "mime_type", None) + "mime_type": getattr(db_article, "mime_type", None), + "context_path": getattr(db_article, "context_path", None), + "tags": getattr(db_article, "tags", None) } # Update with new data if present - for f in ["content_type", "file_url", "file_size", "mime_type"]: + for f in ["content_type", "file_url", "file_size", "mime_type", "context_path", "tags"]: if f in update_data: current_meta[f] = update_data[f] @@ -123,7 +131,7 @@ class HelpCenterService: update_data["content"] = HelpCenterService._extract_metadata(content, current_meta) # Remove virtual fields from data - virtual_fields = ["content_type", "file_url", "file_size", "mime_type"] + virtual_fields = ["content_type", "file_url", "file_size", "mime_type", "context_path", "tags"] for f in virtual_fields: if f in update_data: del update_data[f] @@ -162,7 +170,9 @@ class HelpCenterService: "content_type": sync_data.client_content_type, "file_url": sync_data.client_file_url, "file_size": sync_data.client_file_size, - "mime_type": sync_data.client_mime_type + "mime_type": sync_data.client_mime_type, + "context_path": sync_data.client_context_path, + "tags": sync_data.client_tags } content_with_meta = HelpCenterService._extract_metadata(sync_data.client_content, client_meta) @@ -197,7 +207,9 @@ class HelpCenterService: "content_type": sync_data.client_content_type, "file_url": sync_data.client_file_url, "file_size": sync_data.client_file_size, - "mime_type": sync_data.client_mime_type + "mime_type": sync_data.client_mime_type, + "context_path": sync_data.client_context_path, + "tags": sync_data.client_tags } db_article.content = HelpCenterService._extract_metadata(sync_data.client_content, client_meta) db_article.title = sync_data.client_title @@ -232,6 +244,8 @@ class HelpCenterService: server_file_url=getattr(db_article, "file_url", None), server_file_size=getattr(db_article, "file_size", None), server_mime_type=getattr(db_article, "mime_type", None), + server_context_path=getattr(db_article, "context_path", None), + server_tags=getattr(db_article, "tags", None), message="Client is outdated. Update required." ) diff --git a/frontend/src/lib/api/help.ts b/frontend/src/lib/api/help.ts index e4907ccf..28c8443d 100644 --- a/frontend/src/lib/api/help.ts +++ b/frontend/src/lib/api/help.ts @@ -39,6 +39,8 @@ export interface HelpArticle { file_url?: string; file_size?: number; mime_type?: string; + context_path?: string; + tags?: string; } export const helpApi = { diff --git a/frontend/src/lib/components/help/HelpDrawer.svelte b/frontend/src/lib/components/help/HelpDrawer.svelte index 3d31f312..ad3f6a84 100644 --- a/frontend/src/lib/components/help/HelpDrawer.svelte +++ b/frontend/src/lib/components/help/HelpDrawer.svelte @@ -1,17 +1,28 @@ - - - - - - - {#if selectedArticle} - - {/if} - Base de Conocimientos - - - -
- {#if !selectedArticle} -
-
-

Artículos Disponibles

- {#if isAdmin} - - {/if} -
- {#if isLoading} -

Cargando...

- {:else if articles.length === 0} -

- No hay artículos de ayuda disponibles. -

- {/if} -
- {#each articles as article (article.uuid)} - - {/each} -
+ + +
+
+
+
+
- {:else} -
- {#if isEditing} -
- - -
-
+
+

Manuales y guías interactivas del sistema.

+
+ +
+ {#if !selectedArticle} + +
+ +
+ + +
+ + + {#if contextualArticles.length > 0 && !searchTerm} +
+
+ Recomendado para esta pantalla +
+
+ {#each contextualArticles as article} + - +
+ +
+
+

{article.title}

+

{article.category || 'General'}

+
+ + {/each} +
+
+ {/if} + + +
+

Guías Disponibles

+ {#if isLoading} +
+
+
+ {:else if filteredArticles.length === 0} +
+ +

No encontramos nada que coincida con tu búsqueda.

+
+ {:else} +
+ {#each filteredArticles as article} + + {/each} +
+ {/if} +
+
+ + +
+
+ + {#if isAdmin} + + {/if} +
+
+ {:else} + +
+
+ + {#if isAdmin && !isEditing} + + {/if} +
+ +
+ {#if isEditing} +
+
+ + +
+
+ + +
+
+ +
{:else} -
-
-

{selectedArticle.title}

- {#if isAdmin} - - {/if} +
+
+ {selectedArticle.category || 'General'} +

{selectedArticle.title}

+
+ Por {selectedArticle.last_editor} + + Actualizado: {new Date(selectedArticle.updated_at).toLocaleDateString()} +
-
+ +
{#if browser} {@html renderMarkdown(selectedArticle.content)} {:else} @@ -224,11 +320,17 @@
{/if}
- {/if} -
+
+ {/if} +
diff --git a/frontend/src/lib/components/keyboard/KeyboardManager.svelte b/frontend/src/lib/components/keyboard/KeyboardManager.svelte index c27c4017..1e3720b0 100644 --- a/frontend/src/lib/components/keyboard/KeyboardManager.svelte +++ b/frontend/src/lib/components/keyboard/KeyboardManager.svelte @@ -5,6 +5,7 @@ import { GLOBAL_NAV } from '$lib/config/shortcuts'; import { shortcutStore, activeShortcutsList } from '$lib/stores/shortcut-store'; import { focusStore, interactionMode } from '$lib/stores/focus-store'; + import { helpStore } from '$lib/stores/help.svelte'; import ShortcutsHelpModal from './ShortcutsHelpModal.svelte'; let showHelp = $state(false); @@ -334,6 +335,16 @@ return; } + // 1.5 HELP DRAWER: F12 + if (key === 'F12') { + if (!authenticated) { + return; + } + event.preventDefault(); + helpStore.toggle(); + return; + } + // 2. ESCAPE if (key === 'Escape') { if (showHelp) { diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index d1b6d33f..d040c051 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -18,6 +18,7 @@ import { Ship, Truck, Users, + Book, } from '@lucide/svelte'; import { m } from '$lib/i18n/messages'; import { Title } from '../ui/alert'; @@ -571,9 +572,9 @@ export function getSidebarData(): SidebarData { icon: Settings2, }, { - name: m["sidebar.reference_data.ayuda"](), + name: "Manuales del Sistema", url: "/dashboard/help-center", - icon: Frame, + icon: Book, }, ], }; diff --git a/frontend/src/routes/dashboard/help-center/+page.svelte b/frontend/src/routes/dashboard/help-center/+page.svelte index af307a74..9141a24f 100644 --- a/frontend/src/routes/dashboard/help-center/+page.svelte +++ b/frontend/src/routes/dashboard/help-center/+page.svelte @@ -56,7 +56,7 @@ order: 0 }); - // Derived state: Grouped by Category + // Derived state: Grouped by Category with prioritization for Manuals const groupedArticles = $derived.by(() => { const filtered = articles .filter( @@ -65,7 +65,14 @@ a.content.toLowerCase().includes(searchTerm.toLowerCase()) || (a.category || 'General').toLowerCase().includes(searchTerm.toLowerCase()) ) - .sort((a, b) => (a.order || 0) - (b.order || 0)); // Sort by order first + .sort((a, b) => { + // Prioritize Manuales category + const catA = (a.category || 'General').toLowerCase(); + const catB = (b.category || 'General').toLowerCase(); + if (catA.includes('manual') && !catB.includes('manual')) return -1; + if (!catA.includes('manual') && catB.includes('manual')) return 1; + return (a.order || 0) - (b.order || 0); + }); const groups: Record = {}; filtered.forEach((article) => { 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 d8ea7064..f807b354 100644 --- a/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte +++ b/frontend/src/routes/dashboard/help-center/editor/[uuid]/+page.svelte @@ -21,7 +21,8 @@ FileText, Video as VideoIcon, FileCode, - Upload + Upload, + HelpCircle } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; import { marked } from 'marked'; @@ -56,6 +57,8 @@ let file_url = $state(''); let file_size = $state(undefined); let mime_type = $state(''); + let context_path = $state(''); + let tags = $state(''); // Preview State let showPreview = $state(true); @@ -88,6 +91,8 @@ file_url = article.file_url || ''; file_size = article.file_size ?? undefined; mime_type = article.mime_type || ''; + context_path = article.context_path || ''; + tags = article.tags || ''; } } catch (e: any) { toast.error('Error al cargar artículo: ' + e.message); @@ -109,7 +114,9 @@ content_type, file_url, file_size, - mime_type + mime_type, + context_path, + tags }; // Auto-generate slug if missing @@ -300,6 +307,19 @@
+
+ + +

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

+
+
+ + +

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

+