feat: implement approval workflow interface and content management services
This commit is contained in:
parent
8063a6c0b6
commit
f6177fbdb5
56
doc/infohub.md
Normal file
56
doc/infohub.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
desarrollado los módulos completos para Info Hub y Workflows de Aprobación, unificando la lógica a través de un servicio reactivo para que la información fluya dinámicamente.
|
||||||
|
|
||||||
|
La aplicación compila al 100% sin advertencias y mantiene el estándar de alta gama (Executive Suite UI) con tarjetas interactivas, animaciones suaves y gráficas vectoriales.
|
||||||
|
|
||||||
|
Aquí tienes el detalle técnico de la implementación de nivel arquitectónico:
|
||||||
|
|
||||||
|
1. Estado Compartido y Lógica Reactiva (Signals)
|
||||||
|
Para comunicar ambos módulos sin acoplar los componentes creados, implementé un servicio core centralizado en
|
||||||
|
|
||||||
|
content.service.ts
|
||||||
|
. Este servicio actúa como la base de datos simulada en memoria y utiliza Angular Signals (signal y computed de lectura optimizada) para sincronizar en tiempo real el catálogo de comunicados y la cola de revisión:
|
||||||
|
|
||||||
|
Mantiene los comunicados ya aprobados y listos para publicarse.
|
||||||
|
Controla la cola de pendientes con niveles incrementales de validación (approvalLevel).
|
||||||
|
Provee métodos para reaccionar a likes, comentarios y compartidos en redes sociales.
|
||||||
|
2. Módulo Info Hub (CMS & Integración Social)
|
||||||
|
Construí el componente en
|
||||||
|
|
||||||
|
info-hub.component.ts
|
||||||
|
con las siguientes características premium:
|
||||||
|
|
||||||
|
Feed de Noticias: Muestra tarjetas elegantes de comunicados de forma cronológica (título, cuerpo, etiquetas de categoría y targets de redes).
|
||||||
|
Sección de Comentarios: Desplegable interactivo por artículo para leer aportes y agregar nuevos en tiempo real.
|
||||||
|
Módulo de Redes Sociales: Widgets estilo "Executive Suite" que muestran el alcance simulado en LinkedIn, X y la Intranet.
|
||||||
|
Analíticas: Un gráfico de líneas SVG fluido y responsivo que proyecta el alcance de contenido en las últimas semanas (sin requerir librerías pesadas externas).
|
||||||
|
Modal de Compartido (Glassmorphism): Al hacer clic en "Compartir", se despliega una interfaz translúcida que permite seleccionar las redes objetivo (LinkedIn, X, Intranet) y simular el incremento del alcance orgánico.
|
||||||
|
3. Módulo Workflows (Formulario & Motor de Aprobación BPM)
|
||||||
|
Creé el componente en
|
||||||
|
|
||||||
|
workflows.component.ts
|
||||||
|
enfocado en la administración de publicaciones:
|
||||||
|
|
||||||
|
Formulario de Redacción: Permite escribir nuevos anuncios, asignarlos a categorías (Tecnología, RR.HH., Operaciones, Seguridad) y marcar los canales objetivo donde se desea propagar.
|
||||||
|
Cola de Aprobaciones Jerárquicas: Lista de forma interactiva las propuestas en curso.
|
||||||
|
Nivel 1 (Amarillo): Revisión Técnica.
|
||||||
|
Nivel 2 (Púrpura): Autorización de Recursos Humanos.
|
||||||
|
Si se rechaza, se elimina de la cola. Si se aprueba en el Nivel 2, el artículo se publica automáticamente en el Info Hub, desapareciendo de la cola del motor BPM y actualizando la lista global al instante.
|
||||||
|
Historial Rápido: Una sección lateral para auditar los últimos artículos aprobados con un enlace rápido para ir a visualizarlos en el portal general.
|
||||||
|
4. Rutas y Cableado de Navegación
|
||||||
|
Registré las dos nuevas rutas lazy-loaded (info-hub y workflows) en
|
||||||
|
|
||||||
|
app.routes.ts
|
||||||
|
, protegiéndolas bajo el guardia de seguridad Active Directory (authGuard).
|
||||||
|
Modifiqué el archivo
|
||||||
|
|
||||||
|
dashboard.component.ts
|
||||||
|
(las tarjetas del menú inicial) para sustituir las pestañas locales anteriores por navegación de rutas reales a través de this.router.navigate(...).
|
||||||
|
Al hacer clic en la tarjeta de Info Hub, te redirige a /info-hub.
|
||||||
|
Al hacer clic en la tarjeta de Workflows, te redirige a /workflows.
|
||||||
|
Cómo probar el flujo completo:
|
||||||
|
Ve al panel de inicio de la Intranet y haz clic en la tarjeta de Workflows.
|
||||||
|
Escribe una nueva propuesta (ej: "Plan de Capacitación Angular 18"), selecciona canales de redes y haz clic en "Enviar a Flujo de Aprobación".
|
||||||
|
En la lista de aprobaciones a la derecha, verás tu post en amarillo (Nivel 1). Haz clic en "Avanzar a Nivel 2". El post cambiará de color a púrpura (Nivel 2).
|
||||||
|
Vuelve a hacer clic en "Aprobar y Publicar". El post se eliminará de la cola BPM y se posteará de forma automatizada.
|
||||||
|
Haz clic en el enlace "Ver en Info Hub" de abajo o haz clic en el logo de la Intranet arriba (que te llevará de vuelta al Dashboard) e ingresa al Info Hub.
|
||||||
|
¡Verás tu nuevo post listo en el feed general! Puedes darle a ❤️, abrir los comentarios para escribir tu opinión, o presionar el botón 🔗 para simular compartirlo en LinkedIn o X.
|
||||||
@ -21,6 +21,16 @@ export const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
loadComponent: () => import('./features/directorio/directorio.component').then(m => m.DirectorioComponent)
|
loadComponent: () => import('./features/directorio/directorio.component').then(m => m.DirectorioComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'info-hub',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
loadComponent: () => import('./features/info-hub/info-hub.component').then(m => m.InfoHubComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'workflows',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
loadComponent: () => import('./features/workflows/workflows.component').then(m => m.WorkflowsComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'reservas',
|
path: 'reservas',
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
|
|||||||
178
frontend/src/app/core/services/content.service.ts
Normal file
178
frontend/src/app/core/services/content.service.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { Injectable, signal, computed } from '@angular/core';
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
body: string;
|
||||||
|
author: string;
|
||||||
|
date: string;
|
||||||
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
|
approvalLevel: number; // 1 = Tech Review, 2 = HR Review, 3 = Approved
|
||||||
|
likes: number;
|
||||||
|
shares: number;
|
||||||
|
commentsCount: number;
|
||||||
|
shareTargets: string[];
|
||||||
|
comments: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ContentService {
|
||||||
|
private currentId = 5;
|
||||||
|
|
||||||
|
// Signal for published articles (visible in Info Hub)
|
||||||
|
private _articles = signal<Article[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Lanzamiento de la Nueva Intranet Corporativa',
|
||||||
|
category: 'Tecnología',
|
||||||
|
body: 'Hoy lanzamos oficialmente nuestra nueva plataforma de intranet con integración a Active Directory y un sistema unificado de reservas de espacios. ¡Optimiza tus flujos diarios desde hoy!',
|
||||||
|
author: 'Jess Miller',
|
||||||
|
date: 'Hace 2 horas',
|
||||||
|
status: 'approved',
|
||||||
|
approvalLevel: 3,
|
||||||
|
likes: 42,
|
||||||
|
shares: 12,
|
||||||
|
commentsCount: 3,
|
||||||
|
shareTargets: ['LinkedIn', 'X', 'Intranet'],
|
||||||
|
comments: [
|
||||||
|
'Excelente diseño, muy responsivo.',
|
||||||
|
'Me encanta el asistente de reservas con IA.',
|
||||||
|
'Gran trabajo a todo el equipo!'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Políticas de Trabajo Híbrido 2026',
|
||||||
|
category: 'Recursos Humanos',
|
||||||
|
body: 'Actualizamos las directrices de trabajo híbrido. Los equipos podrán acordar hasta 3 días de trabajo remoto a la semana, coordinando la ocupación de puestos a través de la autogestión de espacios.',
|
||||||
|
author: 'Dirección de RR.HH.',
|
||||||
|
date: 'Ayer',
|
||||||
|
status: 'approved',
|
||||||
|
approvalLevel: 3,
|
||||||
|
likes: 85,
|
||||||
|
shares: 24,
|
||||||
|
commentsCount: 2,
|
||||||
|
shareTargets: ['LinkedIn', 'Intranet'],
|
||||||
|
comments: [
|
||||||
|
'Gran paso adelante para la flexibilidad.',
|
||||||
|
'Muy claro el proceso de reserva de escritorios.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Proyecto de Sostenibilidad y Oficina Verde',
|
||||||
|
category: 'Operaciones',
|
||||||
|
body: 'Iniciamos el programa Oficina Verde para reducir el consumo de papel y optimizar la climatización de salas mediante sensores IoT integrados. Coordina tus reuniones en salas eco-eficientes.',
|
||||||
|
author: 'Marta Soler (Operaciones)',
|
||||||
|
date: 'Hace 3 días',
|
||||||
|
status: 'approved',
|
||||||
|
approvalLevel: 3,
|
||||||
|
likes: 56,
|
||||||
|
shares: 8,
|
||||||
|
commentsCount: 1,
|
||||||
|
shareTargets: ['X', 'Intranet'],
|
||||||
|
comments: [
|
||||||
|
'¡Apoyo total a esta iniciativa!'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Signal for pending approvals (visible in Workflow Module)
|
||||||
|
private _pending = signal<Article[]>([
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'Manual de Seguridad de Datos para Desarrolladores',
|
||||||
|
category: 'Seguridad',
|
||||||
|
body: 'Documento técnico con las directrices de codificación segura, rotación de secretos en Azure y cumplimiento con OWASP Top 10 para APIs REST.',
|
||||||
|
author: 'David Chen',
|
||||||
|
date: 'Hace 1 hora',
|
||||||
|
status: 'pending',
|
||||||
|
approvalLevel: 1, // 1st level approval needed
|
||||||
|
likes: 0,
|
||||||
|
shares: 0,
|
||||||
|
commentsCount: 0,
|
||||||
|
shareTargets: ['Intranet'],
|
||||||
|
comments: []
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
articles = computed(() => this._articles());
|
||||||
|
pending = computed(() => this._pending());
|
||||||
|
|
||||||
|
addPending(title: string, category: string, body: string, author: string, shareTargets: string[]): void {
|
||||||
|
const newArticle: Article = {
|
||||||
|
id: this.currentId++,
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
body,
|
||||||
|
author,
|
||||||
|
date: 'Hace un momento',
|
||||||
|
status: 'pending',
|
||||||
|
approvalLevel: 1,
|
||||||
|
likes: 0,
|
||||||
|
shares: 0,
|
||||||
|
commentsCount: 0,
|
||||||
|
shareTargets,
|
||||||
|
comments: []
|
||||||
|
};
|
||||||
|
this._pending.update(prev => [newArticle, ...prev]);
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceApproval(id: number): void {
|
||||||
|
let articleToMove: Article | undefined;
|
||||||
|
|
||||||
|
this._pending.update(prev => {
|
||||||
|
const items = prev.map(art => {
|
||||||
|
if (art.id === id) {
|
||||||
|
const nextLevel = art.approvalLevel + 1;
|
||||||
|
if (nextLevel > 2) {
|
||||||
|
// Reaches level 3 -> Approved & Published
|
||||||
|
articleToMove = { ...art, status: 'approved', approvalLevel: 3, date: 'Publicado hace un momento' };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...art, approvalLevel: nextLevel };
|
||||||
|
}
|
||||||
|
return art;
|
||||||
|
});
|
||||||
|
return items.filter(art => art !== null) as Article[];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (articleToMove) {
|
||||||
|
this._articles.update(prev => [articleToMove!, ...prev]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectArticle(id: number): void {
|
||||||
|
this._pending.update(prev => prev.filter(art => art.id !== id));
|
||||||
|
}
|
||||||
|
|
||||||
|
likeArticle(id: number): void {
|
||||||
|
this._articles.update(prev => {
|
||||||
|
return prev.map(art => art.id === id ? { ...art, likes: art.likes + 1 } : art);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shareArticle(id: number): void {
|
||||||
|
this._articles.update(prev => {
|
||||||
|
return prev.map(art => art.id === id ? { ...art, shares: art.shares + 1 } : art);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addComment(id: number, comment: string): void {
|
||||||
|
this._articles.update(prev => {
|
||||||
|
return prev.map(art => {
|
||||||
|
if (art.id === id) {
|
||||||
|
return {
|
||||||
|
...art,
|
||||||
|
comments: [...art.comments, comment],
|
||||||
|
commentsCount: art.commentsCount + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return art;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -78,7 +78,7 @@ interface UserProfile {
|
|||||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:20px;">
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:20px;">
|
||||||
|
|
||||||
<!-- Card 1: Info Hub — Deep Navy #0D1B2A -->
|
<!-- Card 1: Info Hub — Deep Navy #0D1B2A -->
|
||||||
<div (click)="setActiveTab('workflow')"
|
<div (click)="navigateToInfoHub()"
|
||||||
style="position:relative;background:#0D1B2A;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(13,27,42,0.28);">
|
style="position:relative;background:#0D1B2A;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(13,27,42,0.28);">
|
||||||
<!-- Inner decorative border -->
|
<!-- Inner decorative border -->
|
||||||
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.10);border-radius:12px;pointer-events:none;"></div>
|
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.10);border-radius:12px;pointer-events:none;"></div>
|
||||||
@ -118,7 +118,7 @@ interface UserProfile {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 3: Workflows — Deep Viridian #0A2520 -->
|
<!-- Card 3: Workflows — Deep Viridian #0A2520 -->
|
||||||
<div (click)="setActiveTab('workflow')"
|
<div (click)="navigateToWorkflows()"
|
||||||
style="position:relative;background:#0A2520;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(10,37,32,0.30);">
|
style="position:relative;background:#0A2520;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(10,37,32,0.30);">
|
||||||
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
|
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
|
||||||
<div style="position:absolute;bottom:-40px;right:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(16,185,129,0.14) 0%,transparent 70%);pointer-events:none;"></div>
|
<div style="position:absolute;bottom:-40px;right:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(16,185,129,0.14) 0%,transparent 70%);pointer-events:none;"></div>
|
||||||
@ -803,6 +803,14 @@ export class DashboardComponent implements OnInit {
|
|||||||
this.router.navigate(['/directorio']);
|
this.router.navigate(['/directorio']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToInfoHub(): void {
|
||||||
|
this.router.navigate(['/info-hub']);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToWorkflows(): void {
|
||||||
|
this.router.navigate(['/workflows']);
|
||||||
|
}
|
||||||
|
|
||||||
bookSpace() {
|
bookSpace() {
|
||||||
this.bookingSuccess.set(true);
|
this.bookingSuccess.set(true);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -10,12 +10,17 @@ interface Employee {
|
|||||||
initials: string;
|
initials: string;
|
||||||
role: string;
|
role: string;
|
||||||
department: string;
|
department: string;
|
||||||
|
contractType: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
clients: number;
|
clients: number;
|
||||||
rate: number;
|
rate: number;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
email: string;
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
joinedDate: string;
|
||||||
avatarColor: string;
|
avatarColor: string;
|
||||||
|
avatarTextColor: string;
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -25,186 +30,425 @@ interface Employee {
|
|||||||
template: `
|
template: `
|
||||||
<div class="dir-shell">
|
<div class="dir-shell">
|
||||||
|
|
||||||
<!-- ═══════════════════════════════ MAIN PANEL ═══════════════════════════════ -->
|
<!-- ═══════════════════════════════ TIMI PEOPLE NAVBAR ═══════════════════════════════ -->
|
||||||
<main class="dir-main">
|
<nav class="dir-topbar">
|
||||||
|
<div class="dir-topbar__left">
|
||||||
<!-- Module Header -->
|
<div class="dir-topbar__logo">
|
||||||
<header class="dir-header">
|
<span class="dir-topbar__logo-icon">✨</span>
|
||||||
<div class="dir-header__title-block">
|
<span class="dir-topbar__logo-text">App <strong>People</strong></span>
|
||||||
<h1 class="dir-header__title">Directorio de Personal</h1>
|
|
||||||
<div class="dir-header__badge">
|
|
||||||
<span class="dir-header__badge-dot"></span>
|
|
||||||
Sincronizado con Active Directory
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="dir-header__count">{{ filteredEmployees().length }} empleados</span>
|
<ul class="dir-topbar__menu">
|
||||||
</header>
|
<li class="dir-topbar__menu-item dir-topbar__menu-item--active">Employee</li>
|
||||||
|
<li class="dir-topbar__menu-item">Contract</li>
|
||||||
<!-- Search + Filters Bar -->
|
<li class="dir-topbar__menu-item">Department</li>
|
||||||
<div class="dir-filters">
|
<li class="dir-topbar__menu-item">Manpower Planning</li>
|
||||||
<!-- Search -->
|
<li class="dir-topbar__menu-item">Organization Structure</li>
|
||||||
<div class="dir-filters__search">
|
</ul>
|
||||||
<svg class="dir-filters__search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
|
||||||
<circle cx="11" cy="11" r="7" stroke="#78716C" stroke-width="1.8"/>
|
|
||||||
<path d="M16.5 16.5L21 21" stroke="#78716C" stroke-width="1.8" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
class="dir-filters__input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Buscar por nombre, rol o habilidad..."
|
|
||||||
[(ngModel)]="searchQuery"
|
|
||||||
(ngModelChange)="onSearch($event)"
|
|
||||||
/>
|
|
||||||
@if (searchQuery) {
|
|
||||||
<button class="dir-filters__clear" (click)="clearSearch()">✕</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Department dropdown -->
|
|
||||||
<div class="dir-filters__select-wrap">
|
|
||||||
<select class="dir-filters__select" [(ngModel)]="selectedDept" (ngModelChange)="onDeptChange($event)">
|
|
||||||
<option value="ALL">Todos los Departamentos</option>
|
|
||||||
<option value="IT">IT & Desarrollo</option>
|
|
||||||
<option value="Diseño">Diseño y UI/UX</option>
|
|
||||||
<option value="Infraestructura">Infraestructura</option>
|
|
||||||
<option value="Seguridad">Seguridad</option>
|
|
||||||
</select>
|
|
||||||
<svg class="dir-filters__select-arrow" width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
||||||
<path d="M2 4l4 4 4-4" stroke="#78716C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reset pill -->
|
|
||||||
@if (selectedDept !== 'ALL' || searchQuery) {
|
|
||||||
<button class="dir-filters__reset" (click)="resetFilters()">
|
|
||||||
Limpiar filtros
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results summary -->
|
<div class="dir-topbar__right">
|
||||||
<p class="dir-results-summary">
|
<button class="dir-topbar__btn-add" (click)="resetFilters()">+</button>
|
||||||
Mostrando <strong>{{ filteredEmployees().length }}</strong> de <strong>{{ employees().length }}</strong> empleados registrados
|
<button class="dir-topbar__btn-icon">
|
||||||
</p>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="dir-topbar__btn-icon">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||||
|
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="dir-topbar__user-avatar">JM</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════ MAIN CONTENT ═══════════════════════════════ -->
|
||||||
|
<main class="dir-main">
|
||||||
|
|
||||||
|
<!-- Header: Employee Title and Import/Export/Add Buttons -->
|
||||||
|
<div class="dir-content-header">
|
||||||
|
<div class="dir-content-header__left">
|
||||||
|
<h1 class="dir-content-header__title">Employee</h1>
|
||||||
|
<div class="dir-content-header__stats">
|
||||||
|
<span class="dir-content-header__stat-active">● Active 28</span>
|
||||||
|
<span class="dir-content-header__stat-inactive">● Inactive 4</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dir-content-header__right">
|
||||||
|
<button class="dir-btn-secondary">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
|
</svg>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<button class="dir-btn-secondary">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
<button class="dir-btn-primary">
|
||||||
|
<span class="dir-btn-primary__icon">+</span>
|
||||||
|
Add Employee
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters & Search Bar -->
|
||||||
|
<div class="dir-filters-bar">
|
||||||
|
<div class="dir-filters-bar__left">
|
||||||
|
<div class="dir-search-input">
|
||||||
|
<svg class="dir-search-input__icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
[(ngModel)]="searchQuery"
|
||||||
|
(ngModelChange)="onSearch($event)"
|
||||||
|
class="dir-search-input__field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-filter-dropdown">
|
||||||
|
<button class="dir-filter-dropdown__trigger">
|
||||||
|
Type
|
||||||
|
<span class="dir-filter-dropdown__arrow">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-filter-dropdown">
|
||||||
|
<button class="dir-filter-dropdown__trigger">
|
||||||
|
Status
|
||||||
|
<span class="dir-filter-dropdown__arrow">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-filter-dropdown">
|
||||||
|
<button class="dir-filter-dropdown__trigger">
|
||||||
|
Role
|
||||||
|
<span class="dir-filter-dropdown__arrow">▼</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="dir-btn-advanced">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||||
|
</svg>
|
||||||
|
Advance Filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-filters-bar__right">
|
||||||
|
<button class="dir-btn-transfer">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M17 2.1l4 4-4 4M3 6h18M7 21.9l-4-4 4-4M21 18H3"></path>
|
||||||
|
</svg>
|
||||||
|
Transfer Employee
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="dir-view-toggle">
|
||||||
|
<button class="dir-view-toggle__btn dir-view-toggle__btn--active">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="3" width="7" height="7"></rect>
|
||||||
|
<rect x="14" y="14" width="7" height="7"></rect>
|
||||||
|
<rect x="3" y="14" width="7" height="7"></rect>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="dir-view-toggle__btn">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="8" y1="6" x2="21" y2="6"></line>
|
||||||
|
<line x1="8" y1="12" x2="21" y2="12"></line>
|
||||||
|
<line x1="8" y1="18" x2="21" y2="18"></line>
|
||||||
|
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
||||||
|
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
||||||
|
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Employee Grid -->
|
<!-- Employee Grid -->
|
||||||
<div class="dir-grid">
|
<div class="dir-grid">
|
||||||
@for (emp of filteredEmployees(); track emp.id) {
|
@for (emp of filteredEmployees(); track emp.id) {
|
||||||
<article class="dir-card">
|
<article class="dir-card">
|
||||||
|
<!-- Top bar with Active badge & Options -->
|
||||||
<!-- AD ID chip (monospace, top-right) -->
|
<div class="dir-card__top">
|
||||||
<div class="dir-card__ad-id">{{ emp.adId }}</div>
|
<span class="dir-card__status-badge"
|
||||||
|
[ngClass]="{
|
||||||
<!-- Avatar + Rating row -->
|
'dir-card__status-badge--active': emp.status === 'Active',
|
||||||
<div class="dir-card__avatar-row">
|
'dir-card__status-badge--invited': emp.status === 'Invited'
|
||||||
<div class="dir-card__avatar" [style.background]="emp.avatarColor">
|
}">
|
||||||
{{ emp.initials }}
|
{{ emp.status }}
|
||||||
</div>
|
</span>
|
||||||
<div class="dir-card__rating">
|
<button class="dir-card__options">···</button>
|
||||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="#C9973C">
|
|
||||||
<path d="M6 1l1.39 2.82L10.5 4.27l-2.25 2.19.53 3.1L6 8.1l-2.78 1.46.53-3.1L1.5 4.27l3.11-.45L6 1z"/>
|
|
||||||
</svg>
|
|
||||||
<span>{{ emp.rating }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Name + Role -->
|
<!-- Avatar & Identity -->
|
||||||
<div class="dir-card__identity">
|
<div class="dir-card__body">
|
||||||
|
<div class="dir-card__avatar-container">
|
||||||
|
<div class="dir-card__avatar" [style.background]="emp.avatarColor" [style.color]="emp.avatarTextColor">
|
||||||
|
{{ emp.initials }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h3 class="dir-card__name">{{ emp.name }}</h3>
|
<h3 class="dir-card__name">{{ emp.name }}</h3>
|
||||||
<span class="dir-card__role">{{ emp.role }}</span>
|
<span class="dir-card__role">{{ emp.role }}</span>
|
||||||
<span class="dir-card__dept">{{ emp.department }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tags -->
|
|
||||||
<div class="dir-card__tags">
|
|
||||||
@for (tag of emp.tags; track tag) {
|
|
||||||
<span class="dir-card__tag">{{ tag }}</span>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<div class="dir-card__divider"></div>
|
<div class="dir-card__divider"></div>
|
||||||
|
|
||||||
<!-- Metrics -->
|
<!-- Detailed Info Block -->
|
||||||
<div class="dir-card__metrics">
|
<div class="dir-card__details">
|
||||||
<div class="dir-card__metric">
|
<div class="dir-card__detail-item">
|
||||||
<span class="dir-card__metric-label">Clientes</span>
|
<span class="dir-card__detail-label">#{{ emp.adId }}</span>
|
||||||
<span class="dir-card__metric-value">{{ emp.clients }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dir-card__metric-sep"></div>
|
|
||||||
<div class="dir-card__metric">
|
<div class="dir-card__detail-item">
|
||||||
<span class="dir-card__metric-label">Tarifa/Hora</span>
|
<svg class="dir-card__detail-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<span class="dir-card__metric-value">\${{ emp.rate }}</span>
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect>
|
||||||
|
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="dir-card__detail-text">{{ emp.department }} - {{ emp.contractType }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-card__detail-item">
|
||||||
|
<svg class="dir-card__detail-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||||
|
<polyline points="22,6 12,13 2,6"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span class="dir-card__detail-text text-truncate">{{ emp.email }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dir-card__detail-item">
|
||||||
|
<svg class="dir-card__detail-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="dir-card__detail-text">{{ emp.phone }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- Divider -->
|
||||||
<button class="dir-card__cta" (click)="contact(emp)">
|
<div class="dir-card__divider"></div>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
|
||||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"
|
|
||||||
stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
Get In Touch
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
<!-- Footer info -->
|
||||||
|
<div class="dir-card__footer">
|
||||||
|
<span class="dir-card__joined">Joined on {{ emp.joinedDate }}</span>
|
||||||
|
<button class="dir-card__btn-details" (click)="contact(emp)">
|
||||||
|
View details >
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Empty state -->
|
|
||||||
@if (filteredEmployees().length === 0) {
|
|
||||||
<div class="dir-empty">
|
|
||||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
||||||
<circle cx="22" cy="22" r="14" stroke="#D4C5A9" stroke-width="2"/>
|
|
||||||
<path d="M32 32L42 42" stroke="#D4C5A9" stroke-width="2.5" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
<p>Sin resultados para <strong>"{{ searchQuery }}"</strong></p>
|
|
||||||
<button (click)="resetFilters()">Limpiar búsqueda</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
<!-- Empty state -->
|
||||||
</div>
|
@if (filteredEmployees().length === 0) {
|
||||||
|
<div class="dir-empty">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="22" cy="22" r="14"></circle>
|
||||||
|
<line x1="32" y1="32" x2="42" y2="42"></line>
|
||||||
|
</svg>
|
||||||
|
<p class="dir-empty__text">No se encontraron resultados para <strong>"{{ searchQuery }}"</strong></p>
|
||||||
|
<button (click)="resetFilters()" class="dir-empty__btn">Limpiar filtros</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Contact toast -->
|
</main>
|
||||||
@if (contactToast()) {
|
|
||||||
<div class="dir-toast">
|
<!-- Contact toast -->
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
@if (contactToast()) {
|
||||||
<path d="M20 6L9 17l-5-5" stroke="#065F46" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<div class="dir-toast">
|
||||||
</svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
Mensaje enviado a {{ contactToast() }}
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</div>
|
</svg>
|
||||||
}
|
Sincronizando canal AD para <strong>{{ contactToast() }}</strong>...
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
`,
|
`,
|
||||||
styleUrls: ['./directorio.component.css']
|
styleUrls: ['./directorio.component.css']
|
||||||
})
|
})
|
||||||
export class DirectorioComponent implements OnInit {
|
export class DirectorioComponent implements OnInit {
|
||||||
readonly router = inject(Router);
|
|
||||||
|
|
||||||
contactToast = signal('');
|
contactToast = signal('');
|
||||||
|
|
||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
selectedDept = 'ALL';
|
|
||||||
|
|
||||||
private _searchSignal = signal('');
|
private _searchSignal = signal('');
|
||||||
private _deptSignal = signal('ALL');
|
|
||||||
|
|
||||||
employees = signal<Employee[]>([]);
|
employees = signal<Employee[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
adId: 'EMP01',
|
||||||
|
name: 'Bagus Fikri',
|
||||||
|
initials: 'BF',
|
||||||
|
role: 'CEO',
|
||||||
|
department: 'Managerial',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.9,
|
||||||
|
clients: 85,
|
||||||
|
rate: 150,
|
||||||
|
tags: ['Leadership', 'Strategy', 'Active Directory'],
|
||||||
|
email: 'bagusfikri@gmail.com',
|
||||||
|
phone: '+62 123 456 789',
|
||||||
|
joinedDate: '29 Oct, 2020',
|
||||||
|
avatarColor: '#DEF7EC',
|
||||||
|
avatarTextColor: '#03543F',
|
||||||
|
status: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
adId: 'EMP02',
|
||||||
|
name: 'Indrapm',
|
||||||
|
initials: 'IP',
|
||||||
|
role: 'Illustrator',
|
||||||
|
department: 'Design',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.8,
|
||||||
|
clients: 64,
|
||||||
|
rate: 65,
|
||||||
|
tags: ['Illustration', 'Vector', 'Figma'],
|
||||||
|
email: 'indrapm@gmail.com',
|
||||||
|
phone: '+62 765 432 109',
|
||||||
|
joinedDate: '1 Feb, 2019',
|
||||||
|
avatarColor: '#E1EFFE',
|
||||||
|
avatarTextColor: '#1E429F',
|
||||||
|
status: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
adId: 'EMP03',
|
||||||
|
name: 'Mufti Hidayat',
|
||||||
|
initials: 'MH',
|
||||||
|
role: 'Project Manager',
|
||||||
|
department: 'Managerial',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.7,
|
||||||
|
clients: 42,
|
||||||
|
rate: 90,
|
||||||
|
tags: ['Agile', 'Scrum', 'Planning'],
|
||||||
|
email: 'mufti@gmail.com',
|
||||||
|
phone: '+62 112 233 445',
|
||||||
|
joinedDate: '15 Feb, 2022',
|
||||||
|
avatarColor: '#FDF2F2',
|
||||||
|
avatarTextColor: '#9B1C1C',
|
||||||
|
status: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
adId: 'EMP04',
|
||||||
|
name: 'Fauzan Arditianyah',
|
||||||
|
initials: 'FA',
|
||||||
|
role: 'UI Designer',
|
||||||
|
department: 'Design',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.9,
|
||||||
|
clients: 75,
|
||||||
|
rate: 80,
|
||||||
|
tags: ['UI Design', 'Wireframing', 'Prototyping'],
|
||||||
|
email: 'fauzan@gmail.com',
|
||||||
|
phone: '+62 998 877 665',
|
||||||
|
joinedDate: '22 Sep, 2018',
|
||||||
|
avatarColor: '#F0F5FF',
|
||||||
|
avatarTextColor: '#5850EC',
|
||||||
|
status: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
adId: 'EMP05',
|
||||||
|
name: 'Raihan Fikri',
|
||||||
|
initials: 'RF',
|
||||||
|
role: 'QC & Research',
|
||||||
|
department: 'Human Resources',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.6,
|
||||||
|
clients: 20,
|
||||||
|
rate: 70,
|
||||||
|
tags: ['Quality', 'Testing', 'Research'],
|
||||||
|
email: 'raihan@gmail.com',
|
||||||
|
phone: '+62 555 666 777',
|
||||||
|
joinedDate: '10 Oct, 2020',
|
||||||
|
avatarColor: '#EDEBFE',
|
||||||
|
avatarTextColor: '#5521B5',
|
||||||
|
status: 'Invited'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
adId: 'EMP06',
|
||||||
|
name: 'Panji Dwi',
|
||||||
|
initials: 'PD',
|
||||||
|
role: 'UI Designer',
|
||||||
|
department: 'Design',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.8,
|
||||||
|
clients: 58,
|
||||||
|
rate: 75,
|
||||||
|
tags: ['UI/UX', 'Figma', 'Responsive'],
|
||||||
|
email: 'panji@gmail.com',
|
||||||
|
phone: '+62 333 444 555',
|
||||||
|
joinedDate: '5 May, 2021',
|
||||||
|
avatarColor: '#E3F2FD',
|
||||||
|
avatarTextColor: '#0D47A1',
|
||||||
|
status: 'Invited'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
adId: 'EMP07',
|
||||||
|
name: 'Bagas',
|
||||||
|
initials: 'B',
|
||||||
|
role: 'UI Designer',
|
||||||
|
department: 'Design',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.8,
|
||||||
|
clients: 60,
|
||||||
|
rate: 75,
|
||||||
|
tags: ['UI Design', 'Design Systems'],
|
||||||
|
email: 'bagas@gmail.com',
|
||||||
|
phone: '+62 888 999 000',
|
||||||
|
joinedDate: '12 Aug, 2022',
|
||||||
|
avatarColor: '#FCE8E6',
|
||||||
|
avatarTextColor: '#C53030',
|
||||||
|
status: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
adId: 'EMP08',
|
||||||
|
name: 'Luqito Raymorley',
|
||||||
|
initials: 'LR',
|
||||||
|
role: 'Project Manager',
|
||||||
|
department: 'Administration',
|
||||||
|
contractType: 'Fulltime',
|
||||||
|
rating: 4.9,
|
||||||
|
clients: 90,
|
||||||
|
rate: 110,
|
||||||
|
tags: ['Management', 'Delivery', 'Budgets'],
|
||||||
|
email: 'luqito@gmail.com',
|
||||||
|
phone: '+62 444 333 222',
|
||||||
|
joinedDate: '30 Nov, 2017',
|
||||||
|
avatarColor: '#E8F5E9',
|
||||||
|
avatarTextColor: '#1B5E20',
|
||||||
|
status: 'Active'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
filteredEmployees = computed(() => {
|
filteredEmployees = computed(() => {
|
||||||
const q = this._searchSignal().toLowerCase().trim();
|
const q = this._searchSignal().toLowerCase().trim();
|
||||||
const dept = this._deptSignal();
|
|
||||||
|
|
||||||
return this.employees().filter(emp => {
|
return this.employees().filter(emp => {
|
||||||
const matchesSearch = !q ||
|
return !q ||
|
||||||
emp.name.toLowerCase().includes(q) ||
|
emp.name.toLowerCase().includes(q) ||
|
||||||
emp.role.toLowerCase().includes(q) ||
|
emp.role.toLowerCase().includes(q) ||
|
||||||
emp.tags.some(t => t.toLowerCase().includes(q));
|
emp.department.toLowerCase().includes(q) ||
|
||||||
|
emp.adId.toLowerCase().includes(q);
|
||||||
const matchesDept = dept === 'ALL' ||
|
|
||||||
emp.department === dept;
|
|
||||||
|
|
||||||
return matchesSearch && matchesDept;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -214,10 +458,6 @@ export class DirectorioComponent implements OnInit {
|
|||||||
this._searchSignal.set(value);
|
this._searchSignal.set(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeptChange(value: string): void {
|
|
||||||
this._deptSignal.set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSearch(): void {
|
clearSearch(): void {
|
||||||
this.searchQuery = '';
|
this.searchQuery = '';
|
||||||
this._searchSignal.set('');
|
this._searchSignal.set('');
|
||||||
@ -225,9 +465,7 @@ export class DirectorioComponent implements OnInit {
|
|||||||
|
|
||||||
resetFilters(): void {
|
resetFilters(): void {
|
||||||
this.searchQuery = '';
|
this.searchQuery = '';
|
||||||
this.selectedDept = 'ALL';
|
|
||||||
this._searchSignal.set('');
|
this._searchSignal.set('');
|
||||||
this._deptSignal.set('ALL');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contact(emp: Employee): void {
|
contact(emp: Employee): void {
|
||||||
|
|||||||
344
frontend/src/app/features/info-hub/info-hub.component.ts
Normal file
344
frontend/src/app/features/info-hub/info-hub.component.ts
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ContentService, Article } from '../../core/services/content.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-info-hub',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
template: `
|
||||||
|
<div class="flex flex-col gap-6 p-2 lg:p-6 min-h-[85vh] animate-fadeIn">
|
||||||
|
|
||||||
|
<!-- Module Header -->
|
||||||
|
<header class="flex justify-between items-center flex-wrap gap-4 border-b border-black/5 pb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black text-slate-900 uppercase tracking-tight">Info Hub</h1>
|
||||||
|
<p class="text-sm text-slate-500 font-medium">Información corporativa centralizada e integración de redes sociales</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 text-xs">
|
||||||
|
<span class="bg-indigo-50 border border-indigo-100 px-3 py-1.5 rounded-full text-indigo-600 font-bold flex items-center gap-1.5">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-indigo-500 animate-pulse"></span>
|
||||||
|
Canal Interno Activo
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Grid Layout -->
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
<!-- Left 2 columns: News Feed -->
|
||||||
|
<div class="xl:col-span-2 flex flex-col gap-6">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 tracking-tight">Comunicados y Publicaciones Recientes</h2>
|
||||||
|
<span class="text-xs font-semibold text-slate-400 font-mono">{{ contentService.articles().length }} artículos publicados</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feed Grid -->
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
@for (art of contentService.articles(); track art.id) {
|
||||||
|
<!-- Article Card (Jowel style / Glassmorphic look in light mode) -->
|
||||||
|
<article class="bg-white border border-slate-200/80 rounded-3xl p-6 shadow-sm hover:shadow-md transition-all duration-300 relative flex flex-col gap-4">
|
||||||
|
|
||||||
|
<!-- Category tag & date -->
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<span class="px-3 py-1 rounded-full bg-slate-100 text-slate-600 font-bold uppercase tracking-wider text-[9px]">
|
||||||
|
{{ art.category }}
|
||||||
|
</span>
|
||||||
|
<span class="text-slate-400 font-medium">{{ art.date }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title and body -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-xl font-extrabold text-slate-900 leading-snug">{{ art.title }}</h3>
|
||||||
|
<p class="text-sm text-slate-600 leading-relaxed font-light">{{ art.body }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Share targets preview -->
|
||||||
|
<div class="flex items-center gap-2 flex-wrap text-[10px] text-slate-400">
|
||||||
|
<span class="font-bold uppercase tracking-wider">Publicado en:</span>
|
||||||
|
@for (t of art.shareTargets; track t) {
|
||||||
|
<span class="bg-slate-50 border border-slate-200 px-2 py-0.5 rounded text-slate-600 font-mono">
|
||||||
|
{{ t }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-slate-100" />
|
||||||
|
|
||||||
|
<!-- Card Actions (Likes, comments, shares) -->
|
||||||
|
<div class="flex justify-between items-center flex-wrap gap-4 text-xs font-bold">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- Like button -->
|
||||||
|
<button (click)="contentService.likeArticle(art.id)" class="flex items-center gap-1.5 text-slate-500 hover:text-red-500 transition-colors py-1 px-2.5 rounded-lg hover:bg-red-50">
|
||||||
|
<span>❤️</span>
|
||||||
|
<span>{{ art.likes }}</span>
|
||||||
|
</button>
|
||||||
|
<!-- Comments trigger -->
|
||||||
|
<button (click)="toggleComments(art.id)" class="flex items-center gap-1.5 text-slate-500 hover:text-blue-500 transition-colors py-1 px-2.5 rounded-lg hover:bg-blue-50">
|
||||||
|
<span>💬</span>
|
||||||
|
<span>{{ art.commentsCount }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Share trigger button -->
|
||||||
|
<button (click)="openShareModal(art)" class="flex items-center gap-1.5 bg-slate-900 hover:bg-amber-600 text-white hover:text-white px-4 py-2 rounded-2xl transition-all duration-300 font-bold uppercase tracking-wider text-[10px]">
|
||||||
|
<span>🔗 Compartir</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expanded Comments Section -->
|
||||||
|
@if (commentsExpanded(art.id)) {
|
||||||
|
<div class="mt-2 p-4 bg-slate-50 rounded-2xl flex flex-col gap-3 animate-fadeIn">
|
||||||
|
<h4 class="text-xs font-bold text-slate-500 uppercase tracking-widest">Comentarios ({{ art.comments.length }})</h4>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2.5 max-h-48 overflow-y-auto pr-1">
|
||||||
|
@for (com of art.comments; track com) {
|
||||||
|
<div class="text-xs bg-white p-2.5 border border-slate-200/50 rounded-xl leading-relaxed">
|
||||||
|
<span class="font-semibold text-slate-700 block mb-0.5">Colaborador</span>
|
||||||
|
<span class="text-slate-600 font-light">{{ com }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Comment Form -->
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Escribe un comentario..."
|
||||||
|
[(ngModel)]="newComments[art.id]"
|
||||||
|
(keyup.enter)="submitComment(art.id)"
|
||||||
|
class="flex-grow px-4 py-2 border border-slate-200 rounded-full text-xs outline-none focus:border-slate-400 bg-white" />
|
||||||
|
<button (click)="submitComment(art.id)" class="px-4 py-2 bg-slate-900 text-white rounded-full text-xs font-bold hover:bg-slate-800 transition">
|
||||||
|
Enviar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right column: Social Integration and Reach Charts -->
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 tracking-tight">Redes Sociales & Impacto</h2>
|
||||||
|
|
||||||
|
<!-- Social Networks Reach Widget (Navy design style) -->
|
||||||
|
<div class="relative bg-[#0D1B2A] rounded-3xl p-6 text-white overflow-hidden shadow-lg border border-white/5 flex flex-col gap-5 min-h-[220px]">
|
||||||
|
<div class="absolute inset-10 border border-white/10 rounded-2xl pointer-events-none"></div>
|
||||||
|
<div class="absolute -top-10 -right-10 w-36 h-36 bg-radial-gradient from-amber-500/10 to-transparent blur-xl pointer-events-none"></div>
|
||||||
|
|
||||||
|
<span class="text-amber-500 font-bold uppercase tracking-widest text-[9px]">Métricas Globales</span>
|
||||||
|
<div class="flex justify-between items-end">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-3xl font-black tracking-tight leading-none">12,450</h3>
|
||||||
|
<span class="text-[10px] text-white/50 font-bold uppercase tracking-widest mt-1 block">Impresiones Totales</span>
|
||||||
|
</div>
|
||||||
|
<span class="bg-emerald-500/20 text-emerald-400 font-bold text-xs px-2.5 py-0.5 rounded-full">+24% este mes</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-white/10" />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-center">
|
||||||
|
<div class="bg-white/5 p-3 rounded-2xl border border-white/5">
|
||||||
|
<span class="text-[9px] text-white/40 font-bold uppercase tracking-wider block mb-1">LinkedIn</span>
|
||||||
|
<span class="text-lg font-extrabold text-white">8.4k reach</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/5 p-3 rounded-2xl border border-white/5">
|
||||||
|
<span class="text-[9px] text-white/40 font-bold uppercase tracking-wider block mb-1">X (Twitter)</span>
|
||||||
|
<span class="text-lg font-extrabold text-white">4.0k reach</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reach Projection SVG Chart (Plum style) -->
|
||||||
|
<div class="relative bg-[#1A0E2E] rounded-3xl p-6 text-white overflow-hidden shadow-lg border border-white/5 flex flex-col gap-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-widest text-purple-300">Alcance de Contenidos</h3>
|
||||||
|
<span class="text-[10px] font-mono text-purple-400 font-bold bg-purple-500/15 px-2 py-0.5 rounded-full">KPI Activo</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-white/50 font-light">Visualización del crecimiento e impacto de comunicados internos.</p>
|
||||||
|
|
||||||
|
<!-- Pure SVG Line Chart -->
|
||||||
|
<div class="w-full h-32 mt-2">
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 400 150" preserveAspectRatio="none">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="chartGradInfo" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#C9973C" stop-opacity="0.35"/>
|
||||||
|
<stop offset="100%" stop-color="#C9973C" stop-opacity="0.0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<line x1="40" y1="120" x2="380" y2="120" stroke="rgba(255,255,255,0.06)" stroke-width="1" />
|
||||||
|
<line x1="40" y1="75" x2="380" y2="75" stroke="rgba(255,255,255,0.06)" stroke-width="1" />
|
||||||
|
<line x1="40" y1="30" x2="380" y2="30" stroke="rgba(255,255,255,0.06)" stroke-width="1" />
|
||||||
|
|
||||||
|
<path d="M 40,120 Q 120,110 200,60 T 360,25 L 360,130 L 40,130 Z" fill="url(#chartGradInfo)" />
|
||||||
|
<path d="M 40,120 Q 120,110 200,60 T 360,25" fill="none" stroke="#C9973C" stroke-width="3.5" />
|
||||||
|
|
||||||
|
<circle cx="40" cy="120" r="4.5" fill="#C9973C" />
|
||||||
|
<circle cx="200" cy="60" r="4.5" fill="#C9973C" />
|
||||||
|
<circle cx="360" cy="25" r="5" fill="#E9D5FF" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between text-[9px] text-white/40 font-mono mt-1">
|
||||||
|
<span>Semana 1</span>
|
||||||
|
<span>Semana 2</span>
|
||||||
|
<span>Semana 3</span>
|
||||||
|
<span>Semana 4</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Sharing Activity Feed (Viridian style) -->
|
||||||
|
<div class="relative bg-[#0A2520] rounded-3xl p-6 text-white overflow-hidden shadow-lg border border-white/5 flex flex-col gap-4">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-widest text-emerald-400">Canales de Redes Sociales</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex justify-between items-center text-xs p-3 rounded-2xl bg-white/5 border border-white/5">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">Campañas en LinkedIn</span>
|
||||||
|
<span class="text-[10px] text-emerald-300/70">Automatización de posts habilitada</span>
|
||||||
|
</div>
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center text-xs p-3 rounded-2xl bg-white/5 border border-white/5">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-bold">API Integrada de X</span>
|
||||||
|
<span class="text-[10px] text-emerald-300/70">Publicación inmediata activa</span>
|
||||||
|
</div>
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sharing Modal Overlay (Glassmorphism) -->
|
||||||
|
@if (activeShareModal()) {
|
||||||
|
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-md flex items-center justify-center z-50 animate-fadeIn" (click)="closeShareModal()">
|
||||||
|
<div class="bg-white border border-slate-200 w-full max-w-lg p-6 rounded-3xl shadow-2xl flex flex-col gap-5 m-4 animate-scaleUp" (click)="$event.stopPropagation()">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="text-lg font-black text-slate-900 uppercase tracking-tight">Compartir Publicación</h3>
|
||||||
|
<button (click)="closeShareModal()" class="text-slate-400 hover:text-slate-700 text-lg">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-slate-500 font-light">Selecciona los destinos de red social donde deseas propagar este artículo corporativo:</p>
|
||||||
|
|
||||||
|
<div class="bg-slate-50 p-4 border border-slate-100 rounded-2xl">
|
||||||
|
<strong class="text-sm text-slate-800 block mb-1">{{ activeShareModal()?.title }}</strong>
|
||||||
|
<p class="text-xs text-slate-500 line-clamp-3 leading-relaxed">{{ activeShareModal()?.body }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social check options -->
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<label class="flex items-center gap-3 p-3 border border-slate-150 rounded-2xl bg-white hover:bg-slate-50 cursor-pointer text-xs font-semibold text-slate-700 transition">
|
||||||
|
<input type="checkbox" [(ngModel)]="shareLinkedIn" class="w-4 h-4 accent-indigo-600 rounded" />
|
||||||
|
<span>🔗 Publicar en <strong>LinkedIn corporativo</strong></span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-3 p-3 border border-slate-150 rounded-2xl bg-white hover:bg-slate-50 cursor-pointer text-xs font-semibold text-slate-700 transition">
|
||||||
|
<input type="checkbox" [(ngModel)]="shareX" class="w-4 h-4 accent-indigo-600 rounded" />
|
||||||
|
<span>🔗 Publicar en <strong>X (Twitter) oficial</strong></span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-3 p-3 border border-slate-150 rounded-2xl bg-white hover:bg-slate-50 cursor-pointer text-xs font-semibold text-slate-700 transition">
|
||||||
|
<input type="checkbox" [(ngModel)]="shareIntranet" class="w-4 h-4 accent-indigo-600 rounded" />
|
||||||
|
<span>🔗 Publicar en la <strong>Intranet de la Empresa</strong></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Action Buttons -->
|
||||||
|
<div class="flex gap-4 mt-2 justify-end">
|
||||||
|
<button (click)="closeShareModal()" class="px-5 py-2.5 rounded-full border border-slate-200 text-xs font-bold hover:bg-slate-50 transition text-slate-600">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button (click)="confirmShare()" class="px-5 py-2.5 rounded-full bg-slate-900 hover:bg-amber-600 text-white font-bold text-xs uppercase tracking-wider transition-all duration-300">
|
||||||
|
Publicar e Incrementar Alcance
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
.animate-fadeIn {
|
||||||
|
animation: fadeIn 0.35s ease-out forwards;
|
||||||
|
}
|
||||||
|
.animate-scaleUp {
|
||||||
|
animation: scaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes scaleUp {
|
||||||
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class InfoHubComponent {
|
||||||
|
contentService = inject(ContentService);
|
||||||
|
|
||||||
|
// Expanded comments indices map
|
||||||
|
expandedComments = signal<{ [key: number]: boolean }>({});
|
||||||
|
newComments: { [key: number]: string } = {};
|
||||||
|
|
||||||
|
// Share modal state
|
||||||
|
activeShareModal = signal<Article | null>(null);
|
||||||
|
shareLinkedIn = true;
|
||||||
|
shareX = false;
|
||||||
|
shareIntranet = true;
|
||||||
|
|
||||||
|
toggleComments(id: number): void {
|
||||||
|
this.expandedComments.update(prev => ({
|
||||||
|
...prev,
|
||||||
|
[id]: !prev[id]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
commentsExpanded(id: number): boolean {
|
||||||
|
return !!this.expandedComments()[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
submitComment(id: number): void {
|
||||||
|
const com = this.newComments[id]?.trim();
|
||||||
|
if (com) {
|
||||||
|
this.contentService.addComment(id, com);
|
||||||
|
this.newComments[id] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openShareModal(art: Article): void {
|
||||||
|
this.activeShareModal.set(art);
|
||||||
|
// Reset check state
|
||||||
|
this.shareLinkedIn = art.shareTargets.includes('LinkedIn');
|
||||||
|
this.shareX = art.shareTargets.includes('X');
|
||||||
|
this.shareIntranet = art.shareTargets.includes('Intranet');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeShareModal(): void {
|
||||||
|
this.activeShareModal.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmShare(): void {
|
||||||
|
const art = this.activeShareModal();
|
||||||
|
if (art) {
|
||||||
|
this.contentService.shareArticle(art.id);
|
||||||
|
|
||||||
|
// Update targets list dynamically
|
||||||
|
const newTargets: string[] = [];
|
||||||
|
if (this.shareLinkedIn) newTargets.push('LinkedIn');
|
||||||
|
if (this.shareX) newTargets.push('X');
|
||||||
|
if (this.shareIntranet) newTargets.push('Intranet');
|
||||||
|
|
||||||
|
art.shareTargets = newTargets;
|
||||||
|
|
||||||
|
this.closeShareModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
354
frontend/src/app/features/workflows/workflows.component.ts
Normal file
354
frontend/src/app/features/workflows/workflows.component.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ContentService, Article } from '../../core/services/content.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-workflows',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
template: `
|
||||||
|
<div class="relative bg-gradient-to-br from-[#06120F] via-[#0A2520] to-[#0D1B2A] text-white p-6 lg:p-8 rounded-[32px] border border-white/10 shadow-2xl min-h-[85vh] flex flex-col gap-8 animate-fadeIn overflow-hidden">
|
||||||
|
<!-- Ambient background glows -->
|
||||||
|
<div class="absolute -top-40 -right-40 w-96 h-96 bg-emerald-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
<div class="absolute -bottom-40 -left-40 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.01)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.01)_1px,transparent_1px)] bg-[size:32px_32px] pointer-events-none opacity-20"></div>
|
||||||
|
|
||||||
|
<!-- Module Header -->
|
||||||
|
<header class="flex justify-between items-center flex-wrap gap-4 border-b border-white/10 pb-6 relative z-10">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-black text-white uppercase tracking-wider flex items-center gap-3">
|
||||||
|
<svg class="w-8 h-8 text-emerald-400 filter drop-shadow-[0_0_8px_rgba(52,211,153,0.5)]" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="10" cy="12" r="5" stroke="currentColor" stroke-width="2.5"/>
|
||||||
|
<circle cx="38" cy="12" r="5" stroke="currentColor" stroke-width="2.5"/>
|
||||||
|
<circle cx="24" cy="36" r="5" stroke="currentColor" stroke-width="2.5"/>
|
||||||
|
<path d="M15 12H33" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M10 17V28c0 2.5 2 4.5 4.5 4.5H19" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M38 17V28c0 2.5-2 4.5-4.5 4.5H29" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
Workflows de Aprobación
|
||||||
|
</h1>
|
||||||
|
<p class="text-xs text-emerald-300/60 font-medium tracking-wide mt-1">Motor de reglas multinivel para aprobación y publicación de contenidos</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 text-xs">
|
||||||
|
<span class="bg-emerald-500/10 border border-emerald-500/30 px-4 py-2 rounded-full text-emerald-400 font-bold flex items-center gap-2 shadow-[0_0_15px_rgba(16,185,129,0.1)]">
|
||||||
|
<span class="w-2 h-2 bg-emerald-400 rounded-full animate-ping"></span>
|
||||||
|
Motor AD: Conectado
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Grid Columns -->
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-8 relative z-10 flex-grow">
|
||||||
|
|
||||||
|
<!-- Left column: Add New Proposal Form & Engine Info -->
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<h2 class="text-xs uppercase font-black tracking-widest text-emerald-400 flex items-center gap-2">
|
||||||
|
<span>➕</span> Agregar Nueva Propuesta
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="bg-black/30 backdrop-blur-md border border-white/10 rounded-3xl p-6 flex flex-col gap-5 shadow-xl">
|
||||||
|
<!-- Title Input -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-[10px] uppercase font-bold text-slate-400 tracking-wider">Título del Artículo</label>
|
||||||
|
<input type="text" [(ngModel)]="newTitle" placeholder="Ej. Resultados del Segundo Trimestre"
|
||||||
|
class="px-4 py-3 rounded-xl border border-white/10 outline-none text-xs w-full focus:border-emerald-500/50 bg-white/5 text-white placeholder-white/30 focus:shadow-[0_0_10px_rgba(52,211,153,0.15)] transition" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Select -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-[10px] uppercase font-bold text-slate-400 tracking-wider">Categoría</label>
|
||||||
|
<div class="relative">
|
||||||
|
<select [(ngModel)]="newCategory"
|
||||||
|
class="px-4 py-3 rounded-xl border border-white/10 outline-none text-xs w-full focus:border-emerald-500/50 bg-[#0B1512] text-white appearance-none cursor-pointer">
|
||||||
|
<option value="Tecnología">Tecnología & Innovación</option>
|
||||||
|
<option value="Recursos Humanos">Recursos Humanos</option>
|
||||||
|
<option value="Operaciones">Operaciones</option>
|
||||||
|
<option value="Seguridad">Seguridad & Redes</option>
|
||||||
|
</select>
|
||||||
|
<div class="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400 text-[10px]">▼</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body Textarea -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-[10px] uppercase font-bold text-slate-400 tracking-wider">Cuerpo del Comunicado</label>
|
||||||
|
<textarea [(ngModel)]="newBody" rows="4" placeholder="Escribe el contenido del mensaje corporativo aquí..."
|
||||||
|
class="px-4 py-3 rounded-xl border border-white/10 outline-none text-xs w-full focus:border-emerald-500/50 bg-white/5 text-white placeholder-white/30 focus:shadow-[0_0_10px_rgba(52,211,153,0.15)] resize-none transition"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Target Channels Checklist -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-[10px] uppercase font-bold text-slate-400 tracking-wider">Canales de Publicación Objetivo</label>
|
||||||
|
<div class="flex flex-col gap-3 mt-1">
|
||||||
|
<label class="flex items-center gap-3 text-xs text-slate-300 font-semibold cursor-pointer group">
|
||||||
|
<input type="checkbox" [(ngModel)]="targetIntranet" class="sr-only peer" />
|
||||||
|
<div class="w-5 h-5 rounded border border-white/15 bg-white/5 peer-checked:bg-emerald-500 peer-checked:border-emerald-400 flex items-center justify-center transition peer-checked:shadow-[0_0_8px_rgba(16,185,129,0.3)]">
|
||||||
|
<svg class="w-3.5 h-3.5 text-white fill-current opacity-0 peer-checked:opacity-100 transition" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="group-hover:text-white transition">Intranet Corporativa</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-3 text-xs text-slate-300 font-semibold cursor-pointer group">
|
||||||
|
<input type="checkbox" [(ngModel)]="targetLinkedIn" class="sr-only peer" />
|
||||||
|
<div class="w-5 h-5 rounded border border-white/15 bg-white/5 peer-checked:bg-emerald-500 peer-checked:border-emerald-400 flex items-center justify-center transition peer-checked:shadow-[0_0_8px_rgba(16,185,129,0.3)]">
|
||||||
|
<svg class="w-3.5 h-3.5 text-white fill-current opacity-0 peer-checked:opacity-100 transition" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="group-hover:text-white transition">LinkedIn de la Empresa</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-3 text-xs text-slate-300 font-semibold cursor-pointer group">
|
||||||
|
<input type="checkbox" [(ngModel)]="targetX" class="sr-only peer" />
|
||||||
|
<div class="w-5 h-5 rounded border border-white/15 bg-white/5 peer-checked:bg-emerald-500 peer-checked:border-emerald-400 flex items-center justify-center transition peer-checked:shadow-[0_0_8px_rgba(16,185,129,0.3)]">
|
||||||
|
<svg class="w-3.5 h-3.5 text-white fill-current opacity-0 peer-checked:opacity-100 transition" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span class="group-hover:text-white transition">Red Social X (Twitter)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button (click)="submitProposal()" class="w-full py-3 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-400 hover:to-teal-500 text-white rounded-xl text-xs font-bold uppercase tracking-widest shadow-[0_4px_15px_rgba(16,185,129,0.25)] hover:shadow-[0_4px_20px_rgba(16,185,129,0.4)] transition-all duration-300 transform hover:-translate-y-0.5 mt-2 active:translate-y-0">
|
||||||
|
Enviar a Flujo de Aprobación
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (formError()) {
|
||||||
|
<div class="text-[11px] text-red-400 font-bold text-center bg-red-950/30 border border-red-500/20 py-2.5 rounded-xl animate-fadeIn">{{ formError() }}</div>
|
||||||
|
}
|
||||||
|
@if (formSuccess()) {
|
||||||
|
<div class="text-[11px] text-emerald-400 font-bold text-center bg-emerald-950/30 border border-emerald-500/20 py-2.5 rounded-xl animate-fadeIn">¡Propuesta enviada con éxito al nivel de revisión 1!</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AD Rules Engine Widget (Viridian/Cyberpunk style) -->
|
||||||
|
<div class="bg-black/30 backdrop-blur-md border border-white/10 rounded-3xl p-6 flex flex-col gap-4 shadow-xl relative overflow-hidden group hover:border-emerald-500/20 transition-all duration-300">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="text-xs font-bold uppercase tracking-widest text-emerald-400">Visualización del Motor AD</h3>
|
||||||
|
<span class="text-[9px] font-mono text-emerald-400 bg-emerald-500/15 border border-emerald-500/30 px-2 py-0.5 rounded-full animate-pulse">Servicio Activo</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-[11px] text-slate-400 leading-relaxed font-light">Visualización técnica del flujo de validación del Active Directory para jerarquías organizacionales.</p>
|
||||||
|
|
||||||
|
<!-- Gears Animation -->
|
||||||
|
<div class="flex items-center justify-center py-4 bg-white/[0.02] border border-white/[0.03] rounded-2xl">
|
||||||
|
<svg class="w-24 h-24 text-emerald-400/80 filter drop-shadow-[0_0_6px_rgba(52,211,153,0.3)]" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="50" cy="50" r="30" stroke="currentColor" stroke-width="2" stroke-dasharray="10, 5" class="animate-spin-slow" />
|
||||||
|
<circle cx="50" cy="50" r="20" stroke="currentColor" stroke-width="1.5" stroke-dasharray="5, 3" class="animate-spin-reverse" />
|
||||||
|
<g transform="translate(50, 50)">
|
||||||
|
<rect x="-3" y="-24" width="6" height="48" fill="currentColor" opacity="0.25" class="animate-spin-medium" />
|
||||||
|
<rect x="-24" y="-3" width="48" height="6" fill="currentColor" opacity="0.25" class="animate-spin-medium" />
|
||||||
|
</g>
|
||||||
|
<text x="50" y="53" text-anchor="middle" font-size="8" fill="#fff" font-weight="bold" class="font-mono tracking-wider opacity-90">AD ENGINE</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3 text-center text-[10px]">
|
||||||
|
<div class="bg-white/5 p-2 rounded-xl border border-white/5 flex flex-col gap-0.5">
|
||||||
|
<span class="text-slate-400 font-bold uppercase tracking-wider">Servidor AD</span>
|
||||||
|
<span class="text-emerald-400 font-bold font-mono">LDAP SECURE</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/5 p-2 rounded-xl border border-white/5 flex flex-col gap-0.5">
|
||||||
|
<span class="text-slate-400 font-bold uppercase tracking-wider">Latencia</span>
|
||||||
|
<span class="text-emerald-400 font-bold font-mono">1.2ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle & Right column: Approval Queue & Published list -->
|
||||||
|
<div class="xl:col-span-2 flex flex-col gap-6">
|
||||||
|
<div class="flex justify-between items-center flex-wrap gap-2">
|
||||||
|
<h2 class="text-xs uppercase font-black tracking-widest text-emerald-400 flex items-center gap-2">
|
||||||
|
<span>📋</span> Cola de Aprobaciones Pendientes
|
||||||
|
</h2>
|
||||||
|
<span class="text-xs font-bold text-slate-400 font-mono bg-white/5 border border-white/10 px-3 py-1 rounded-full">{{ contentService.pending().length }} solicitudes activas</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending List -->
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
@for (art of contentService.pending(); track art.id) {
|
||||||
|
<div class="bg-black/30 backdrop-blur-md border border-white/10 rounded-3xl p-6 shadow-xl flex flex-col gap-4 relative overflow-hidden group hover:border-white/15 transition-all duration-300">
|
||||||
|
|
||||||
|
<!-- Accent glow bar based on approval level -->
|
||||||
|
<div class="absolute left-0 top-0 bottom-0 w-2.5"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-gradient-to-b from-amber-400 to-amber-500 shadow-[2px_0_12px_rgba(245,158,11,0.4)]': art.approvalLevel === 1,
|
||||||
|
'bg-gradient-to-b from-purple-500 to-purple-600 shadow-[2px_0_12px_rgba(147,51,234,0.4)]': art.approvalLevel === 2
|
||||||
|
}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Header -->
|
||||||
|
<div class="flex justify-between items-start gap-4 pl-3">
|
||||||
|
<div>
|
||||||
|
<span class="text-[10px] uppercase font-bold text-slate-400 tracking-wider flex items-center gap-2">
|
||||||
|
<span>👤</span> Autor: <strong class="text-slate-300 font-semibold">{{ art.author }}</strong> · {{ art.date }}
|
||||||
|
</span>
|
||||||
|
<h3 class="text-lg font-black text-white leading-tight mt-1.5">{{ art.title }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Approval Level Badge -->
|
||||||
|
<span class="px-3 py-1.5 rounded-full text-[9px] font-bold uppercase tracking-widest text-white shadow-sm"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-amber-500/15 border border-amber-500/30 text-amber-300': art.approvalLevel === 1,
|
||||||
|
'bg-purple-600/15 border border-purple-600/30 text-purple-300': art.approvalLevel === 2
|
||||||
|
}">
|
||||||
|
{{ art.approvalLevel === 1 ? 'Nivel 1: Revisión Técnica' : 'Nivel 2: Autorización RR.HH.' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Body -->
|
||||||
|
<p class="text-xs text-slate-300 leading-relaxed font-light pl-3 bg-white/[0.02] border border-white/[0.03] p-3.5 rounded-2xl">{{ art.body }}</p>
|
||||||
|
|
||||||
|
<!-- Share Targets -->
|
||||||
|
<div class="flex items-center gap-2 flex-wrap text-[10px] text-slate-400 pl-3">
|
||||||
|
<span class="font-bold uppercase tracking-wider text-[9px]">Destinos Objetivo:</span>
|
||||||
|
@for (t of art.shareTargets; track t) {
|
||||||
|
<span class="bg-white/5 border border-white/10 px-2.5 py-1 rounded text-slate-300 font-mono font-bold">{{ t }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="flex gap-3 justify-end mt-2 border-t border-white/10 pt-4 pl-3">
|
||||||
|
<button (click)="contentService.rejectArticle(art.id)"
|
||||||
|
class="px-5 py-2.5 border border-white/10 hover:border-red-500/40 rounded-xl text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-red-400 hover:bg-red-500/5 transition duration-200">
|
||||||
|
Rechazar Propuesta
|
||||||
|
</button>
|
||||||
|
<button (click)="approveItem(art.id)"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 shadow-[0_4px_12px_rgba(245,158,11,0.25)] hover:shadow-[0_4px_16px_rgba(245,158,11,0.4)]': art.approvalLevel === 1,
|
||||||
|
'bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-400 hover:to-purple-500 shadow-[0_4px_12px_rgba(147,51,234,0.25)] hover:shadow-[0_4px_16px_rgba(147,51,234,0.4)]': art.approvalLevel === 2
|
||||||
|
}"
|
||||||
|
class="px-5 py-2.5 text-white rounded-xl text-[10px] font-bold uppercase tracking-wider transition-all duration-300 transform hover:-translate-y-0.5 active:translate-y-0">
|
||||||
|
{{ art.approvalLevel === 1 ? 'Avanzar a Nivel 2 ➔' : 'Aprobar y Publicar ✓' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (contentService.pending().length === 0) {
|
||||||
|
<div class="bg-white/5 border border-white/10 border-dashed rounded-3xl p-12 text-center text-slate-400 flex flex-col items-center justify-center gap-4 animate-fadeIn">
|
||||||
|
<div class="w-16 h-16 rounded-full bg-emerald-500/10 flex items-center justify-center text-3xl text-emerald-400 border border-emerald-500/20 shadow-[0_0_15px_rgba(16,185,129,0.1)]">
|
||||||
|
🎉
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-black text-white uppercase tracking-wider">¡Cola de Solicitudes al Día!</p>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">No hay propuestas pendientes para revisar en el AD Engine en este momento.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recently Published Feed Shortcut -->
|
||||||
|
<div class="mt-8 flex flex-col gap-6">
|
||||||
|
<h2 class="text-xs uppercase font-black tracking-widest text-emerald-400 flex items-center gap-2">
|
||||||
|
<span>📰</span> Artículos Publicados Recientemente
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
@for (art of contentService.articles().slice(0, 2); track art.id) {
|
||||||
|
<div class="bg-black/20 backdrop-blur-md border border-white/10 rounded-3xl p-6 shadow-xl flex flex-col gap-3 hover:border-emerald-500/20 transition-all duration-300">
|
||||||
|
<div class="flex justify-between items-center text-[10px] font-bold uppercase tracking-widest text-emerald-400">
|
||||||
|
<span>{{ art.category }}</span>
|
||||||
|
<span class="text-emerald-500 flex items-center gap-1.5"><span class="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse"></span> Publicado</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-base font-black text-white leading-tight">{{ art.title }}</h4>
|
||||||
|
<p class="text-xs text-slate-300 line-clamp-2 leading-relaxed font-light bg-white/[0.01] p-2.5 rounded-xl border border-white/[0.02]">{{ art.body }}</p>
|
||||||
|
|
||||||
|
<hr class="border-white/10 my-1" />
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button (click)="router.navigate(['/info-hub'])"
|
||||||
|
class="text-emerald-400 hover:text-emerald-300 font-bold uppercase tracking-wider text-[10px] flex items-center gap-1.5 transition">
|
||||||
|
Ver en Info Hub ➔
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
.animate-fadeIn {
|
||||||
|
animation: fadeIn 0.35s ease-out forwards;
|
||||||
|
}
|
||||||
|
.animate-spin-slow {
|
||||||
|
animation: spin 16s linear infinite;
|
||||||
|
}
|
||||||
|
.animate-spin-medium {
|
||||||
|
animation: spin 10s linear infinite;
|
||||||
|
}
|
||||||
|
.animate-spin-reverse {
|
||||||
|
animation: spin-back 8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes spin-back {
|
||||||
|
from { transform: rotate(360deg); }
|
||||||
|
to { transform: rotate(0deg); }
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class WorkflowsComponent {
|
||||||
|
contentService = inject(ContentService);
|
||||||
|
router = inject(Router);
|
||||||
|
|
||||||
|
// Form states
|
||||||
|
newTitle = '';
|
||||||
|
newCategory = 'Tecnología';
|
||||||
|
newBody = '';
|
||||||
|
targetIntranet = true;
|
||||||
|
targetLinkedIn = false;
|
||||||
|
targetX = false;
|
||||||
|
|
||||||
|
formError = signal<string>('');
|
||||||
|
formSuccess = signal<boolean>(false);
|
||||||
|
|
||||||
|
submitProposal(): void {
|
||||||
|
const t = this.newTitle.trim();
|
||||||
|
const b = this.newBody.trim();
|
||||||
|
if (!t || !b) {
|
||||||
|
this.formError.set('Por favor, completa el título y el cuerpo del comunicado.');
|
||||||
|
setTimeout(() => this.formError.set(''), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets: string[] = [];
|
||||||
|
if (this.targetIntranet) targets.push('Intranet');
|
||||||
|
if (this.targetLinkedIn) targets.push('LinkedIn');
|
||||||
|
if (this.targetX) targets.push('X');
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
this.formError.set('Debes seleccionar al menos un canal de publicación.');
|
||||||
|
setTimeout(() => this.formError.set(''), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contentService.addPending(t, this.newCategory, b, 'Jess Miller', targets);
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
this.newTitle = '';
|
||||||
|
this.newBody = '';
|
||||||
|
this.targetIntranet = true;
|
||||||
|
this.targetLinkedIn = false;
|
||||||
|
this.targetX = false;
|
||||||
|
|
||||||
|
this.formSuccess.set(true);
|
||||||
|
setTimeout(() => this.formSuccess.set(false), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
approveItem(id: number): void {
|
||||||
|
this.contentService.advanceApproval(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user