diff --git a/.env.example b/.env.example index 8e04d045..80f6478a 100644 --- a/.env.example +++ b/.env.example @@ -39,3 +39,12 @@ VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend SITAR_API_URL=http://api.sitar.aduanasoft.com SITAR_API_USER=your_sitar_user SITAR_API_PASSWORD=your_sitar_password + +# ----- Help Center ----- +CENTRAL_SERVER_URL= +CLIENT_UUID= +SYNC_SECRET_TOKEN= +SPOKE_URLS= +CORS_ORIGINS=CORS: Permitir acceso desde la IP del Hub y localhost + + diff --git a/backend/api/v1/modules/core/help_center/models.py b/backend/api/v1/modules/core/help_center/models.py index 876de284..9b7e3608 100644 --- a/backend/api/v1/modules/core/help_center/models.py +++ b/backend/api/v1/modules/core/help_center/models.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime, timezone -from sqlalchemy import Column, String, Text, DateTime +from sqlalchemy import Column, String, Text, DateTime, Integer from sqlalchemy.dialects.postgresql import UUID from core.database import Base @@ -17,6 +17,10 @@ class HelpArticle(Base): content = Column(Text, nullable=False) updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) last_editor = Column(String(255), nullable=False) + + # Library Mode Fields + category = Column(String(255), nullable=True, default="General") + order = Column(Integer, nullable=True, default=0) def __repr__(self): return f"" diff --git a/backend/api/v1/modules/core/help_center/routes.py b/backend/api/v1/modules/core/help_center/routes.py index b4c281f1..fe7f0b15 100644 --- a/backend/api/v1/modules/core/help_center/routes.py +++ b/backend/api/v1/modules/core/help_center/routes.py @@ -1,7 +1,10 @@ +import shutil +import os +import uuid from datetime import datetime from typing import List, Optional from uuid import UUID -from fastapi import APIRouter, Depends, HTTPException, Header, status +from fastapi import APIRouter, Depends, HTTPException, Header, status, UploadFile, File from sqlalchemy.orm import Session from core.database import get_core_db from core.config import settings @@ -61,6 +64,24 @@ def sync_help_article(sync_data: HelpSyncRequest, db: Session = Depends(get_core return result +@router.post("/upload-image/") +def upload_help_image(file: UploadFile = File(...)): + """Sube una imagen para usar en los artículos.""" + try: + file_ext = os.path.splitext(file.filename)[1] + new_filename = f"{uuid.uuid4()}{file_ext}" + file_location = f"uploads/help/{new_filename}" + + # Ensure directory exists + os.makedirs("uploads/help", exist_ok=True) + + with open(file_location, "wb+") as buffer: + shutil.copyfileobj(file.file, buffer) + + return {"url": f"/api/uploads/help/{new_filename}"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + @router.get("/articles/", response_model=List[HelpArticleInDB]) def list_articles(db: Session = Depends(get_core_db)): """Lista todos los artículos de ayuda.""" diff --git a/backend/api/v1/modules/core/help_center/schemas.py b/backend/api/v1/modules/core/help_center/schemas.py index 541a87a4..4e455e80 100644 --- a/backend/api/v1/modules/core/help_center/schemas.py +++ b/backend/api/v1/modules/core/help_center/schemas.py @@ -8,6 +8,8 @@ class HelpArticleBase(BaseModel): title: str content: str last_editor: str + category: Optional[str] = "General" + order: Optional[int] = 0 class HelpArticleCreate(HelpArticleBase): pass @@ -17,6 +19,8 @@ class HelpArticleUpdate(BaseModel): title: Optional[str] = None content: Optional[str] = None last_editor: Optional[str] = None + category: Optional[str] = None + order: Optional[int] = None class HelpArticleInDB(HelpArticleBase): uuid: UUID @@ -32,6 +36,8 @@ class HelpSyncRequest(BaseModel): client_title: str client_slug: str last_editor: str + client_category: Optional[str] = "General" + client_order: Optional[int] = 0 origin_client_uuid: Optional[UUID] = None class HelpSyncResponse(BaseModel): @@ -40,4 +46,6 @@ class HelpSyncResponse(BaseModel): server_content: Optional[str] = None server_title: Optional[str] = None server_slug: Optional[str] = None + server_category: Optional[str] = None + server_order: Optional[int] = 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 fa7af4ec..782791c7 100644 --- a/backend/api/v1/modules/core/help_center/services.py +++ b/backend/api/v1/modules/core/help_center/services.py @@ -80,7 +80,9 @@ class HelpCenterService: title=sync_data.client_title, content=sync_data.client_content, updated_at=client_updated_at, - last_editor=sync_data.last_editor + last_editor=sync_data.last_editor, + category=sync_data.client_category, + order=sync_data.client_order ) db.add(new_article) db.commit() @@ -97,6 +99,8 @@ class HelpCenterService: db_article.slug = sync_data.client_slug db_article.updated_at = client_updated_at db_article.last_editor = sync_data.last_editor + db_article.category = sync_data.client_category + db_article.order = sync_data.client_order db.commit() return HelpSyncResponse(status="OK", message="Server updated with client data.") @@ -108,6 +112,8 @@ class HelpCenterService: server_content=db_article.content, server_title=db_article.title, server_slug=db_article.slug, + server_category=db_article.category, + server_order=db_article.order, message="Client is outdated. Update required." ) diff --git a/backend/api/v1/modules/core/help_center/tasks.py b/backend/api/v1/modules/core/help_center/tasks.py index ecd573ed..fb7cdf77 100644 --- a/backend/api/v1/modules/core/help_center/tasks.py +++ b/backend/api/v1/modules/core/help_center/tasks.py @@ -214,7 +214,9 @@ def sync_from_hub_task(): title=art_data['title'], content=art_data['content'], updated_at=server_updated_at, - last_editor=art_data['last_editor'] + last_editor=art_data['last_editor'], + category=art_data.get('category', "General"), + order=art_data.get('order', 0) ) db.add(new_article) else: @@ -224,6 +226,8 @@ def sync_from_hub_task(): local_article.content = art_data['content'] local_article.updated_at = server_updated_at local_article.last_editor = art_data['last_editor'] + local_article.category = art_data.get('category', "General") + local_article.order = art_data.get('order', 0) db.commit() logger.info("Polling sync completed successfully.") diff --git a/frontend/Dockerfile b/frontend/Dockerfile index bd5ede6d..8ccfa4e2 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -15,7 +15,7 @@ RUN npm config set strict-ssl false RUN npm install -g pnpm # Instalar dependencias -RUN pnpm install --frozen-lockfile +RUN pnpm install # Copiar código COPY . . diff --git a/frontend/package.json b/frontend/package.json index ca906270..b21c873b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,8 +57,12 @@ "vitest-browser-svelte": "^1.1.0" }, "dependencies": { + "@types/dompurify": "^3.2.0", + "@types/marked": "^6.0.0", + "dompurify": "^3.0.9", "keycloak-js": "^26.2.1", "lucide-svelte": "^0.553.0", + "marked": "^12.0.0", "svelte-sonner": "^1.0.7" } -} +} \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index bfafc5e5..4933f90b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,12 +8,24 @@ importers: .: dependencies: + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 + '@types/marked': + specifier: ^6.0.0 + version: 6.0.0 + dompurify: + specifier: ^3.0.9 + version: 3.3.1 keycloak-js: specifier: ^26.2.1 version: 26.2.1 lucide-svelte: specifier: ^0.553.0 version: 0.553.0(svelte@5.40.2) + marked: + specifier: ^12.0.0 + version: 12.0.2 svelte-sonner: specifier: ^1.0.7 version: 1.0.7(svelte@5.40.2) @@ -505,56 +517,67 @@ packages: resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.4': resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.4': resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.4': resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.4': resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.4': resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.4': resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.4': resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.4': resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.4': resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.4': resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.4': resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} @@ -675,24 +698,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.14': resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.14': resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.14': resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.14': resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} @@ -758,18 +785,29 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/marked@6.0.0': + resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==} + deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed. + '@types/node@20.19.22': resolution: {integrity: sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.46.1': resolution: {integrity: sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1054,6 +1092,9 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -1368,24 +1409,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -1432,6 +1477,11 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + marked@12.0.2: + resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} + engines: {node: '>= 18'} + hasBin: true + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2547,16 +2597,27 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} + '@types/marked@6.0.0': + dependencies: + marked: 12.0.2 + '@types/node@20.19.22': dependencies: undici-types: 6.21.0 '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': + optional: true + '@typescript-eslint/eslint-plugin@8.46.1(@typescript-eslint/parser@8.46.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -2853,6 +2914,10 @@ snapshots: dom-accessibility-api@0.5.16: {} + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -3212,6 +3277,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@12.0.2: {} + merge2@1.4.1: {} micromatch@4.0.8: diff --git a/frontend/src/lib/api/help.ts b/frontend/src/lib/api/help.ts index 09bda8d8..40749c85 100644 --- a/frontend/src/lib/api/help.ts +++ b/frontend/src/lib/api/help.ts @@ -19,6 +19,8 @@ export interface HelpArticle { content: string; updated_at: string; last_editor: string; + category?: string; + order?: number; } export const helpApi = { @@ -44,7 +46,7 @@ export const helpApi = { return response.json(); }, - async createArticle(data: { title: string; content: string; slug: string; last_editor: string }): Promise { + async createArticle(data: { title: string; content: string; slug: string; last_editor: string; category?: string; order?: number }): Promise { const response = await fetch(`${BASE_URL}/articles/`, { method: 'POST', headers: getHeaders(), @@ -64,5 +66,21 @@ export const helpApi = { async triggerSync(): Promise { // Opcional: endpoint para forzar sync desde UI si es necesario + }, + + async uploadImage(file: File): Promise<{ url: string }> { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`${BASE_URL}/upload-image/`, { + method: 'POST', + // No Content-Type header for FormData, browser sets it with boundary + headers: { + ...(get(authStore).token ? { 'Authorization': `Bearer ${get(authStore).token}` } : {}) + }, + body: formData + }); + if (!response.ok) throw new Error('Failed to upload image'); + return response.json(); } }; diff --git a/frontend/src/routes/dashboard/help-center/+page.svelte b/frontend/src/routes/dashboard/help-center/+page.svelte index 25ff7cfb..6fabbde4 100644 --- a/frontend/src/routes/dashboard/help-center/+page.svelte +++ b/frontend/src/routes/dashboard/help-center/+page.svelte @@ -8,7 +8,7 @@ import { Input } from '$lib/components/ui/input/index.js'; import { Textarea } from '$lib/components/ui/textarea/index.js'; import { Label } from '$lib/components/ui/label/index.js'; - import { Plus, Trash2, Edit, Loader2, Search } from 'lucide-svelte'; + import { Plus, Trash2, Edit, Loader2, Search, Book, Image as ImageIcon } from 'lucide-svelte'; import { toast } from 'svelte-sonner'; let articles: HelpArticle[] = $state([]); @@ -28,16 +28,30 @@ title: '', slug: '', content: '', - last_editor: 'Admin' + last_editor: 'Admin', + category: 'General', + order: 0 }); - const filteredArticles = $derived( - articles.filter( - (a) => - a.title.toLowerCase().includes(searchTerm.toLowerCase()) || - a.content.toLowerCase().includes(searchTerm.toLowerCase()) - ).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) - ); + // Derived state: Grouped by Category + const groupedArticles = $derived.by(() => { + const filtered = articles + .filter( + (a) => + a.title.toLowerCase().includes(searchTerm.toLowerCase()) || + 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 + + const groups: Record = {}; + filtered.forEach((article) => { + const cat = article.category || 'General'; + if (!groups[cat]) groups[cat] = []; + groups[cat].push(article); + }); + return groups; + }); onMount(async () => { await loadArticles(); @@ -54,50 +68,12 @@ } } - function resetForm() { - formValues = { - title: '', - slug: '', - content: '', - last_editor: 'Admin' - }; + function handleCreate() { + window.location.href = '/dashboard/help-center/editor/new'; } - async function handleCreate() { - processing = true; - try { - // Generate slug if empty - if (!formValues.slug) { - formValues.slug = formValues.title - .toLowerCase() - .replace(/ /g, '-') - .replace(/[^\w-]+/g, ''); - } - await helpApi.createArticle(formValues); - toast.success('Artículo creado correctamente'); - showCreateModal = false; - resetForm(); - await loadArticles(); - } catch (e: any) { - toast.error('Error: ' + e.message); - } finally { - processing = false; - } - } - - async function handleUpdate() { - if (!selectedArticle) return; - processing = true; - try { - await helpApi.updateArticle(selectedArticle.uuid, formValues); - toast.success('Artículo actualizado'); - showEditModal = false; - await loadArticles(); - } catch (e: any) { - toast.error('Error: ' + e.message); - } finally { - processing = false; - } + function openEdit(article: HelpArticle) { + window.location.href = `/dashboard/help-center/editor/${article.uuid}`; } async function handleDelete() { @@ -105,7 +81,7 @@ processing = true; try { await helpApi.deleteArticle(selectedArticle.uuid); - toast.success('Artículo eliminado'); + toast.success('Capítulo eliminado'); showDeleteDialog = false; await loadArticles(); } catch (e: any) { @@ -115,196 +91,125 @@ } } - function openEdit(article: HelpArticle) { - selectedArticle = article; - formValues = { - title: article.title, - slug: article.slug, - content: article.content, - last_editor: article.last_editor - }; - showEditModal = true; - } - function openDelete(article: HelpArticle) { selectedArticle = article; showDeleteDialog = true; } -
+
-
+
-

