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..e31921bc 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,11 +48,16 @@ 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" - 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 @@ -82,7 +91,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 +114,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 +134,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 +173,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 +210,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 +247,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/backend/core/security.py b/backend/core/security.py index c2e12023..bfdac99c 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -600,6 +600,10 @@ def validate_company_access( .first() ) + if not company: + print(f"DEBUG: validate_company_access: No se encontró la compañía {company_id} para el tenant {tenant_id}") + logger.warning(f"validate_company_access: No se encontró la compañía {company_id} para el tenant {tenant_id}") + return company is not None except Exception as e: logger.error("Error validating company access: %s", e) diff --git a/backend/main.py b/backend/main.py index 8af725a5..2fc08859 100644 --- a/backend/main.py +++ b/backend/main.py @@ -65,8 +65,8 @@ async def on_startup(): if settings.DEBUG: app.add_middleware(RequestLoggingMiddleware) -app.add_middleware(LicenseValidationMiddleware) app.add_middleware(TenantMiddleware) +app.add_middleware(LicenseValidationMiddleware) app.add_middleware(UserContextMiddleware) # CORS debe ser el último en añadirse para que sea el más externo diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 72f1428b..d24b48c9 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,6 +1,7 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", "hello_world": "Hello, {name} from en!", + "exchange_rate_error_title": "Exchange Rate Error", "dashboard": { "greeting_morning": "Good morning", "greeting_afternoon": "Good afternoon", @@ -103,10 +104,11 @@ }, "sidebar": { "dashboard": "Dashboard", + "help_center": "System Manuals", "management_label": "Management", "bulk_upload": { - "title": "Bulk uploads", - "entry": "CSV import" + "title": "Bulk Uploads", + "entry": "CSV Import" }, "reference_data": { "title": "Fixed Catalogs", @@ -1800,4 +1802,4 @@ "download_csv": "Download CSV" } } -} +} \ No newline at end of file diff --git a/frontend/messages/es.json b/frontend/messages/es.json index c45f076d..708f548a 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1,6 +1,7 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", "hello_world": "Hello, {name} from es!", + "exchange_rate_error_title": "Error de Tipo de Cambio", "dashboard": { "greeting_morning": "Buenos días", "greeting_afternoon": "Buenas tardes", @@ -103,6 +104,7 @@ }, "sidebar": { "dashboard": "Dashboard", + "help_center": "Manuales del Sistema", "management_label": "Gestión", "bulk_upload": { "title": "Cargas masivas", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index dc118038..d34ecb93 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -16,6 +16,7 @@ function normalizeAbsoluteApiBaseUrl(raw: string): string { if (s.startsWith('https:') && !s.startsWith('https://')) { s = 'https://' + s.slice('https:'.length).replace(/^\/+/, ''); } + if (s.startsWith('/')) return s; if (/^https?:\/\//i.test(s)) return s; return `http://${s.replace(/^\/+/, '')}`; } 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 8ed7c314..da3d68db 100644 --- a/frontend/src/lib/components/help/HelpDrawer.svelte +++ b/frontend/src/lib/components/help/HelpDrawer.svelte @@ -1,17 +1,29 @@ - - {#snippet child({ props })} - - {/snippet} - - - - + + +
+
+
{#if selectedArticle} - {/if} - Base de Conocimientos - - +
+ +
+

+ {selectedArticle ? 'Artículo de Ayuda' : 'Centro de Ayuda'} +

+
+
+

Manuales y guías interactivas del sistema.

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

Artículos Disponibles

- {#if isAdmin} - +
+

+ {searchTerm ? 'Resultados de búsqueda' : 'Manuales del Sistema'} +

+ {#if articles.length > 0} + {articles.length} artículos {/if}
+ {#if isLoading} -

Cargando...

+
+
+

Cargando base de conocimientos...

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

- No hay artículos de ayuda disponibles. -

+
+
+ +
+
+

+ {hasError ? 'Error de conexión' : 'Biblioteca vacía'} +

+

+ {hasError + ? 'No pudimos conectar con el servidor de ayuda.' + : 'No hay artículos registrados para esta sección aún.'} +

+
+ {#if hasError} + + {:else if isAdmin} + + {/if} +
+ {:else} + {@const displayList = searchTerm ? filteredArticles : articles} + {#if displayList.length === 0} +
+ +

No encontramos nada para "{searchTerm}"

+ +
+ {:else} +
+ {#each displayList as article} + + {/each} +
+ {/if} {/if} -
- {#each articles as article (article.uuid)} - - {/each} -
- {:else} -
+
+ + +
+
+ + {#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)} + +
+ {#if selectedArticle.content_type === 'pdf' && selectedArticle.file_url} +
+ +
{:else} -
{selectedArticle.content}
+ {#if browser} + {@html renderMarkdown(selectedArticle.content)} + {:else} +
{selectedArticle.content}
+ {/if} {/if}
{/if}
- {/if} -
+
+ {/if} +
diff --git a/frontend/src/lib/components/keyboard/KeyboardManager.svelte b/frontend/src/lib/components/keyboard/KeyboardManager.svelte index 651661df..7f2fa59b 100644 --- a/frontend/src/lib/components/keyboard/KeyboardManager.svelte +++ b/frontend/src/lib/components/keyboard/KeyboardManager.svelte @@ -6,6 +6,7 @@ import { hasAccessTokenInDocument } from '$lib/access-token-cookie-browser'; 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); @@ -335,6 +336,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 41c7427e..499c597f 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'; @@ -650,9 +651,9 @@ export function getSidebarData(): SidebarData { icon: Settings2, }, { - name: m["sidebar.reference_data.ayuda"](), + name: m["sidebar.help_center"](), 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..d0a3b428 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); @@ -71,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; } @@ -88,6 +95,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 +118,9 @@ content_type, file_url, file_size, - mime_type + mime_type, + context_path, + tags }; // Auto-generate slug if missing @@ -279,6 +290,11 @@
+
+ + +

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

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

URL donde aparecerá este artículo.

+
+
+ + +
+
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 6ecf1384..24c69218 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -47,6 +47,10 @@ export default defineConfig({ '/api/uploads': { target: 'http://backend:8000', changeOrigin: true + }, + '/api/v1/core/help-center': { + target: 'http://backend:8000', + changeOrigin: true } } },