feat: implement Kinetix reservation module with Angular 18, AI-assisted form filling, and executive design system
This commit is contained in:
parent
a83a901bf9
commit
38c623722a
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,3 +7,6 @@ dist
|
|||||||
/frontend/.angular/cache
|
/frontend/.angular/cache
|
||||||
/.vscode
|
/.vscode
|
||||||
|
|
||||||
|
|
||||||
|
# Engram AI agent memory
|
||||||
|
.engram/
|
||||||
|
|||||||
1495
doc/presentacion_kinetix.html
Normal file
1495
doc/presentacion_kinetix.html
Normal file
File diff suppressed because it is too large
Load Diff
329
doc/propuesta_kinetix.md
Normal file
329
doc/propuesta_kinetix.md
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
# Propuesta Técnica y Comercial
|
||||||
|
## Plataforma de Intranet Corporativa · **Kinetix**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **Preparado por:** Jess Miller · Arquitecta de Software Senior
|
||||||
|
> **Fecha:** Junio 2026
|
||||||
|
> **Versión:** 1.0 — Confidencial
|
||||||
|
> **Para:** [Nombre de la Empresa Objetivo]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resumen Ejecutivo
|
||||||
|
|
||||||
|
**Kinetix** es una plataforma de intranet corporativa de nueva generación, diseñada para consolidar en un único ecosistema digital todos los procesos internos de una organización empresarial moderna: gestión de identidades, reserva de espacios, aprobación de contenidos, directorio de personal y ticketing de incidencias.
|
||||||
|
|
||||||
|
La plataforma está operativa, con frontend y backend en producción local, y lista para su despliegue en infraestructura cloud o on-premise. Está construida sobre un stack tecnológico de primer nivel —**Angular 18, NestJS 10 y PostgreSQL 15**— con autenticación nativa vía **Microsoft Azure Entra ID (Active Directory)**, lo que garantiza integración inmediata con el ecosistema Microsoft de cualquier empresa.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Kinetix no es un prototipo. Es un producto en desarrollo activo con arquitectura escalable, código limpio y documentación técnica estructurada. Esta propuesta presenta su adopción como solución central de intranet para [Empresa].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problema que Resuelve
|
||||||
|
|
||||||
|
Las empresas modernas operan con herramientas fragmentadas: un sistema para reservas, otro para el directorio, formularios manuales para workflows de aprobación y correos internos para incidencias. Este modelo genera:
|
||||||
|
|
||||||
|
| Problema | Impacto |
|
||||||
|
|---|---|
|
||||||
|
| Fragmentación de herramientas | Pérdida de tiempo y errores de coordinación |
|
||||||
|
| Sin integración con Active Directory | Onboarding lento, acceso manual, riesgos de seguridad |
|
||||||
|
| Reservas de salas sin control centralizado | Conflictos de disponibilidad, overbooking |
|
||||||
|
| Aprobaciones por correo | Sin trazabilidad, cuellos de botella operativos |
|
||||||
|
| Directorio desactualizado | Información incorrecta, baja colaboración |
|
||||||
|
|
||||||
|
**Kinetix resuelve todos estos problemas en una sola plataforma integrada.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. La Solución: Plataforma Kinetix
|
||||||
|
|
||||||
|
### 2.1 Visión General
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ KINETIX INTRANET │
|
||||||
|
│ "Un ecosistema. Todos los procesos internos en uno." │
|
||||||
|
├──────────────┬──────────────┬──────────────┬────────────────┤
|
||||||
|
│ Directorio │ Espacios │ Workflows │ Info Hub CMS │
|
||||||
|
│ Corporativo │ & Reservas │ de Aprobación│ + Ticketing │
|
||||||
|
│ (Entra ID) │ (Calendar) │ (BPM Multi- │ (SLA/Tickets) │
|
||||||
|
│ │ + IA Parser │ nivel) │ │
|
||||||
|
└──────────────┴──────────────┴──────────────┴────────────────┘
|
||||||
|
↑ ↑ ↑
|
||||||
|
SSO / JWT PostgreSQL DB Azure Entra ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Módulos Implementados
|
||||||
|
|
||||||
|
#### 🏢 Módulo 1 — Directorio Corporativo (IAM/HR)
|
||||||
|
Sincronizado con **Microsoft Entra ID (Azure AD)**. Muestra el organigrama real de la empresa con perfiles dinámicos: nombre, rol, departamento, skills, métricas y contacto directo.
|
||||||
|
|
||||||
|
- Búsqueda en tiempo real por nombre, rol o habilidad
|
||||||
|
- Filtrado por departamento
|
||||||
|
- ID de Active Directory (AD-XXXX) en cada perfil
|
||||||
|
- Badge de calificación y tarifa por hora
|
||||||
|
- Diseño de tarjetas con micro-animaciones y hover
|
||||||
|
|
||||||
|
#### 📅 Módulo 2 — Autogestión de Espacios y Activos (PMS)
|
||||||
|
Sistema completo de reservas con **calendario dinámico** con vistas Mes/Semana/Día.
|
||||||
|
|
||||||
|
- Clic en cualquier slot del calendario → redirige a formulario de reserva
|
||||||
|
- Filtros en tiempo real por tipo de recurso (salas, equipos, vehículos)
|
||||||
|
- **Asistente IA de lenguaje natural**: el usuario escribe "Reservar sala para el jueves 5 personas" y el sistema auto-completa el formulario usando un parser de IA
|
||||||
|
- Confirmación y notificación a calendario de Active Directory
|
||||||
|
|
||||||
|
#### ⚙️ Módulo 3 — Workflow de Aprobación y Publicación (BPM)
|
||||||
|
Motor de reglas multinivel integrado con SSO y Active Directory para jerarquías de aprobación de contenidos institucionales.
|
||||||
|
|
||||||
|
- Creación → Validación → Publicación automática
|
||||||
|
- Estados de aprobación trazables
|
||||||
|
- Publicación en canales de comunicación interna
|
||||||
|
|
||||||
|
#### 📰 Módulo 4 — Info Hub / CMS Corporativo
|
||||||
|
Gestión de contenidos corporativos: artículos, comunicados y publicaciones institucionales con editor integrado.
|
||||||
|
|
||||||
|
#### 🎫 Módulo 5 — Ticketing e Incidencias
|
||||||
|
Creación, flujo y asignación de tickets internos con gestión de SLAs y prioridades de soporte técnico y operativo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Stack Tecnológico
|
||||||
|
|
||||||
|
| Capa | Tecnología | Versión | Justificación |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Frontend** | Angular | 18.0 | Framework enterprise estándar de Google. Signals natively reactive. |
|
||||||
|
| **Backend** | NestJS | 10.0 | Framework TypeScript para APIs REST escalables y modulares |
|
||||||
|
| **Autenticación** | Azure Entra ID + JWT | — | SSO corporativo con Passport.js. Sin gestión de contraseñas propias |
|
||||||
|
| **Base de Datos** | PostgreSQL | 15 | Transaccional, ACID-compliant. Crítico para reservas sin overbooking |
|
||||||
|
| **ORM** | Prisma | — | Migraciones tipeadas, esquema declarativo |
|
||||||
|
| **Contenedores** | Docker + Docker Compose | — | Portabilidad total entre entornos |
|
||||||
|
| **Estilos** | TailwindCSS + Vanilla CSS | — | Sistema de diseño Executive Suite personalizado |
|
||||||
|
| **IA Parser** | API de IA (NLP) | — | Procesamiento de lenguaje natural para reservas conversacionales |
|
||||||
|
|
||||||
|
### 3.1 Arquitectura de Seguridad SSO
|
||||||
|
|
||||||
|
```
|
||||||
|
Usuario → App Angular → Azure Entra ID (login)
|
||||||
|
↓
|
||||||
|
Token JWT (con roles AD)
|
||||||
|
↓
|
||||||
|
Backend NestJS valida token
|
||||||
|
con passport-azure-ad / passport-jwt
|
||||||
|
↓
|
||||||
|
Acceso según rol: employee | manager | admin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ninguna contraseña se almacena en Kinetix.** La autenticación es 100% delegada a Active Directory de la empresa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Arquitectura del Sistema
|
||||||
|
|
||||||
|
```
|
||||||
|
kinetix/
|
||||||
|
├── frontend/ ← SPA Angular 18 (Standalone Components + Signals)
|
||||||
|
│ └── src/app/
|
||||||
|
│ ├── core/ ← Guards SSO, interceptores HTTP, auth services
|
||||||
|
│ ├── shared/ ← Componentes reutilizables del Design System
|
||||||
|
│ └── features/ ← Módulos por dominio (reservas, directorio, etc.)
|
||||||
|
│
|
||||||
|
├── backend/ ← API REST NestJS (TypeScript modular)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── auth/ ← Módulo SSO Active Directory / JWT
|
||||||
|
│ ├── cms/ ← Gestión de contenidos
|
||||||
|
│ ├── directory/ ← Directorio corporativo
|
||||||
|
│ ├── workflow/ ← Motor de aprobaciones
|
||||||
|
│ ├── spaces/ ← Reservas de espacios (con anti-overbooking)
|
||||||
|
│ └── ticketing/ ← Sistema de incidencias
|
||||||
|
│
|
||||||
|
├── standards/ ← Documentación técnica y estándares del proyecto
|
||||||
|
├── docker-compose.yml ← Infraestructura local: PostgreSQL + pgAdmin
|
||||||
|
└── setup.ps1 / .sh ← Scripts de onboarding para nuevos devs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Diseño e Interfaz de Usuario
|
||||||
|
|
||||||
|
Kinetix utiliza el **Executive Suite Design System**, un sistema de diseño corporativo premium desarrollado a medida:
|
||||||
|
|
||||||
|
- **Paleta:** Ivory `#F5F4F1` + Negro sofisticado `#0F0D0A` + Dorado bronce `#C9973C`
|
||||||
|
- **Tarjetas Joya (Blackjack System):** Fondos oscuros en Navy, Plum, Viridian, Espresso para los módulos principales
|
||||||
|
- **Tipografía:** Inter (titulares) + Outfit (cuerpo)
|
||||||
|
- **Glassmorphism y micro-animaciones** para interactividad premium
|
||||||
|
- **Responsive design** completo: mobile, tablet, desktop
|
||||||
|
- **Sidebar colapsable** con estado de sistema en tiempo real
|
||||||
|
|
||||||
|
> El diseño fue validado contra estándares documentados en `standards/04-ui-ux-neon.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Estado Actual del Proyecto
|
||||||
|
|
||||||
|
| Módulo | Estado | Detalle |
|
||||||
|
|---|---|---|
|
||||||
|
| Autenticación SSO | ✅ Implementado | JWT + Guards + Interceptores HTTP |
|
||||||
|
| Dashboard principal | ✅ Implementado | KPIs, cards de módulos, navegación |
|
||||||
|
| Directorio de Personal | ✅ Implementado | Búsqueda reactiva, filtros, tarjetas AD |
|
||||||
|
| Calendario de Reservas | ✅ Implementado | Vistas Mes/Semana/Día, filtros dinámicos |
|
||||||
|
| Formulario de Reserva + IA | ✅ Implementado | Parser NLP con mock local y API IA |
|
||||||
|
| Workflow de Aprobación | 🔄 En desarrollo | Motor de reglas, vistas de simulación |
|
||||||
|
| Info Hub / CMS | 📋 Planificado | Editor WYSIWYG, taxonomías |
|
||||||
|
| Ticketing | 📋 Planificado | SLA, asignación, prioridades |
|
||||||
|
| Base de datos PostgreSQL | ✅ Configurado | Docker Compose, schema documentado |
|
||||||
|
| Despliegue cloud | 📋 Pendiente | Azure / AWS / on-premise |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Plan de Acción (Roadmap de Entrega)
|
||||||
|
|
||||||
|
### Fase 0 — Diagnóstico e Integración *(Semanas 1–2)*
|
||||||
|
|
||||||
|
| Actividad | Responsable | Entregable |
|
||||||
|
|---|---|---|
|
||||||
|
| Reunión de kickoff con equipo técnico | Arquitecta + IT Cliente | Acta de requerimientos |
|
||||||
|
| Auditoría del Active Directory existente | Arquitecta + Admin AD | Mapeo de grupos, roles y UOs |
|
||||||
|
| Definición de ambientes (dev / staging / prod) | DevOps | Documento de infraestructura |
|
||||||
|
| Firma de NDA y contrato de desarrollo | Legal | Contrato ejecutado |
|
||||||
|
|
||||||
|
### Fase 1 — Setup e Integración SSO *(Semanas 3–4)*
|
||||||
|
|
||||||
|
| Actividad | Entregable |
|
||||||
|
|---|---|
|
||||||
|
| Configurar Azure Entra ID con la tenant del cliente | App registrada en Azure Portal |
|
||||||
|
| Integrar OAuth2 + JWT con roles reales de AD | Auth funcional en entorno del cliente |
|
||||||
|
| Onboarding técnico al equipo de IT interno | Workshop de 4 horas |
|
||||||
|
| Setup de base de datos PostgreSQL en producción | DB productiva con migraciones aplicadas |
|
||||||
|
|
||||||
|
**Hito:** ✅ Login con credenciales reales de la empresa funcionando.
|
||||||
|
|
||||||
|
### Fase 2 — Módulos Core *(Semanas 5–10)*
|
||||||
|
|
||||||
|
| Semana | Módulo | Entregable |
|
||||||
|
|---|---|---|
|
||||||
|
| 5–6 | Directorio Corporativo | Perfiles sincronizados con AD real |
|
||||||
|
| 7–8 | Gestión de Espacios + IA | Calendario + reservas en producción |
|
||||||
|
| 9–10 | Workflow de Aprobación | Motor de reglas con notificaciones |
|
||||||
|
|
||||||
|
**Hito:** ✅ Piloto con 50 usuarios internos en entorno staging.
|
||||||
|
|
||||||
|
### Fase 3 — Módulos Secundarios *(Semanas 11–14)*
|
||||||
|
|
||||||
|
| Semana | Módulo | Entregable |
|
||||||
|
|---|---|---|
|
||||||
|
| 11–12 | Info Hub / CMS | Editor de artículos operativo |
|
||||||
|
| 13–14 | Ticketing e Incidencias | Sistema de tickets con SLA |
|
||||||
|
|
||||||
|
**Hito:** ✅ Plataforma completa en staging. UAT (User Acceptance Testing).
|
||||||
|
|
||||||
|
### Fase 4 — Lanzamiento *(Semanas 15–16)*
|
||||||
|
|
||||||
|
| Actividad | Entregable |
|
||||||
|
|---|---|
|
||||||
|
| Despliegue en producción cloud/on-premise | Plataforma 100% en vivo |
|
||||||
|
| Capacitación a usuarios finales (por roles) | Manual de usuario + sesiones |
|
||||||
|
| Documentación técnica de operación | Runbook de DevOps |
|
||||||
|
| Soporte post-lanzamiento (30 días incluidos) | Canal de soporte dedicado |
|
||||||
|
|
||||||
|
**Hito:** 🚀 Go-Live oficial de Kinetix para [Empresa].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Diagrama de Fases
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gantt
|
||||||
|
title Roadmap de Implementación Kinetix
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
section Fase 0 — Diagnóstico
|
||||||
|
Kickoff & Auditoría AD :2026-07-01, 7d
|
||||||
|
Definición de Ambientes :2026-07-07, 7d
|
||||||
|
section Fase 1 — SSO & Setup
|
||||||
|
Azure Entra ID Integration :2026-07-14, 7d
|
||||||
|
PostgreSQL Producción :2026-07-21, 7d
|
||||||
|
section Fase 2 — Core
|
||||||
|
Directorio Corporativo :2026-07-28, 14d
|
||||||
|
Gestión de Espacios + IA :2026-08-11, 14d
|
||||||
|
Workflow de Aprobación :2026-08-25, 14d
|
||||||
|
section Fase 3 — Secundarios
|
||||||
|
Info Hub / CMS :2026-09-08, 14d
|
||||||
|
Ticketing & Incidencias :2026-09-22, 14d
|
||||||
|
section Fase 4 — Lanzamiento
|
||||||
|
Despliegue Producción :2026-10-06, 7d
|
||||||
|
Capacitación & Go-Live :2026-10-13, 7d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Propuesta Económica
|
||||||
|
|
||||||
|
### Modelo de Precios Sugerido
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Los siguientes valores son referenciales y ajustables según el tamaño de la organización, cantidad de usuarios y requerimientos adicionales de personalización.
|
||||||
|
|
||||||
|
| Concepto | Descripción | Estimado |
|
||||||
|
|---|---|---|
|
||||||
|
| **Licencia de Plataforma** | Uso de la plataforma Kinetix base | A negociar |
|
||||||
|
| **Implementación e Integración** | Fases 0–1: Setup, SSO, infraestructura | 4–6 semanas |
|
||||||
|
| **Desarrollo de Módulos Core** | Fase 2: Directorio, Espacios, Workflow | 6 semanas |
|
||||||
|
| **Módulos Secundarios** | Fase 3: CMS, Ticketing | 4 semanas |
|
||||||
|
| **Soporte Post-Lanzamiento** | 30 días incluidos + contratos de mantenimiento opcionales | Incluido |
|
||||||
|
|
||||||
|
#### Modalidades de Contratación
|
||||||
|
|
||||||
|
| Modalidad | Descripción | Recomendado para |
|
||||||
|
|---|---|---|
|
||||||
|
| **Llave en mano** | Precio fijo por entrega completa | Empresas con presupuesto definido |
|
||||||
|
| **Tiempo y materiales** | Pago por hora/sprint según avance | Proyectos con alcance variable |
|
||||||
|
| **SaaS Mensual** | Hosting + mantenimiento + actualizaciones | Empresas que prefieren OpEx |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Por Qué Kinetix sobre Alternativas
|
||||||
|
|
||||||
|
| Criterio | Kinetix | SharePoint | Confluence | Desarrollo a medida desde cero |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Integración AD nativa | ✅ | ✅ | ⚠️ Plugin | ✅ |
|
||||||
|
| Personalizable 100% | ✅ | ⚠️ Limitado | ⚠️ Limitado | ✅ |
|
||||||
|
| Stack moderno (Angular + NestJS) | ✅ | ❌ | ❌ | ✅ |
|
||||||
|
| Asistente IA integrado | ✅ | ❌ | ❌ | Solo si se desarrolla |
|
||||||
|
| Tiempo de implementación | **~4 meses** | 3–12 meses | 2–6 meses | 12–18 meses |
|
||||||
|
| Costo de implementación | **Medio** | Alto | Medio | Muy alto |
|
||||||
|
| Curva de aprendizaje usuarios | Baja | Alta | Media | Variable |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Garantías y Compromisos
|
||||||
|
|
||||||
|
- ✅ **Código fuente entregado:** La empresa es propietaria del código al finalizar el proyecto
|
||||||
|
- ✅ **Documentación técnica completa:** Estándares, esquemas DB, guías de API
|
||||||
|
- ✅ **Sin vendor lock-in:** Stack 100% open source (Angular, NestJS, PostgreSQL)
|
||||||
|
- ✅ **Seguridad by design:** OWASP Top 10, JWT validation, HTTPS obligatorio
|
||||||
|
- ✅ **Escalabilidad:** Arquitectura modular preparada para 10,000+ usuarios
|
||||||
|
- ✅ **Soporte 30 días post-lanzamiento** incluido en el contrato base
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Próximos Pasos
|
||||||
|
|
||||||
|
1. **Reunión de presentación** (30–45 min): Demo en vivo de la plataforma actual
|
||||||
|
2. **Workshop técnico** (2h): Revisión de requerimientos específicos con IT y RR.HH.
|
||||||
|
3. **Propuesta económica formal** ajustada al volumen de usuarios y módulos requeridos
|
||||||
|
4. **Firma de NDA** para compartir acceso al repositorio y documentación interna
|
||||||
|
5. **Kick-off** de Fase 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contacto
|
||||||
|
|
||||||
|
**Jess Miller**
|
||||||
|
Arquitecta de Software Senior · Full-Stack Engineer
|
||||||
|
📧 jess@kinetix.dev
|
||||||
|
🔗 github.com/kinetix
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Documento confidencial. Preparado exclusivamente para [Nombre de la Empresa]. Prohibida su reproducción parcial o total sin autorización expresa.*
|
||||||
29
doc/walkthrough.md
Normal file
29
doc/walkthrough.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Walkthrough: Módulo de Autogestión de Espacios y Activos con Asistente IA
|
||||||
|
|
||||||
|
Se ha completado la arquitectura e implementación del módulo de gestión de reservas en el frontend de Kinetix utilizando Angular 18 (Standalone Components, Signals y enrutamiento perezoso).
|
||||||
|
|
||||||
|
## Cambios Realizados
|
||||||
|
|
||||||
|
### Servicio de IA
|
||||||
|
- [ia-parser.service.ts](file:///d:/Jess/agent/Kinetix/frontend/src/app/features/reservas/services/ia-parser.service.ts): Servicio de comunicación NLP/IA con parseo heurístico inteligente como fallback de desarrollo para auto-llenar los formularios mediante prompts en lenguaje natural.
|
||||||
|
|
||||||
|
### Componentes de Presentación e Interfaz
|
||||||
|
- [calendar-booking.component.ts](file:///d:/Jess/agent/Kinetix/frontend/src/app/features/reservas/calendar-booking/calendar-booking.component.ts): Vista del calendario interactivo a pantalla completa con soporte de vistas y filtrado reactivo de salas, escritorios y activos.
|
||||||
|
- [calendar-booking.component.html](file:///d:/Jess/agent/Kinetix/frontend/src/app/features/reservas/calendar-booking/calendar-booking.component.html): Estructura visual responsiva estilizada con el **Executive Suite Design System** (paleta Ivory y tarjetas joya de alta gama).
|
||||||
|
- [reserva-form.component.ts](file:///d:/Jess/agent/Kinetix/frontend/src/app/features/reservas/reserva-form/reserva-form.component.ts): Lógica del formulario de reserva que escucha `QueryParams` para pre-seleccionar fecha/tipo de recurso y gestiona el binding con el Asistente IA.
|
||||||
|
- [reserva-form.component.html](file:///d:/Jess/agent/Kinetix/frontend/src/app/features/reservas/reserva-form/reserva-form.component.html): Diseño elegante de dos paneles que destaca la interfaz del Asistente de IA y provee inputs completamente accesibles.
|
||||||
|
|
||||||
|
### Enrutamiento
|
||||||
|
- [app.routes.ts](file:///d:/Jess/agent/Kinetix/frontend/src/app/app.routes.ts): Agregado el cargado perezoso (Lazy Loading) para las rutas principales del módulo de reservas (`/reservas/calendario` y `/reservas/nuevo`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verificación de Compilación
|
||||||
|
|
||||||
|
La compilación se completó exitosamente sin errores:
|
||||||
|
```bash
|
||||||
|
npx ng build --configuration development
|
||||||
|
```
|
||||||
|
Generó los siguientes fragmentos perezosos sin comprometer el tamaño inicial del bundle principal:
|
||||||
|
- `features-reservas-reserva-form-reserva-form-component` (407.74 kB)
|
||||||
|
- `features-reservas-calendar-booking-calendar-booking-component` (27.11 kB)
|
||||||
@ -34,15 +34,11 @@
|
|||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "500kb",
|
|
||||||
"maximumError": "1mb"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "6kb",
|
"baseline": "5kb",
|
||||||
"maximumError": "8kb"
|
"maximumWarning": "15kb",
|
||||||
|
"maximumError": "20kb"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|||||||
259
frontend/dist/kinetix-frontend/3rdpartylicenses.txt
vendored
259
frontend/dist/kinetix-frontend/3rdpartylicenses.txt
vendored
@ -1,259 +0,0 @@
|
|||||||
@angular/common
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/core
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/forms
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/platform-browser
|
|
||||||
MIT
|
|
||||||
|
|
||||||
@angular/router
|
|
||||||
MIT
|
|
||||||
|
|
||||||
rxjs
|
|
||||||
Apache-2.0
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tslib
|
|
||||||
0BSD
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
||||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
||||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
||||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
zone.js
|
|
||||||
MIT
|
|
||||||
The MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
13
frontend/dist/kinetix-frontend/index.html
vendored
13
frontend/dist/kinetix-frontend/index.html
vendored
@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="es" data-critters-container>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Kinetix Intranet</title>
|
|
||||||
<base href="/">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
||||||
<style>@import"https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;900&family=Inter:wght@400;600;700;800;900&display=swap";:root{--bg-page:#F5F4F1;--bg-surface:#FFFFFF;--bg-surface-2:#F0EEE9;--card-navy:#0D1B2A;--card-plum:#1A0E2E;--card-viridian:#0A2520;--card-espresso:#1C0E08;--gold-luxe:#C9973C;--gold-dark:#A67C28;--gold-light:#F0C060;--indigo-rich:#3730A3;--sapphire:#1E3A8A;--emerald-deep:#065F46;--neon-blue:#0EA5E9;--neon-cyan:#06B6D4;--text-primary:#0F0D0A;--text-secondary:#44403C;--text-muted:#78716C;--text-placeholder:#A8A29E;--border-light:rgba(15, 13, 10, .08);--border-medium:rgba(15, 13, 10, .14);--border-gold:rgba(201, 151, 60, .3);--bg-dark:#0D1B2A;--bg-card:rgba(17, 22, 41, .7);--bg-card-solid:#111629;--accent-cyan:#0EA5E9;--accent-magenta:hsl(320, 100%, 50%);--accent-purple:hsl(262, 80%, 60%);--bg-deep-dark:#02040A;--text-light:#FFFFFF;--text-muted-old:#858FA3}*,*:before,*:after{box-sizing:border-box}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;font-family:Outfit,Inter,sans-serif;font-weight:400;background:var(--bg-page);color:var(--text-primary);min-height:100vh;overflow-x:hidden}</style><link rel="stylesheet" href="styles.f4c113ce27f47953.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.f4c113ce27f47953.css"></noscript></head>
|
|
||||||
<body class="bg-black text-white antialiased">
|
|
||||||
<app-root></app-root>
|
|
||||||
<script src="runtime.c5542b8815441565.js" type="module"></script><script src="polyfills.db3ad031cea79ddb.js" type="module"></script><script src="main.0b345ae61172dea0.js" type="module"></script></body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
@ -20,8 +20,10 @@ import { CommonModule } from '@angular/common';
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-wordmark">
|
<div class="header-wordmark">
|
||||||
<span class="header-name">Kinetix</span>
|
<a routerLink="/dashboard">
|
||||||
<span class="header-sub">Intranet Corporativa</span>
|
<span class="header-name">Kinetix</span>
|
||||||
|
<span class="header-sub">Intranet Corporativa</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-meta">Consorcio · 2026</div>
|
<div class="header-meta">Consorcio · 2026</div>
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes)
|
provideRouter(routes),
|
||||||
|
provideHttpClient(withFetch())
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,11 @@ export const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent)
|
loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'directorio',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
loadComponent: () => import('./features/directorio/directorio.component').then(m => m.DirectorioComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'reservas',
|
path: 'reservas',
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
|
|||||||
@ -97,7 +97,7 @@ interface UserProfile {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 2: Directorio — Deep Plum #1A0E2E -->
|
<!-- Card 2: Directorio — Deep Plum #1A0E2E -->
|
||||||
<div (click)="setActiveTab('directory')"
|
<div (click)="navigateToDirectorio()"
|
||||||
style="position:relative;background:#1A0E2E;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(26,14,46,0.30);">
|
style="position:relative;background:#1A0E2E;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(26,14,46,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;top:-40px;left:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(124,58,237,0.16) 0%,transparent 70%);pointer-events:none;"></div>
|
<div style="position:absolute;top:-40px;left:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(124,58,237,0.16) 0%,transparent 70%);pointer-events:none;"></div>
|
||||||
@ -137,7 +137,7 @@ interface UserProfile {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 4: Espacios — Deep Espresso #1C0E08 -->
|
<!-- Card 4: Espacios — Deep Espresso #1C0E08 -->
|
||||||
<div (click)="setActiveTab('spaces')"
|
<div (click)="navigateToCalendario()"
|
||||||
style="position:relative;background:#1C0E08;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(28,14,8,0.32);">
|
style="position:relative;background:#1C0E08;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(28,14,8,0.32);">
|
||||||
<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;top:-40px;left:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(251,146,60,0.14) 0%,transparent 70%);pointer-events:none;"></div>
|
<div style="position:absolute;top:-40px;left:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(251,146,60,0.14) 0%,transparent 70%);pointer-events:none;"></div>
|
||||||
@ -616,203 +616,30 @@ interface UserProfile {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<aside class="w-full lg:w-64 flex flex-col gap-3">
|
|
||||||
<!-- Active User Profile Widget Sincronizado con Active Directory -->
|
|
||||||
<div class="glass-panel p-4 rounded-2xl flex flex-col gap-3 relative border-cyan-500/20 bg-slate-950/45">
|
|
||||||
<div class="absolute -right-2 -top-2 w-8 h-8 bg-cyan-500/10 rounded-full blur-md"></div>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-tr from-cyan-400 to-indigo-500 p-0.5 shadow-md">
|
|
||||||
<div class="w-full h-full bg-[#0a0a0c] rounded-[10px] flex items-center justify-center font-black text-sm text-cyan-400">
|
|
||||||
{{ currentUser().avatar }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col min-w-0">
|
|
||||||
<span class="text-sm font-extrabold text-white truncate">{{ currentUser().name }}</span>
|
|
||||||
<span class="text-[10px] text-neon-cyan font-bold tracking-wide uppercase truncate">{{ currentUser().role }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-[9px] text-white/40 font-mono truncate border-t border-white/5 pt-2 mt-1">
|
|
||||||
SSO: {{ currentUser().email }}
|
|
||||||
</div>
|
|
||||||
<button (click)="logout()" class="w-full py-1.5 rounded-xl border border-white/10 hover:border-magenta-500/30 hover:bg-magenta-950/20 text-white/60 hover:text-magenta-400 font-extrabold text-[10px] uppercase tracking-wider transition-all mt-1">
|
|
||||||
Cerrar Sesión
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glass-panel p-4 rounded-2xl flex flex-col gap-2">
|
|
||||||
<div class="text-xs font-bold text-white/40 uppercase tracking-widest px-3 mb-2">Panel Kinetix</div>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('inicio')"
|
|
||||||
[class.border-l-4]="activeTab() === 'inicio'"
|
|
||||||
[class.border-cyan-400]="activeTab() === 'inicio'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">⚡</span> Consorcio Kinetix
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('directory')"
|
|
||||||
[class.border-l-4]="activeTab() === 'directory'"
|
|
||||||
[class.border-cyan-400]="activeTab() === 'directory'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">👤</span> Directorio AD
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('workflow')"
|
|
||||||
[class.border-l-4]="activeTab() === 'workflow'"
|
|
||||||
[class.border-magenta-400]="activeTab() === 'workflow'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">⚙️</span> Workflow Aprobación
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('spaces')"
|
|
||||||
[class.border-l-4]="activeTab() === 'spaces'"
|
|
||||||
[class.border-purple-400]="activeTab() === 'spaces'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">📅</span> Gestión de Espacios
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('metrics')"
|
|
||||||
[class.border-l-4]="activeTab() === 'metrics'"
|
|
||||||
[class.border-cyan-400]="activeTab() === 'metrics'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">📈</span> Adopción y KPIs
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('admin')"
|
|
||||||
[class.border-l-4]="activeTab() === 'admin'"
|
|
||||||
[class.border-magenta-400]="activeTab() === 'admin'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">🔑</span> Visión Admin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- System Status Mini Card -->
|
|
||||||
<div class="glass-panel p-4 rounded-2xl text-xs flex flex-col gap-2">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-white/50">Estado API:</span>
|
|
||||||
<span class="text-emerald-400 font-bold flex items-center gap-1.5">
|
|
||||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> ONLINE
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-white/50">Sesión SSO:</span>
|
|
||||||
<span class="text-cyan-400 font-bold">ACTIVE AD</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-white/50">Sincronización:</span>
|
|
||||||
<span class="text-slate-300 font-mono">Entra ID (AD)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div style="background:linear-gradient(135deg,#0D1B2A 0%,#1A0E2E 50%,#0A2520 100%);border-radius:24px;padding:48px 40px;position:relative;overflow:hidden;display:flex;flex-direction:column;gap:16px;box-shadow:0 16px 48px rgba(13,27,42,0.25);">
|
|
||||||
<!-- Ambient glow -->
|
|
||||||
<div style="position:absolute;top:-60px;right:-60px;width:300px;height:300px;background:radial-gradient(circle,rgba(201,151,60,0.12) 0%,transparent 70%);pointer-events:none;"></div>
|
|
||||||
<div style="position:absolute;bottom:-60px;left:-40px;width:260px;height:260px;background:radial-gradient(circle,rgba(10,37,32,0.80) 0%,transparent 70%);pointer-events:none;"></div>
|
|
||||||
|
|
||||||
<span style="font-family:'Inter',sans-serif;font-size:9px;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:rgba(201,151,60,0.75);position:relative;z-index:1;">Portal de Consorcio · Intranet 2026</span>
|
|
||||||
<h2 style="font-family:'Inter',sans-serif;font-size:38px;font-weight:900;letter-spacing:-0.02em;color:#fff;margin:0;line-height:1.05;position:relative;z-index:1;">
|
|
||||||
Consorcio Corporativo <span style="background:linear-gradient(90deg,#C9973C,#F0C060);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">Kinetix</span>
|
|
||||||
</h2>
|
|
||||||
<p style="font-size:14px;color:rgba(255,255,255,0.50);line-height:1.65;max-width:560px;margin:0;font-weight:300;position:relative;z-index:1;">
|
|
||||||
Plataforma unificada de administración y autogestión de recursos del consorcio. Control centralizado, monitoreo en tiempo real y acceso seguro vía Active Directory.
|
|
||||||
</p>
|
|
||||||
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;position:relative;z-index:1;">
|
|
||||||
<button (click)="setActiveTab('directory')" style="height:44px;padding:0 24px;border-radius:9999px;background:linear-gradient(135deg,#C9973C,#A67C28);border:none;color:#fff;font-family:'Inter',sans-serif;font-size:11px;font-weight:800;letter-spacing:0.14em;text-transform:uppercase;cursor:pointer;box-shadow:0 4px 16px rgba(166,124,40,0.35);transition:transform 0.2s;">
|
|
||||||
Directorio Consorcio
|
|
||||||
</button>
|
|
||||||
<button (click)="setActiveTab('spaces')" style="height:44px;padding:0 22px;border-radius:9999px;background:rgba(255,255,255,0.07);border:1px solid rgba(255,255,255,0.14);color:rgba(255,255,255,0.85);font-family:'Inter',sans-serif;font-size:11px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;cursor:pointer;transition:all 0.2s;">
|
|
||||||
Reserva de Salas
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="w-full lg:w-64 flex flex-col gap-3">
|
<aside class="w-full lg:w-64 flex flex-col gap-3">
|
||||||
<!-- Active User Profile Widget Sincronizado con Active Directory -->
|
<!-- Active User Profile Widget Sincronizado con Active Directory -->
|
||||||
<div class="glass-panel p-4 rounded-2xl flex flex-col gap-3 relative border-cyan-500/20 bg-slate-950/45">
|
<div class="glass-panel p-4 rounded-2xl flex flex-col gap-3 relative border-cyan-500/20 bg-slate-950/45">
|
||||||
<div class="absolute -right-2 -top-2 w-8 h-8 bg-cyan-500/10 rounded-full blur-md"></div>
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-3">
|
<span class="text-white/50"></span>
|
||||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-tr from-cyan-400 to-indigo-500 p-0.5 shadow-md">
|
<span class="text-emerald-400 font-bold flex items-center gap-1.5">
|
||||||
<div class="w-full h-full bg-[#0a0a0c] rounded-[10px] flex items-center justify-center font-black text-sm text-cyan-400">
|
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||||
{{ currentUser().avatar }}
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col min-w-0">
|
|
||||||
<span class="text-sm font-extrabold text-white truncate">{{ currentUser().name }}</span>
|
|
||||||
<span class="text-[10px] text-neon-cyan font-bold tracking-wide uppercase truncate">{{ currentUser().role }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[9px] text-white/40 font-mono truncate border-t border-white/5 pt-2 mt-1">
|
<div class="w-full py-1.5 rounded-xl border border-white/10 hover:border-magenta-500/30 hover:bg-magenta-950/20 text-white/60 hover:text-magenta-400 font-extrabold text-[10px] uppercase tracking-wider transition-all mt-1">
|
||||||
SSO: {{ currentUser().email }}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="absolute -right-2 -top-2 w-8 h-8 bg-cyan-500/10 rounded-full blur-md">
|
||||||
<button (click)="logout()" class="w-full py-1.5 rounded-xl border border-white/10 hover:border-magenta-500/30 hover:bg-magenta-950/20 text-white/60 hover:text-magenta-400 font-extrabold text-[10px] uppercase tracking-wider transition-all mt-1">
|
<button (click)="logout()" class="w-full py-1.5 rounded-xl border border-white/10 hover:border-magenta-500/30 hover:bg-magenta-950/20 text-white/60 hover:text-magenta-400 font-extrabold text-[10px] uppercase tracking-wider transition-all mt-1">
|
||||||
Cerrar Sesión
|
Cerrar Sesión
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="glass-panel p-4 rounded-2xl flex flex-col gap-2">
|
|
||||||
<div class="text-xs font-bold text-white/40 uppercase tracking-widest px-3 mb-2">Panel Kinetix</div>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('inicio')"
|
|
||||||
[class.border-l-4]="activeTab() === 'inicio'"
|
|
||||||
[class.border-cyan-400]="activeTab() === 'inicio'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">⚡</span> Consorcio Kinetix
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('directory')"
|
|
||||||
[class.border-l-4]="activeTab() === 'directory'"
|
|
||||||
[class.border-cyan-400]="activeTab() === 'directory'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">👤</span> Directorio AD
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('workflow')"
|
|
||||||
[class.border-l-4]="activeTab() === 'workflow'"
|
|
||||||
[class.border-magenta-400]="activeTab() === 'workflow'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">⚙️</span> Workflow Aprobación
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('spaces')"
|
|
||||||
[class.border-l-4]="activeTab() === 'spaces'"
|
|
||||||
[class.border-purple-400]="activeTab() === 'spaces'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">📅</span> Gestión de Espacios
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('metrics')"
|
|
||||||
[class.border-l-4]="activeTab() === 'metrics'"
|
|
||||||
[class.border-cyan-400]="activeTab() === 'metrics'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">📈</span> Adopción y KPIs
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button (click)="setActiveTab('admin')"
|
|
||||||
[class.border-l-4]="activeTab() === 'admin'"
|
|
||||||
[class.border-magenta-400]="activeTab() === 'admin'"
|
|
||||||
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-left text-sm font-semibold transition-all hover:bg-white/5 text-slate-300 hover:text-white">
|
|
||||||
<span class="text-lg">🔑</span> Visión Admin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- System Status Mini Card -->
|
|
||||||
<div class="glass-panel p-4 rounded-2xl text-xs flex flex-col gap-2">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-white/50">Estado API:</span>
|
|
||||||
<span class="text-emerald-400 font-bold flex items-center gap-1.5">
|
|
||||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> ONLINE
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-white/50">Sesión SSO:</span>
|
|
||||||
<span class="text-cyan-400 font-bold">ACTIVE AD</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-white/50">Sincronización:</span>
|
|
||||||
<span class="text-slate-300 font-mono">Entra ID (AD)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@ -968,6 +795,14 @@ export class DashboardComponent implements OnInit {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToCalendario(): void {
|
||||||
|
this.router.navigate(['/reservas/calendario']);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToDirectorio(): void {
|
||||||
|
this.router.navigate(['/directorio']);
|
||||||
|
}
|
||||||
|
|
||||||
bookSpace() {
|
bookSpace() {
|
||||||
this.bookingSuccess.set(true);
|
this.bookingSuccess.set(true);
|
||||||
}
|
}
|
||||||
|
|||||||
696
frontend/src/app/features/directorio/directorio.component.css
Normal file
696
frontend/src/app/features/directorio/directorio.component.css
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
/* Layout General y Elementos Comunes */
|
||||||
|
.dir-shell {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #F5F4F1;
|
||||||
|
font-family: 'Inter', 'Outfit', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar,
|
||||||
|
.dir-main {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar {
|
||||||
|
width: 228px;
|
||||||
|
background: #0F0D0A;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 0 20px;
|
||||||
|
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 40;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar--collapsed {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 36px 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Componentes Flex Comunes */
|
||||||
|
.dir-sidebar__brand,
|
||||||
|
.dir-sidebar__brand-icon,
|
||||||
|
.dir-sidebar__nav-item,
|
||||||
|
.dir-sidebar__user,
|
||||||
|
.dir-header__title-block,
|
||||||
|
.dir-header__badge,
|
||||||
|
.dir-filters,
|
||||||
|
.dir-card__avatar-row,
|
||||||
|
.dir-card__avatar,
|
||||||
|
.dir-card__rating,
|
||||||
|
.dir-card__identity,
|
||||||
|
.dir-card__metrics,
|
||||||
|
.dir-card__cta,
|
||||||
|
.dir-empty,
|
||||||
|
.dir-toast,
|
||||||
|
.dir-sidebar__toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Elementos Interactivos y Botones */
|
||||||
|
.dir-sidebar__toggle,
|
||||||
|
.dir-filters__input,
|
||||||
|
.dir-filters__select,
|
||||||
|
.dir-filters__reset,
|
||||||
|
.dir-card__ad-id,
|
||||||
|
.dir-card__rating,
|
||||||
|
.dir-card__tag,
|
||||||
|
.dir-empty button {
|
||||||
|
border: 1px solid #E8E4DC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__toggle,
|
||||||
|
.dir-sidebar__nav-item,
|
||||||
|
.dir-filters__input,
|
||||||
|
.dir-filters__reset,
|
||||||
|
.dir-card,
|
||||||
|
.dir-card__cta,
|
||||||
|
.dir-empty button {
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: -13px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #FFF;
|
||||||
|
color: #78716C;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(15, 13, 10, 0.12);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__toggle:hover {
|
||||||
|
background: #C9973C;
|
||||||
|
color: #FFF;
|
||||||
|
border-color: #C9973C;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tipografía y Textos */
|
||||||
|
.dir-sidebar__brand-name,
|
||||||
|
.dir-card__name,
|
||||||
|
.dir-card__metric-value {
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0F0D0A;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__brand {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 18px 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__brand-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: rgba(201, 151, 60, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__brand-name {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #FFF;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__section-label {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.28);
|
||||||
|
padding: 0 18px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 0 10px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__nav-item {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: rgba(255, 255, 255, 0.48);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__nav-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__nav-item--active {
|
||||||
|
background: rgba(201, 151, 60, 0.12);
|
||||||
|
color: #C9973C;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__nav-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Footer */
|
||||||
|
.dir-sidebar__footer {
|
||||||
|
padding: 16px 14px 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user-avatar {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #C9973C, #A67C28);
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user-avatar--sm {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #FFF;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user-role {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.36);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__status {
|
||||||
|
font-size: 9.5px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__status strong {
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #34D399;
|
||||||
|
margin-right: 5px;
|
||||||
|
animation: pulse-dot 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__status-dot--sm {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin: 4px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: .4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Principal */
|
||||||
|
.dir-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-header__title-block {
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-header__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #0F0D0A;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-header__badge {
|
||||||
|
gap: 7px;
|
||||||
|
background: #FFF;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #57534E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-header__badge-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #34D399;
|
||||||
|
animation: pulse-dot 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-header__count {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #A8A29E;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filtros y Buscador */
|
||||||
|
.dir-filters {
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__search {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px 40px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #FFF;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: #0F0D0A;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 13, 10, 0.04);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__input::placeholder {
|
||||||
|
color: #A8A29E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__input:focus {
|
||||||
|
border-color: #C9973C;
|
||||||
|
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #A8A29E;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__clear:hover {
|
||||||
|
color: #0F0D0A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__select-wrap {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__select {
|
||||||
|
appearance: none;
|
||||||
|
padding: 11px 36px 11px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #FFF;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #0F0D0A;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 13, 10, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__select:focus {
|
||||||
|
border-color: #C9973C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__reset {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #78716C;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-filters__reset:hover {
|
||||||
|
border-color: #C9973C;
|
||||||
|
color: #C9973C;
|
||||||
|
background: rgba(201, 151, 60, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-results-summary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #A8A29E;
|
||||||
|
margin: 0 0 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-results-summary strong {
|
||||||
|
color: #57534E;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid y Tarjetas */
|
||||||
|
.dir-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card {
|
||||||
|
position: relative;
|
||||||
|
background: #FFF;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid rgba(15, 13, 10, 0.07);
|
||||||
|
box-shadow: 0 1px 4px rgba(15, 13, 10, 0.05);
|
||||||
|
gap: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
cursor: default;
|
||||||
|
transition: box-shadow 0.22s cubic-bezier(0.4, 0, 0.2, 1), transform 0.22s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.22s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card:hover {
|
||||||
|
box-shadow: 0 8px 28px rgba(15, 13, 10, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(201, 151, 60, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__ad-id {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #A8A29E;
|
||||||
|
background: #FAF9F6;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__avatar-row {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__avatar {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #FFF;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__rating {
|
||||||
|
gap: 4px;
|
||||||
|
background: #FAF9F6;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0F0D0A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__identity {
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__name {
|
||||||
|
font-size: 17px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__role {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #C9973C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__dept {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #A8A29E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #57534E;
|
||||||
|
background: #F5F4F1;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #F0EEE9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__metric {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__metric-label {
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: #A8A29E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__metric-value {
|
||||||
|
font-size: 17px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__metric-sep {
|
||||||
|
width: 1px;
|
||||||
|
height: 36px;
|
||||||
|
background: #F0EEE9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__cta {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1.5px solid #0F0D0A;
|
||||||
|
background: transparent;
|
||||||
|
color: #0F0D0A;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
cursor: pointer;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-card__cta:hover {
|
||||||
|
background: #0F0D0A;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estados Vacíos y Toasts */
|
||||||
|
.dir-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #A8A29E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-empty p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-empty button {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #78716C;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-empty button:hover {
|
||||||
|
border-color: #C9973C;
|
||||||
|
color: #C9973C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 28px;
|
||||||
|
right: 28px;
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #D1FAE5;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 13, 10, 0.1);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 13px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #065F46;
|
||||||
|
gap: 9px;
|
||||||
|
z-index: 999;
|
||||||
|
animation: slide-toast 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-toast {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px)
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media Queries Responsivas */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.dir-sidebar {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__brand-name,
|
||||||
|
.dir-sidebar__section-label,
|
||||||
|
.dir-sidebar__nav-label,
|
||||||
|
.dir-sidebar__user-info,
|
||||||
|
.dir-sidebar__status,
|
||||||
|
.dir-sidebar__toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-sidebar__user {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-main {
|
||||||
|
padding: 24px 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.dir-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-main {
|
||||||
|
padding: 20px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
317
frontend/src/app/features/directorio/directorio.component.ts
Normal file
317
frontend/src/app/features/directorio/directorio.component.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { Component, signal, computed, inject, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Router, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
interface Employee {
|
||||||
|
id: number;
|
||||||
|
adId: string;
|
||||||
|
name: string;
|
||||||
|
initials: string;
|
||||||
|
role: string;
|
||||||
|
department: string;
|
||||||
|
rating: number;
|
||||||
|
clients: number;
|
||||||
|
rate: number;
|
||||||
|
tags: string[];
|
||||||
|
email: string;
|
||||||
|
avatarColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-directorio',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, RouterModule],
|
||||||
|
template: `
|
||||||
|
<div class="dir-shell">
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════ SIDEBAR ═══════════════════════════════ -->
|
||||||
|
<aside class="dir-sidebar" [class.dir-sidebar--collapsed]="sidebarCollapsed()">
|
||||||
|
|
||||||
|
<!-- Toggle btn -->
|
||||||
|
<button class="dir-sidebar__toggle" (click)="toggleSidebar()" title="Colapsar menú">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path [attr.d]="sidebarCollapsed() ? 'M6 2l6 6-6 6' : 'M10 2L4 8l6 6'"
|
||||||
|
stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Brand -->
|
||||||
|
<div class="dir-sidebar__brand" (click)="router.navigate(['/dashboard'])">
|
||||||
|
<div class="dir-sidebar__brand-icon">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 2L22 7L12 12L2 7L12 2Z" stroke="#C9973C" stroke-width="1.8" stroke-linejoin="round"/>
|
||||||
|
<path d="M2 12L12 17L22 12" stroke="#C9973C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2 17L12 22L22 17" stroke="#C9973C" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
@if (!sidebarCollapsed()) {
|
||||||
|
<span class="dir-sidebar__brand-name">Kinetix</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav label -->
|
||||||
|
@if (!sidebarCollapsed()) {
|
||||||
|
<div class="dir-sidebar__section-label">Panel Kinetix</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Nav items -->
|
||||||
|
<nav class="dir-sidebar__nav">
|
||||||
|
@for (item of navItems; track item.route) {
|
||||||
|
<a [routerLink]="item.route" class="dir-sidebar__nav-item"
|
||||||
|
[class.dir-sidebar__nav-item--active]="isActiveRoute(item.route)"
|
||||||
|
[title]="item.label">
|
||||||
|
<span class="dir-sidebar__nav-icon" [innerHTML]="item.icon"></span>
|
||||||
|
@if (!sidebarCollapsed()) {
|
||||||
|
<span class="dir-sidebar__nav-label">{{ item.label }}</span>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Footer: User Profile + System Status -->
|
||||||
|
<div class="dir-sidebar__footer">
|
||||||
|
@if (!sidebarCollapsed()) {
|
||||||
|
<div class="dir-sidebar__user">
|
||||||
|
<div class="dir-sidebar__user-avatar">JM</div>
|
||||||
|
<div class="dir-sidebar__user-info">
|
||||||
|
<span class="dir-sidebar__user-name">Jess Miller</span>
|
||||||
|
<span class="dir-sidebar__user-role">Admin · HR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dir-sidebar__status">
|
||||||
|
<span class="dir-sidebar__status-dot"></span>
|
||||||
|
Estado API: <strong>ONLINE</strong> | Sesión SSO: <strong>ACTIVE AD</strong>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="dir-sidebar__user-avatar dir-sidebar__user-avatar--sm">JM</div>
|
||||||
|
<div class="dir-sidebar__status-dot dir-sidebar__status-dot--sm"></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════ MAIN PANEL ═══════════════════════════════ -->
|
||||||
|
<main class="dir-main">
|
||||||
|
|
||||||
|
<!-- Module Header -->
|
||||||
|
<header class="dir-header">
|
||||||
|
<div class="dir-header__title-block">
|
||||||
|
<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>
|
||||||
|
<span class="dir-header__count">{{ filteredEmployees().length }} empleados</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Search + Filters Bar -->
|
||||||
|
<div class="dir-filters">
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="dir-filters__search">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Results summary -->
|
||||||
|
<p class="dir-results-summary">
|
||||||
|
Mostrando <strong>{{ filteredEmployees().length }}</strong> de <strong>{{ employees().length }}</strong> empleados registrados
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Employee Grid -->
|
||||||
|
<div class="dir-grid">
|
||||||
|
@for (emp of filteredEmployees(); track emp.id) {
|
||||||
|
<article class="dir-card">
|
||||||
|
|
||||||
|
<!-- AD ID chip (monospace, top-right) -->
|
||||||
|
<div class="dir-card__ad-id">{{ emp.adId }}</div>
|
||||||
|
|
||||||
|
<!-- Avatar + Rating row -->
|
||||||
|
<div class="dir-card__avatar-row">
|
||||||
|
<div class="dir-card__avatar" [style.background]="emp.avatarColor">
|
||||||
|
{{ emp.initials }}
|
||||||
|
</div>
|
||||||
|
<div class="dir-card__rating">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Name + Role -->
|
||||||
|
<div class="dir-card__identity">
|
||||||
|
<h3 class="dir-card__name">{{ emp.name }}</h3>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div class="dir-card__divider"></div>
|
||||||
|
|
||||||
|
<!-- Metrics -->
|
||||||
|
<div class="dir-card__metrics">
|
||||||
|
<div class="dir-card__metric">
|
||||||
|
<span class="dir-card__metric-label">Clientes</span>
|
||||||
|
<span class="dir-card__metric-value">{{ emp.clients }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dir-card__metric-sep"></div>
|
||||||
|
<div class="dir-card__metric">
|
||||||
|
<span class="dir-card__metric-label">Tarifa/Hora</span>
|
||||||
|
<span class="dir-card__metric-value">\${{ emp.rate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<button class="dir-card__cta" (click)="contact(emp)">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</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>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact toast -->
|
||||||
|
@if (contactToast()) {
|
||||||
|
<div class="dir-toast">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M20 6L9 17l-5-5" stroke="#065F46" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
Mensaje enviado a {{ contactToast() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styleUrls: ['./directorio.component.css']
|
||||||
|
})
|
||||||
|
export class DirectorioComponent implements OnInit {
|
||||||
|
readonly router = inject(Router);
|
||||||
|
|
||||||
|
sidebarCollapsed = signal(false);
|
||||||
|
contactToast = signal('');
|
||||||
|
|
||||||
|
searchQuery = '';
|
||||||
|
selectedDept = 'ALL';
|
||||||
|
|
||||||
|
private _searchSignal = signal('');
|
||||||
|
private _deptSignal = signal('ALL');
|
||||||
|
|
||||||
|
navItems = [
|
||||||
|
{ label: 'Panel Principal', route: '/dashboard', icon: '⚡' },
|
||||||
|
{ label: 'Directorio AD', route: '/directorio', icon: '👤' },
|
||||||
|
{ label: 'Gestión de Espacios', route: '/reservas/calendario', icon: '📅' },
|
||||||
|
];
|
||||||
|
|
||||||
|
employees = signal<Employee[]>([]);
|
||||||
|
|
||||||
|
filteredEmployees = computed(() => {
|
||||||
|
const q = this._searchSignal().toLowerCase().trim();
|
||||||
|
const dept = this._deptSignal();
|
||||||
|
|
||||||
|
return this.employees().filter(emp => {
|
||||||
|
const matchesSearch = !q ||
|
||||||
|
emp.name.toLowerCase().includes(q) ||
|
||||||
|
emp.role.toLowerCase().includes(q) ||
|
||||||
|
emp.tags.some(t => t.toLowerCase().includes(q));
|
||||||
|
|
||||||
|
const matchesDept = dept === 'ALL' ||
|
||||||
|
emp.department === dept;
|
||||||
|
|
||||||
|
return matchesSearch && matchesDept;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ngOnInit(): void { }
|
||||||
|
|
||||||
|
toggleSidebar(): void {
|
||||||
|
this.sidebarCollapsed.update(v => !v);
|
||||||
|
}
|
||||||
|
|
||||||
|
isActiveRoute(route: string): boolean {
|
||||||
|
return this.router.url === route || this.router.url.startsWith(route + '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(value: string): void {
|
||||||
|
this._searchSignal.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeptChange(value: string): void {
|
||||||
|
this._deptSignal.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch(): void {
|
||||||
|
this.searchQuery = '';
|
||||||
|
this._searchSignal.set('');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFilters(): void {
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.selectedDept = 'ALL';
|
||||||
|
this._searchSignal.set('');
|
||||||
|
this._deptSignal.set('ALL');
|
||||||
|
}
|
||||||
|
|
||||||
|
contact(emp: Employee): void {
|
||||||
|
this.contactToast.set(emp.name);
|
||||||
|
setTimeout(() => this.contactToast.set(''), 3500);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,511 @@
|
|||||||
|
/* Layout Principal y Contenedores */
|
||||||
|
.calendar-layout {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 160px);
|
||||||
|
background-color: var(--bg-surface, #FFF);
|
||||||
|
border: 1px solid var(--border-light, rgba(15, 13, 10, 0.08));
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 20px rgba(15, 13, 10, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-sidebar,
|
||||||
|
.calendar-view-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background: #FFF;
|
||||||
|
border-right: 1px solid var(--border-light, rgba(15, 13, 10, 0.08));
|
||||||
|
padding: 24px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-view-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botones y Elementos Interactivos */
|
||||||
|
.btn-create,
|
||||||
|
.btn-today,
|
||||||
|
.view-select {
|
||||||
|
border: 1px solid var(--border-medium, rgba(15, 13, 10, 0.14));
|
||||||
|
background: #FFF;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--text-primary, #0F0D0A);
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 24px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: box-shadow 0.2s, background-color 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create:hover {
|
||||||
|
background-color: #F8F9FA;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-create .plus-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--gold-luxe, #C9973C);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-today,
|
||||||
|
.view-select {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-today:hover,
|
||||||
|
.hour-slot-cell:hover,
|
||||||
|
.btn-create:hover {
|
||||||
|
background-color: #F8F9FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-arrow {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--border-medium, rgba(15, 13, 10, 0.08));
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-arrow:hover,
|
||||||
|
.mini-day-cell:hover,
|
||||||
|
.month-day-cell:hover {
|
||||||
|
background-color: var(--bg-page, #F5F4F1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-select {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mini Calendario de la Barra Lateral */
|
||||||
|
.mini-calendar,
|
||||||
|
.filter-section,
|
||||||
|
.filter-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar,
|
||||||
|
.filter-section {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #44403C);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar-grid,
|
||||||
|
.grid-month {
|
||||||
|
display: grid;
|
||||||
|
grid-template-cols: repeat(7, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-calendar-grid {
|
||||||
|
text-align: center;
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-day-name,
|
||||||
|
.day-name-cell {
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-day-name {
|
||||||
|
font-size: 10px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-day-cell {
|
||||||
|
font-size: 11px;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-day-cell.other-month {
|
||||||
|
color: var(--text-placeholder, #A8A29E);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-day-cell.today,
|
||||||
|
.day-number.today,
|
||||||
|
.month-day-number.today {
|
||||||
|
background-color: var(--gold-luxe, #C9973C);
|
||||||
|
color: #FFF !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filtros */
|
||||||
|
.filter-title {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-checkbox {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--gold-luxe, #C9973C);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Principal */
|
||||||
|
.main-header {
|
||||||
|
height: 64px;
|
||||||
|
background: var(--bg-surface, #FFF);
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(0, 0, 0, 0.08));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header-left,
|
||||||
|
.header-title-group,
|
||||||
|
.header-app-logo,
|
||||||
|
.main-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title-group {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-app-logo {
|
||||||
|
gap: 8px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-app-logo span {
|
||||||
|
color: var(--gold-luxe);
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-date-label {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: var(--text-primary, #0F0D0A);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vista Semanal */
|
||||||
|
.week-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 800px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-header,
|
||||||
|
.week-grid-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-cols: 64px repeat(7, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-header {
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(0, 0, 0, 0.08));
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #FFF;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timezone-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-right: 1px solid var(--border-light, rgba(0, 0, 0, 0.08));
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day-header,
|
||||||
|
.day-column {
|
||||||
|
border-right: 1px solid var(--border-light, rgba(0, 0, 0, 0.08));
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day-header:last-child,
|
||||||
|
.day-column:last-child,
|
||||||
|
.day-name-cell:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-name {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-grid-body {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-labels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--border-light, rgba(0, 0, 0, 0.08));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-label-cell {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-column {
|
||||||
|
position: relative;
|
||||||
|
background-color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hour-slot-cell {
|
||||||
|
height: 60px;
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(15, 13, 10, 0.05));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eventos e Indicadores */
|
||||||
|
.calendar-event {
|
||||||
|
position: absolute;
|
||||||
|
left: 4px;
|
||||||
|
right: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
border-left: 4px solid;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-event .event-time {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time-indicator {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #EA4335;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time-indicator::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -6px;
|
||||||
|
top: -5px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #EA4335;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vista Mensal */
|
||||||
|
.day-names-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-cols: repeat(7, 1fr);
|
||||||
|
background: var(--bg-page, #F0EEE9);
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(15, 13, 10, 0.08));
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-name-cell {
|
||||||
|
padding: 12px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-right: 1px solid var(--border-light, rgba(0, 0, 0, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-month {
|
||||||
|
grid-template-rows: repeat(6, 1fr);
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 500px;
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(15, 13, 10, 0.08));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-day-cell {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
border-right: 1px solid var(--border-light, rgba(15, 13, 10, 0.08));
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(15, 13, 10, 0.08));
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #FFF;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-day-cell.other-month {
|
||||||
|
background-color: #FAF9F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-day-number {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #44403C);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media Queries */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.calendar-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-sidebar {
|
||||||
|
width: 100% !important;
|
||||||
|
border-right: none !important;
|
||||||
|
border-bottom: 1px solid var(--border-light, rgba(0, 0, 0, 0.08));
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-main {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-view-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,177 +1,243 @@
|
|||||||
<div class="flex flex-col md:flex-row h-screen w-full bg-[#F5F4F1] text-[#0F0D0A] font-sans overflow-hidden">
|
<div class="calendar-layout">
|
||||||
<!-- Sidebar de Filtros Dinámicos (Estilo Glassmorphism / Kinetix) -->
|
<!-- Sidebar de Filtros e Info -->
|
||||||
<aside class="w-full md:w-80 bg-white border-r border-[#0F0D0A]/10 p-6 flex flex-col justify-between gap-6 shrink-0 shadow-sm">
|
<aside class="calendar-sidebar">
|
||||||
<div class="flex flex-col gap-6">
|
<button (click)="selectDay(currentDate())" class="btn-create">
|
||||||
<div class="flex items-center gap-3">
|
<span class="plus-icon">+</span>
|
||||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-[#C9973C] to-[#A67C28] flex items-center justify-center text-white shadow-md shadow-[#C9973C]/20">
|
<span>Crear Reserva</span>
|
||||||
<span class="material-icons font-bold text-lg">business_center</span>
|
</button>
|
||||||
</div>
|
|
||||||
<div>
|
<!-- Mini Calendario -->
|
||||||
<h2 class="text-lg font-bold tracking-tight text-[#0F0D0A]">Reservas</h2>
|
<div class="mini-calendar">
|
||||||
<p class="text-xs text-[#78716C]">Autogestión de Espacios e IA</p>
|
<div class="mini-calendar-header">
|
||||||
|
<span class="mini-calendar-title">{{ monthYearLabel() }}</span>
|
||||||
|
<div class="mini-calendar-nav">
|
||||||
|
<button (click)="prev()" class="nav-arrow" style="width:24px; height:24px; font-size:12px;"><</button>
|
||||||
|
<button (click)="next()" class="nav-arrow" style="width:24px; height:24px; font-size:12px;">></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mini-calendar-grid">
|
||||||
|
<!-- Nombres días -->
|
||||||
|
<span class="mini-day-name">D</span>
|
||||||
|
<span class="mini-day-name">L</span>
|
||||||
|
<span class="mini-day-name">M</span>
|
||||||
|
<span class="mini-day-name">M</span>
|
||||||
|
<span class="mini-day-name">J</span>
|
||||||
|
<span class="mini-day-name">V</span>
|
||||||
|
<span class="mini-day-name">S</span>
|
||||||
|
|
||||||
<!-- Filtros Rápidos -->
|
<!-- Celdas -->
|
||||||
<div class="flex flex-col gap-2">
|
@for (day of calendarDays().slice(0, 35); track day.getTime()) {
|
||||||
<label class="text-xs font-bold uppercase tracking-wider text-[#78716C] mb-1">Categorías</label>
|
<span
|
||||||
|
(click)="selectDay(day)"
|
||||||
<button
|
[class.today]="isToday(day)"
|
||||||
(click)="setFilter('all')"
|
[class.other-month]="!isCurrentMonth(day)"
|
||||||
[class.bg-[#0D1B2A]]="selectedFilter() === 'all'"
|
class="mini-day-cell">
|
||||||
[class.text-white]="selectedFilter() === 'all'"
|
{{ day.getDate() }}
|
||||||
class="flex items-center gap-3 px-4 py-3 rounded-xl border border-[#0F0D0A]/10 hover:border-[#C9973C] transition-all text-left text-sm font-medium">
|
</span>
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-[#C9973C]"></span>
|
}
|
||||||
<span>Ver Todo</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
(click)="setFilter('room')"
|
|
||||||
[class.bg-[#0D1B2A]]="selectedFilter() === 'room'"
|
|
||||||
[class.text-white]="selectedFilter() === 'room'"
|
|
||||||
class="flex items-center gap-3 px-4 py-3 rounded-xl border border-[#0F0D0A]/10 hover:border-[#C9973C] transition-all text-left text-sm font-medium">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-indigo-600"></span>
|
|
||||||
<span>Salas de Juntas</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
(click)="setFilter('desk')"
|
|
||||||
[class.bg-[#0D1B2A]]="selectedFilter() === 'desk'"
|
|
||||||
[class.text-white]="selectedFilter() === 'desk'"
|
|
||||||
class="flex items-center gap-3 px-4 py-3 rounded-xl border border-[#0F0D0A]/10 hover:border-[#C9973C] transition-all text-left text-sm font-medium">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-amber-600"></span>
|
|
||||||
<span>Oficinas / Escritorios</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
(click)="setFilter('asset')"
|
|
||||||
[class.bg-[#0D1B2A]]="selectedFilter() === 'asset'"
|
|
||||||
[class.text-white]="selectedFilter() === 'asset'"
|
|
||||||
class="flex items-center gap-3 px-4 py-3 rounded-xl border border-[#0F0D0A]/10 hover:border-[#C9973C] transition-all text-left text-sm font-medium">
|
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-emerald-600"></span>
|
|
||||||
<span>Activos y Equipamiento</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="h-px bg-[#0F0D0A]/10"></div>
|
|
||||||
|
|
||||||
<!-- Leyenda -->
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs font-bold uppercase tracking-wider text-[#78716C]">Información</label>
|
|
||||||
<div class="text-xs text-[#78716C] leading-relaxed">
|
|
||||||
Haz clic en cualquier celda o día en el calendario para agendar una reserva de forma directa utilizando nuestro **Asistente IA**.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botón directo de Reserva IA -->
|
<!-- Filtros de Recursos -->
|
||||||
<button
|
<div class="filter-section">
|
||||||
[routerLink]="['/reservas/nuevo']"
|
<h4 class="filter-title">Categorías</h4>
|
||||||
class="w-full flex items-center justify-center gap-2 h-12 rounded-full bg-gradient-to-r from-[#C9973C] to-[#A67C28] text-white font-bold uppercase text-[10px] tracking-widest shadow-lg shadow-[#A67C28]/25 hover:translate-y-[-2px] transition-all">
|
<div class="filter-list">
|
||||||
<span class="material-icons text-sm">smart_toy</span>
|
<label class="filter-item">
|
||||||
<span>Asistente IA</span>
|
<input type="radio" name="resType" [checked]="selectedFilter() === 'all'" (change)="setFilter('all')" class="filter-checkbox">
|
||||||
</button>
|
<span>Ver Todo</span>
|
||||||
|
</label>
|
||||||
|
<label class="filter-item">
|
||||||
|
<input type="radio" name="resType" [checked]="selectedFilter() === 'room'" (change)="setFilter('room')" class="filter-checkbox">
|
||||||
|
<span>Salas de Juntas</span>
|
||||||
|
</label>
|
||||||
|
<label class="filter-item">
|
||||||
|
<input type="radio" name="resType" [checked]="selectedFilter() === 'desk'" (change)="setFilter('desk')" class="filter-checkbox">
|
||||||
|
<span>Escritorios</span>
|
||||||
|
</label>
|
||||||
|
<label class="filter-item">
|
||||||
|
<input type="radio" name="resType" [checked]="selectedFilter() === 'asset'" (change)="setFilter('asset')" class="filter-checkbox">
|
||||||
|
<span>Activos</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Panel del Calendario (Full Screen Layout) -->
|
<!-- Panel Principal -->
|
||||||
<main class="flex-1 flex flex-col h-full overflow-hidden p-6 gap-6">
|
<main class="calendar-main">
|
||||||
<!-- Cabecera del Calendario -->
|
<header class="main-header">
|
||||||
<header class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-white border border-[#0F0D0A]/10 rounded-2xl p-4 shadow-sm shrink-0">
|
<div class="main-header-left">
|
||||||
<div class="flex items-center gap-4">
|
<div class="header-title-group">
|
||||||
<button (click)="prev()" class="w-10 h-10 rounded-full border border-[#0F0D0A]/10 hover:bg-[#F5F4F1] flex items-center justify-center transition-colors">
|
<div class="header-app-logo">KINETIX <span>Intranet</span></div>
|
||||||
<span class="material-icons text-base"><</span>
|
</div>
|
||||||
</button>
|
<button (click)="today()" class="btn-today">Hoy</button>
|
||||||
<h1 class="text-xl font-bold tracking-tight text-[#0F0D0A] capitalize min-w-[150px] text-center">
|
<button (click)="prev()" class="nav-arrow"><</button>
|
||||||
{{ monthYearLabel() }}
|
<button (click)="next()" class="nav-arrow">></button>
|
||||||
</h1>
|
<span class="current-date-label">{{ monthYearLabel() }}</span>
|
||||||
<button (click)="next()" class="w-10 h-10 rounded-full border border-[#0F0D0A]/10 hover:bg-[#F5F4F1] flex items-center justify-center transition-colors">
|
|
||||||
<span class="material-icons text-base">></span>
|
|
||||||
</button>
|
|
||||||
<button (click)="today()" class="px-4 h-10 rounded-full border border-[#0F0D0A]/10 text-xs font-medium hover:bg-[#F5F4F1] transition-colors ml-2">
|
|
||||||
Hoy
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="main-header-right">
|
||||||
<!-- Vistas de visualización (Día, Semana, Mes) -->
|
<select [value]="currentView()" (change)="setView($any($event.target).value)" class="view-select">
|
||||||
<div class="flex bg-[#F0EEE9] p-1 rounded-full border border-[#0F0D0A]/5">
|
<option value="month">Mes</option>
|
||||||
<button
|
<option value="week">Semana</option>
|
||||||
(click)="setView('month')"
|
<option value="day">Día</option>
|
||||||
[class.bg-white]="currentView() === 'month'"
|
</select>
|
||||||
[class.shadow-sm]="currentView() === 'month'"
|
|
||||||
class="px-4 py-1.5 rounded-full text-xs font-semibold transition-all">
|
|
||||||
Mes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="setView('week')"
|
|
||||||
[class.bg-white]="currentView() === 'week'"
|
|
||||||
[class.shadow-sm]="currentView() === 'week'"
|
|
||||||
class="px-4 py-1.5 rounded-full text-xs font-semibold transition-all">
|
|
||||||
Semana
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
(click)="setView('day')"
|
|
||||||
[class.bg-white]="currentView() === 'day'"
|
|
||||||
[class.shadow-sm]="currentView() === 'day'"
|
|
||||||
class="px-4 py-1.5 rounded-full text-xs font-semibold transition-all">
|
|
||||||
Día
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Grid de Calendario Full-Screen (Modo Mes) -->
|
<!-- Calendario Contenido -->
|
||||||
<div class="flex-1 bg-white border border-[#0F0D0A]/10 rounded-2xl shadow-sm flex flex-col overflow-hidden">
|
<div class="calendar-view-container">
|
||||||
<!-- Días de la semana -->
|
|
||||||
<div class="grid grid-cols-7 bg-[#F0EEE9]/50 border-b border-[#0F0D0A]/10 shrink-0">
|
<!-- 1. VISTA SEMANAL -->
|
||||||
@for (dayName of weekDays; track $index) {
|
@if (currentView() === 'week') {
|
||||||
<div class="py-3 text-center text-xs font-bold tracking-wider text-[#78716C] uppercase border-r border-[#0F0D0A]/10 last:border-r-0">
|
<div class="week-view animate-fadeIn">
|
||||||
{{ dayName }}
|
<!-- Encabezado de la semana -->
|
||||||
|
<div class="week-header">
|
||||||
|
<div class="timezone-label">GMT-04</div>
|
||||||
|
@for (day of currentWeekDays(); track day.getTime()) {
|
||||||
|
<div (click)="selectDay(day)" class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors">
|
||||||
|
<span class="day-name">{{ weekDays[day.getDay()] }}</span>
|
||||||
|
<span [class.today]="isToday(day)" class="day-number">
|
||||||
|
{{ day.getDate() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cuadrícula de Celdas -->
|
<!-- Cuerpo con cuadrícula de horas -->
|
||||||
<div class="grid grid-cols-7 grid-rows-6 flex-1 divide-x divide-y divide-[#0F0D0A]/10">
|
<div class="week-grid-body">
|
||||||
@for (day of calendarDays(); track day.getTime()) {
|
<div class="hour-labels">
|
||||||
<div
|
@for (hour of hours; track hour) {
|
||||||
(click)="selectDay(day)"
|
<div class="hour-label-cell">
|
||||||
[class.bg-slate-50]="!isCurrentMonth(day)"
|
{{ formatHour(hour) }}
|
||||||
class="p-2 flex flex-col gap-1 hover:bg-[#F5F4F1]/60 transition-colors cursor-pointer group relative overflow-hidden">
|
|
||||||
|
|
||||||
<!-- Encabezado de la celda de día -->
|
|
||||||
<div class="flex justify-between items-center shrink-0">
|
|
||||||
<span
|
|
||||||
[class.bg-[#C9973C]]="isToday(day)"
|
|
||||||
[class.text-white]="isToday(day)"
|
|
||||||
[class.font-black]="isToday(day)"
|
|
||||||
[class.w-7]="isToday(day)"
|
|
||||||
[class.h-7]="isToday(day)"
|
|
||||||
[class.rounded-full]="isToday(day)"
|
|
||||||
[class.shadow-md]="isToday(day)"
|
|
||||||
class="flex items-center justify-center text-xs font-bold text-[#78716C] group-hover:text-[#0F0D0A] transition-colors">
|
|
||||||
{{ day.getDate() }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Listado de Eventos / Reservas del día -->
|
|
||||||
<div class="flex-1 flex flex-col gap-1 overflow-y-auto pr-1">
|
|
||||||
@for (event of getEventsForDay(day); track event.id) {
|
|
||||||
<div
|
|
||||||
[class]="event.colorClass"
|
|
||||||
class="text-[10px] leading-snug p-1.5 rounded-md truncate font-medium shadow-sm border"
|
|
||||||
[title]="event.title + ' (' + event.resourceName + ')'">
|
|
||||||
<div class="font-bold truncate">{{ event.title }}</div>
|
|
||||||
<div class="text-[9px] opacity-75">{{ event.resourceName }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hint de clic al pasar cursor -->
|
<!-- Columnas por día -->
|
||||||
<span class="absolute bottom-1 right-2 text-[9px] text-[#C9973C] opacity-0 group-hover:opacity-100 transition-opacity font-semibold">
|
@for (day of currentWeekDays(); track day.getTime()) {
|
||||||
+ Reservar
|
<div class="day-column">
|
||||||
</span>
|
@if (isToday(day)) {
|
||||||
|
<div [style.top.px]="getCurrentTimeTop()" class="current-time-indicator"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (hour of hours; track hour) {
|
||||||
|
<div (click)="selectTimeSlot(day, hour)" class="hour-slot-cell"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Eventos -->
|
||||||
|
@for (event of getEventsForDay(day); track event.id) {
|
||||||
|
<div
|
||||||
|
(click)="$event.stopPropagation(); selectDay(day)"
|
||||||
|
[style]="getEventStyle(event)"
|
||||||
|
[class]="event.colorClass"
|
||||||
|
class="calendar-event cursor-pointer">
|
||||||
|
<span class="event-time">{{ event.start | date:'shortTime' }} - {{ event.end | date:'shortTime' }}</span>
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
<span>{{ event.resourceName }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
<!-- 2. VISTA MENSUAL -->
|
||||||
|
@if (currentView() === 'month') {
|
||||||
|
<div class="animate-fadeIn" style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
||||||
|
<!-- Fila de nombres de días -->
|
||||||
|
<div class="day-names-row">
|
||||||
|
@for (dayName of weekDays; track $index) {
|
||||||
|
<div class="day-name-cell">
|
||||||
|
{{ dayName }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cuadrícula mensual -->
|
||||||
|
<div class="grid-month">
|
||||||
|
@for (day of calendarDays(); track day.getTime()) {
|
||||||
|
<div
|
||||||
|
(click)="selectDay(day)"
|
||||||
|
[class.other-month]="!isCurrentMonth(day)"
|
||||||
|
class="month-day-cell">
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: flex-start; align-items: center; margin-bottom: 4px;">
|
||||||
|
<span
|
||||||
|
[class.today]="isToday(day)"
|
||||||
|
class="month-day-number">
|
||||||
|
{{ day.getDate() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 1; display: flex; flex-direction: column; gap: 4px; overflow-y: auto;">
|
||||||
|
@for (event of getEventsForDay(day); track event.id) {
|
||||||
|
<div
|
||||||
|
(click)="$event.stopPropagation(); selectDay(day)"
|
||||||
|
[class]="event.colorClass"
|
||||||
|
class="calendar-event"
|
||||||
|
style="position: static; width: auto; margin: 0; box-shadow: none; border-left-width: 3px;"
|
||||||
|
[title]="event.title">
|
||||||
|
<div style="font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ event.title }}</div>
|
||||||
|
<div style="font-size: 9px; opacity: 0.75; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ event.resourceName }}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- 3. VISTA DIARIA -->
|
||||||
|
@if (currentView() === 'day') {
|
||||||
|
<div class="week-view animate-fadeIn">
|
||||||
|
<!-- Encabezado de la semana -->
|
||||||
|
<div class="week-header" style="grid-template-cols: 64px 1fr;">
|
||||||
|
<div class="timezone-label">GMT-04</div>
|
||||||
|
<div (click)="selectDay(currentDate())" class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors">
|
||||||
|
<span class="day-name">{{ weekDays[currentDate().getDay()] }}</span>
|
||||||
|
<span [class.today]="isToday(currentDate())" class="day-number">
|
||||||
|
{{ currentDate().getDate() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cuerpo con cuadrícula de horas -->
|
||||||
|
<div class="week-grid-body" style="grid-template-cols: 64px 1fr;">
|
||||||
|
<div class="hour-labels">
|
||||||
|
@for (hour of hours; track hour) {
|
||||||
|
<div class="hour-label-cell">
|
||||||
|
{{ formatHour(hour) }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Columna única por el día actual -->
|
||||||
|
<div class="day-column">
|
||||||
|
@if (isToday(currentDate())) {
|
||||||
|
<div [style.top.px]="getCurrentTimeTop()" class="current-time-indicator"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (hour of hours; track hour) {
|
||||||
|
<div (click)="selectTimeSlot(currentDate(), hour)" class="hour-slot-cell"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Eventos -->
|
||||||
|
@for (event of getEventsForDay(currentDate()); track event.id) {
|
||||||
|
<div
|
||||||
|
(click)="$event.stopPropagation(); selectDay(currentDate())"
|
||||||
|
[style]="getEventStyle(event)"
|
||||||
|
[class]="event.colorClass"
|
||||||
|
class="calendar-event cursor-pointer">
|
||||||
|
<span class="event-time">{{ event.start | date:'shortTime' }} - {{ event.end | date:'shortTime' }}</span>
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
<span>{{ event.resourceName }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,11 +17,14 @@ export interface CalendarEvent {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterModule],
|
imports: [CommonModule, RouterModule],
|
||||||
templateUrl: './calendar-booking.component.html',
|
templateUrl: './calendar-booking.component.html',
|
||||||
styleUrls: []
|
styleUrls: ['./calendar-booking.component.css']
|
||||||
})
|
})
|
||||||
export class CalendarBookingComponent implements OnInit {
|
export class CalendarBookingComponent implements OnInit {
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
|
||||||
|
// Array de horas (0 a 23)
|
||||||
|
hours: number[] = Array.from({ length: 24 }, (_, i) => i);
|
||||||
|
|
||||||
// Signals para gestionar estado reactivamente
|
// Signals para gestionar estado reactivamente
|
||||||
currentView = signal<'month' | 'week' | 'day'>('month');
|
currentView = signal<'month' | 'week' | 'day'>('month');
|
||||||
currentDate = signal<Date>(new Date());
|
currentDate = signal<Date>(new Date());
|
||||||
@ -157,4 +160,64 @@ export class CalendarBookingComponent implements OnInit {
|
|||||||
isCurrentMonth(day: Date): boolean {
|
isCurrentMonth(day: Date): boolean {
|
||||||
return day.getMonth() === this.currentDate().getMonth();
|
return day.getMonth() === this.currentDate().getMonth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Días de la semana actual para la vista semanal
|
||||||
|
currentWeekDays = computed(() => {
|
||||||
|
const today = this.currentDate();
|
||||||
|
const startOfWeek = new Date(today);
|
||||||
|
// Ajustar para que comience en el domingo de la semana
|
||||||
|
startOfWeek.setDate(today.getDate() - today.getDay());
|
||||||
|
|
||||||
|
const days: Date[] = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const d = new Date(startOfWeek);
|
||||||
|
d.setDate(startOfWeek.getDate() + i);
|
||||||
|
days.push(d);
|
||||||
|
}
|
||||||
|
return days;
|
||||||
|
});
|
||||||
|
|
||||||
|
formatHour(hour: number): string {
|
||||||
|
if (hour === 0) return '12 AM';
|
||||||
|
if (hour === 12) return '12 PM';
|
||||||
|
return hour > 12 ? `${hour - 12} PM` : `${hour} AM`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventStyle(event: CalendarEvent): any {
|
||||||
|
const startHour = event.start.getHours() + event.start.getMinutes() / 60;
|
||||||
|
const endHour = event.end.getHours() + event.end.getMinutes() / 60;
|
||||||
|
const duration = Math.max(endHour - startHour, 0.5); // mínimo 30 min
|
||||||
|
|
||||||
|
const top = startHour * 60; // 60px por hora
|
||||||
|
const height = duration * 60;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: `${top}px`,
|
||||||
|
height: `${height}px`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentTimeTop(): number {
|
||||||
|
const now = new Date();
|
||||||
|
const currentHour = now.getHours() + now.getMinutes() / 60;
|
||||||
|
return currentHour * 60; // 60px por hora
|
||||||
|
}
|
||||||
|
|
||||||
|
isSameDay(date1: Date, date2: Date): boolean {
|
||||||
|
return date1.getDate() === date2.getDate() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getFullYear() === date2.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectTimeSlot(day: Date, hour: number): void {
|
||||||
|
const dateStr = day.toISOString().split('T')[0];
|
||||||
|
const hourStr = String(hour).padStart(2, '0') + ':00';
|
||||||
|
this.router.navigate(['/reservas/nuevo'], {
|
||||||
|
queryParams: {
|
||||||
|
fecha: dateStr,
|
||||||
|
hora: hourStr,
|
||||||
|
tipo: this.selectedFilter() !== 'all' ? this.selectedFilter() : 'room'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,272 @@
|
|||||||
|
/* Backdrop para centrar la tarjeta en la pantalla */
|
||||||
|
.form-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--bg-page, #F5F4F1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tarjeta estilo diálogo Google Calendar */
|
||||||
|
.dialog-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 520px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 28px;
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid rgba(15, 13, 10, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cabecera del Diálogo */
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px 16px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
background-color: var(--bg-page, #F5F4F1);
|
||||||
|
color: var(--text-primary, #0F0D0A);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cuerpo del Diálogo */
|
||||||
|
.dialog-body {
|
||||||
|
padding: 0 28px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input de Título Borderless */
|
||||||
|
.title-input-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid rgba(15, 13, 10, 0.08);
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 0;
|
||||||
|
outline: none;
|
||||||
|
color: var(--text-primary, #0F0D0A);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input:focus {
|
||||||
|
border-color: var(--gold-luxe, #C9973C);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pestañas / Tipo de Evento */
|
||||||
|
.tab-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
background: var(--bg-page, #F5F4F1);
|
||||||
|
border: none;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #44403C);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background-color: rgba(201, 151, 60, 0.12);
|
||||||
|
color: var(--gold-dark, #A67C28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover:not(.active) {
|
||||||
|
background-color: rgba(15, 13, 10, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filas con Icono */
|
||||||
|
.dialog-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted, #78716C);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Time Grid */
|
||||||
|
.time-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-cols: 1.5fr 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.time-grid {
|
||||||
|
grid-template-cols: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs del Diálogo */
|
||||||
|
.dialog-input, .dialog-select {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(15, 13, 10, 0.14);
|
||||||
|
background: #FFFFFF;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-input:focus, .dialog-select:focus {
|
||||||
|
border-color: var(--gold-luxe, #C9973C);
|
||||||
|
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(15, 13, 10, 0.14);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-textarea:focus {
|
||||||
|
border-color: var(--gold-luxe, #C9973C);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sección Asistente IA */
|
||||||
|
.ai-assistant-container {
|
||||||
|
background: rgba(201, 151, 60, 0.05);
|
||||||
|
border: 1px dashed rgba(201, 151, 60, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid rgba(15, 13, 10, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-textarea:focus {
|
||||||
|
border-color: var(--gold-luxe, #C9973C);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-assistant-btn {
|
||||||
|
background: linear-gradient(135deg, #C9973C, #A67C28);
|
||||||
|
color: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: fit-content;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pie del Diálogo */
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 28px 24px;
|
||||||
|
border-top: 1px solid rgba(15, 13, 10, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-more-options {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--gold-dark, #A67C28);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-more-options:hover {
|
||||||
|
background-color: var(--bg-page, #F5F4F1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background: #0D1B2A;
|
||||||
|
color: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:hover {
|
||||||
|
background-color: #1A3047;
|
||||||
|
}
|
||||||
@ -1,207 +1,151 @@
|
|||||||
<div class="min-h-screen bg-[#F5F4F1] text-[#0F0D0A] font-sans p-6 flex items-center justify-center overflow-y-auto">
|
<div class="form-container">
|
||||||
<div class="w-full max-w-4xl bg-white border border-[#0F0D0A]/10 rounded-3xl shadow-xl overflow-hidden flex flex-col md:flex-row">
|
<div class="dialog-card animate-fadeIn">
|
||||||
|
|
||||||
<!-- Lado Izquierdo: Información y Asistente IA -->
|
<!-- Cabecera del diálogo con botón de cerrar -->
|
||||||
<div class="w-full md:w-[40%] bg-[#0D1B2A] text-white p-8 flex flex-col justify-between relative">
|
<header class="dialog-header">
|
||||||
<div class="flex flex-col gap-6">
|
<a [routerLink]="['/reservas/calendario']" class="btn-close" title="Cerrar">
|
||||||
<div class="flex items-center gap-3">
|
<span class="material-icons">close</span>
|
||||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-[#C9973C] to-[#A67C28] flex items-center justify-center shadow-md">
|
</a>
|
||||||
<span class="material-icons text-white">smart_toy</span>
|
</header>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-bold tracking-tight text-white">Asistente IA</h2>
|
|
||||||
<p class="text-[10px] uppercase tracking-wider text-[#C9973C] font-semibold">Kinetix Intelligent Suite</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-2xl font-bold tracking-tight mt-4 text-white">
|
<!-- Formulario Principal -->
|
||||||
Agenda con lenguaje natural
|
<form [formGroup]="bookingForm" (ngSubmit)="onSubmit()" class="dialog-body">
|
||||||
</h3>
|
|
||||||
<p class="text-xs text-slate-300 leading-relaxed">
|
|
||||||
Escribe lo que necesitas y nuestro asistente de inteligencia artificial pre-llenará el formulario por ti en segundos.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Input de Lenguaje Natural IA -->
|
<!-- Fila 1: Añadir Título -->
|
||||||
<div class="flex flex-col gap-3 mt-6">
|
<div class="title-input-container">
|
||||||
<label class="text-[10px] uppercase tracking-wider text-slate-400 font-bold">Prompt de IA</label>
|
<input
|
||||||
<div class="relative">
|
type="text"
|
||||||
<textarea
|
formControlName="title"
|
||||||
#promptInput
|
placeholder="Añade un título"
|
||||||
rows="4"
|
class="title-input" />
|
||||||
placeholder="Ej: Necesito reservar una sala para 6 personas mañana a las 3:00 pm con proyector..."
|
@if (bookingForm.get('title')?.touched && bookingForm.get('title')?.invalid) {
|
||||||
class="w-full p-4 rounded-2xl bg-white/5 border border-white/10 text-white text-xs placeholder-slate-500 outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all resize-none"></textarea>
|
<span style="font-size: 10px; color: #EA4335; font-weight: 600; margin-top: 4px; display: block;">
|
||||||
</div>
|
El título es obligatorio.
|
||||||
|
</span>
|
||||||
<button
|
}
|
||||||
(click)="processAiPrompt(promptInput.value)"
|
|
||||||
[disabled]="isLoadingIa()"
|
|
||||||
class="w-full flex items-center justify-center gap-2 h-11 rounded-full bg-gradient-to-r from-[#C9973C] to-[#A67C28] text-white font-bold uppercase text-[10px] tracking-widest shadow-lg shadow-[#A67C28]/25 hover:translate-y-[-2px] transition-all disabled:opacity-50 disabled:translate-y-0">
|
|
||||||
@if (isLoadingIa()) {
|
|
||||||
<span>Procesando...</span>
|
|
||||||
} @else {
|
|
||||||
<span class="material-icons text-xs">auto_awesome</span>
|
|
||||||
<span>Autocompletar Formulario</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pie de Asistente -->
|
<!-- Fila 2: Pestañas de tipo (Estilo Google) -->
|
||||||
<div class="mt-8 text-[10px] text-slate-400 border-t border-white/5 pt-4">
|
<div class="tab-selector">
|
||||||
Diseñado bajo el estándar de IA Corporativa de Kinetix.
|
<button type="button" class="tab-btn active">Evento</button>
|
||||||
|
<button type="button" class="tab-btn">Tarea</button>
|
||||||
|
<button type="button" class="tab-btn">Fuera de la oficina</button>
|
||||||
|
<button type="button" class="tab-btn">Agenda de citas</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lado Derecho: Formulario Estructurado -->
|
<!-- Mensaje de éxito de pre-llenado de la IA -->
|
||||||
<form [formGroup]="bookingForm" (ngSubmit)="onSubmit()" class="w-full md:w-[60%] p-8 flex flex-col gap-6">
|
|
||||||
<header class="flex justify-between items-center shrink-0">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-bold tracking-tight text-[#0F0D0A]">Detalles de la Reserva</h2>
|
|
||||||
<p class="text-xs text-[#78716C]">Confirma los datos para agendar tu espacio o activo</p>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
[routerLink]="['/reservas/calendario']"
|
|
||||||
class="text-xs text-[#78716C] hover:text-[#C9973C] transition-colors font-medium flex items-center gap-1">
|
|
||||||
← Volver al Calendario
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Mensaje de éxito de la IA -->
|
|
||||||
@if (aiSuccessMessage()) {
|
@if (aiSuccessMessage()) {
|
||||||
<div class="p-3 bg-emerald-50 text-emerald-800 border border-emerald-200 rounded-xl text-xs flex items-center gap-2 animate-fadeIn">
|
<div style="padding: 10px 12px; background: rgba(52, 168, 83, 0.08); color: #137333; border: 1.5px solid rgba(52, 168, 83, 0.2); border-radius: 8px; font-size: 12px; display: flex; align-items: center; gap: 8px; font-weight: 600;">
|
||||||
<span class="material-icons text-sm">check_circle</span>
|
<span class="material-icons" style="font-size: 16px;">check_circle</span>
|
||||||
<span>{{ aiSuccessMessage() }}</span>
|
<span>{{ aiSuccessMessage() }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<!-- Fila 3: Horarios (Icono de Reloj) -->
|
||||||
<!-- Título de la reserva -->
|
<div class="dialog-row">
|
||||||
<div class="flex flex-col gap-1.5">
|
<span class="material-icons row-icon">schedule</span>
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Asunto / Título de la Reunión</label>
|
<div class="row-content">
|
||||||
<input
|
<label class="row-label">Fecha y Horarios</label>
|
||||||
type="text"
|
<div class="time-grid">
|
||||||
formControlName="title"
|
<input type="date" formControlName="date" class="dialog-input" />
|
||||||
placeholder="Ej: Sprint Planning Q3"
|
<input type="time" formControlName="startTime" class="dialog-input" />
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all" />
|
<input type="time" formControlName="endTime" class="dialog-input" />
|
||||||
@if (bookingForm.get('title')?.touched && bookingForm.get('title')?.invalid) {
|
</div>
|
||||||
<span class="text-[10px] text-red-500 font-semibold">El título es requerido (mínimo 3 caracteres)</span>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<!-- Fila 4: Asistente IA (Icono de Robot / Smart Toy) -->
|
||||||
<!-- Tipo de Recurso -->
|
<div class="dialog-row">
|
||||||
<div class="flex flex-col gap-1.5">
|
<span class="material-icons row-icon" style="color: var(--gold-dark);">smart_toy</span>
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Tipo de Recurso</label>
|
<div class="row-content">
|
||||||
<select
|
<div class="ai-assistant-container">
|
||||||
formControlName="resourceType"
|
<span class="row-label" style="color: var(--gold-dark); font-weight: bold; display: flex; align-items: center; gap: 4px;">
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none bg-white focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all">
|
<span>Asistente Inteligente Kinetix</span>
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
#promptInput
|
||||||
|
rows="2"
|
||||||
|
placeholder="Escribe: Reserva sala Alpha para 5 personas mañana a las 11:30 am..."
|
||||||
|
class="ai-assistant-textarea"></textarea>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="processAiPrompt(promptInput.value)"
|
||||||
|
[disabled]="isLoadingIa()"
|
||||||
|
class="ai-assistant-btn">
|
||||||
|
@if (isLoadingIa()) {
|
||||||
|
<span>Procesando...</span>
|
||||||
|
} @else {
|
||||||
|
<span class="material-icons" style="font-size: 11px;">auto_awesome</span>
|
||||||
|
<span>Autocompletar</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fila 5: Recursos / Ubicación (Icono de Pin de Ubicación) -->
|
||||||
|
<div class="dialog-row">
|
||||||
|
<span class="material-icons row-icon">place</span>
|
||||||
|
<div class="row-content">
|
||||||
|
<label class="row-label">Ubicación y Recurso</label>
|
||||||
|
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<select formControlName="resourceType" class="dialog-select" style="flex: 1; min-width: 140px;">
|
||||||
<option value="room">Sala de Juntas</option>
|
<option value="room">Sala de Juntas</option>
|
||||||
<option value="desk">Escritorio / Oficina</option>
|
<option value="desk">Escritorio / Oficina</option>
|
||||||
<option value="asset">Activo / Dispositivo</option>
|
<option value="asset">Activo / Dispositivo</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nombre del Recurso -->
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Nombre del Recurso / Espacio</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
formControlName="resourceName"
|
formControlName="resourceName"
|
||||||
placeholder="Ej: Sala Alpha, Escritorio 4B, Laptop"
|
placeholder="Ej: Sala Alpha, Escritorio 4B"
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all" />
|
class="dialog-input"
|
||||||
@if (bookingForm.get('resourceName')?.touched && bookingForm.get('resourceName')?.invalid) {
|
style="flex: 2; min-width: 180px;" />
|
||||||
<span class="text-[10px] text-red-500 font-semibold">El nombre del recurso es requerido</span>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<!-- Fila 6: Invitados y Extras (Icono de Grupos/Personas) -->
|
||||||
<!-- Fecha -->
|
<div class="dialog-row">
|
||||||
<div class="flex flex-col gap-1.5">
|
<span class="material-icons row-icon">group</span>
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Fecha</label>
|
<div class="row-content">
|
||||||
<input
|
<label class="row-label">Asistentes y Opciones</label>
|
||||||
type="date"
|
<div style="display: flex; align-items: center; gap: 24px; flex-wrap: wrap;">
|
||||||
formControlName="date"
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all" />
|
<span style="font-size: 12px; font-weight: 600;">Personas:</span>
|
||||||
@if (bookingForm.get('date')?.touched && bookingForm.get('date')?.invalid) {
|
<input type="number" formControlName="attendees" class="dialog-input" style="width: 70px;" min="1" />
|
||||||
<span class="text-[10px] text-red-500 font-semibold">Requerido</span>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hora Inicio -->
|
<label style="display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; cursor: pointer;">
|
||||||
<div class="flex flex-col gap-1.5">
|
<input type="checkbox" formControlName="hasProjector" style="width: 16px; height: 16px; cursor: pointer;" />
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Hora Inicio</label>
|
<span>Requiere Proyector</span>
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
formControlName="startTime"
|
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all" />
|
|
||||||
@if (bookingForm.get('startTime')?.touched && bookingForm.get('startTime')?.invalid) {
|
|
||||||
<span class="text-[10px] text-red-500 font-semibold">Requerido</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hora Fin -->
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Hora Fin</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
formControlName="endTime"
|
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all" />
|
|
||||||
@if (bookingForm.get('endTime')?.touched && bookingForm.get('endTime')?.invalid) {
|
|
||||||
<span class="text-[10px] text-red-500 font-semibold">Requerido</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<!-- Número Asistentes -->
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Capacidad / Personas</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
formControlName="attendees"
|
|
||||||
class="h-11 px-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all" />
|
|
||||||
@if (bookingForm.get('attendees')?.touched && bookingForm.get('attendees')?.invalid) {
|
|
||||||
<span class="text-[10px] text-red-500 font-semibold">Requerido (mínimo 1)</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Equipamiento Adicional (ej: Proyector) -->
|
|
||||||
<div class="flex items-center gap-3 h-11 mt-4">
|
|
||||||
<input
|
|
||||||
id="hasProjector"
|
|
||||||
type="checkbox"
|
|
||||||
formControlName="hasProjector"
|
|
||||||
class="w-4 h-4 rounded text-[#C9973C] border-[#0F0D0A]/10 focus:ring-[#C9973C]" />
|
|
||||||
<label for="hasProjector" class="text-xs font-semibold text-[#0F0D0A] cursor-pointer">
|
|
||||||
¿Requiere proyector / pantalla?
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Notas / Comentarios -->
|
<!-- Fila 7: Descripción (Icono de Notas/Texto) -->
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="dialog-row">
|
||||||
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Notas Adicionales</label>
|
<span class="material-icons row-icon">notes</span>
|
||||||
|
<div class="row-content">
|
||||||
|
<label class="row-label">Descripción</label>
|
||||||
<textarea
|
<textarea
|
||||||
formControlName="notes"
|
formControlName="notes"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="Comentarios adicionales o solicitudes especiales..."
|
placeholder="Añadir descripción o especificaciones adicionales..."
|
||||||
class="p-4 rounded-xl border border-[#0F0D0A]/10 text-xs outline-none focus:border-[#C9973C] focus:ring-1 focus:ring-[#C9973C] transition-all resize-none"></textarea>
|
class="dialog-textarea"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 mt-4 shrink-0">
|
<!-- Pie de página con botones de guardar -->
|
||||||
<a
|
<footer class="dialog-footer">
|
||||||
[routerLink]="['/reservas/calendario']"
|
<button type="button" [routerLink]="['/reservas/calendario']" class="btn-more-options">
|
||||||
class="flex-1 flex items-center justify-center h-12 rounded-full border border-[#0F0D0A]/10 text-xs font-semibold hover:bg-[#F5F4F1] transition-colors">
|
Más opciones
|
||||||
Cancelar
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="flex-1 flex items-center justify-center h-12 rounded-full bg-gradient-to-r from-[#C9973C] to-[#A67C28] text-white font-bold uppercase text-[10px] tracking-widest shadow-lg shadow-[#A67C28]/25 hover:translate-y-[-2px] transition-all">
|
|
||||||
Confirmar Reserva
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button type="submit" class="btn-save">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { IaParserService, IaParsedBooking } from '../services/ia-parser.service'
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ReactiveFormsModule, RouterModule],
|
imports: [CommonModule, ReactiveFormsModule, RouterModule],
|
||||||
templateUrl: './reserva-form.component.html',
|
templateUrl: './reserva-form.component.html',
|
||||||
styleUrls: []
|
styleUrls: ['./reserva-form.component.css']
|
||||||
})
|
})
|
||||||
export class ReservaFormComponent implements OnInit {
|
export class ReservaFormComponent implements OnInit {
|
||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-black text-white antialiased">
|
<body class="bg-black text-white antialiased">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user