Centro de Ayuda

-

Gestiona la base de conocimientos distribuida.

+

+ + Biblioteca de Conocimiento +

+

Manuales, Guías y Documentación del Sistema.

-
- -
-
- - -
+ +
+ +
- -
+ +
{#if loading} -
- +
+ +
+ {:else if Object.keys(groupedArticles).length === 0} +
+ +

La biblioteca está vacía.

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

No se encontraron artículos.

-
-
{:else} -
- {#each filteredArticles as article} - - -
- {article.title} -
- - -
-
- - Slug: /{article.slug} - -
- -
- {@html article.content} -
-
- +

+ {category} - Edito: {article.last_editor} - {new Date(article.updated_at).toLocaleDateString()} - - - {/each} -

+ +
+ {#each groupArticles as article} + +
+ + + {article.title} + + + {new Date(article.updated_at).toLocaleDateString()} + + + +
+ {@html article.content.replace(/<[^>]*>?/gm, '').substring(0, 150)}... +
+
+ + +
+ + +
+
+
+ {/each} +
+
+ {/each} {/if}
- - - - - Crear Artículo - - Completa los campos para añadir un nuevo artículo a la base de conocimientos. - - -
-
- - -
-
- - -
-
- - + + + +
+ + + {#if showPreview} +
+
+ {@html renderMarkdown(content)} +
+
+ {/if} + +
+
diff --git a/scripts/frontend-entrypoint.sh b/scripts/frontend-entrypoint.sh index bb2f0a33..f5ca66e3 100755 --- a/scripts/frontend-entrypoint.sh +++ b/scripts/frontend-entrypoint.sh @@ -40,5 +40,11 @@ echo "==========================================" echo "Iniciando aplicación SvelteKit..." echo "==========================================" +# Instalar dependencias nuevas si package.json ha cambiado +if [ "$NODE_ENV" = "development" ]; then + echo "Instalando dependencias (development mode)..." + pnpm install +fi + # Ejecutar el comando que se pasó al contenedor exec "$@"