feat: implement core frontend architecture with authentication, dashboard, and AI-powered resource booking modules

This commit is contained in:
Jessica Orozco 2026-06-16 14:46:23 -04:00
parent 97349f05be
commit 8063a6c0b6
29 changed files with 1681 additions and 1407 deletions

6
.env
View File

@ -1,4 +1,4 @@
# Kinetix Intranet - Environment Variables Template # App Intranet - Environment Variables Template
# ========================================== # ==========================================
# BACKEND (NestJS) CONFIGURATION # BACKEND (NestJS) CONFIGURATION
@ -7,7 +7,7 @@ PORT=3000
NODE_ENV=development NODE_ENV=development
# Database Settings (PostgreSQL / Supabase) # Database Settings (PostgreSQL / Supabase)
DATABASE_URL="postgresql://admin:supersecretpassword123@localhost:5432/kinetix_db?schema=public" DATABASE_URL="postgresql://admin:supersecretpassword123@localhost:5432/App_db?schema=public"
# SUPABASE_URL="https://your-project.supabase.co" # SUPABASE_URL="https://your-project.supabase.co"
# SUPABASE_KEY="your-anon-key" # SUPABASE_KEY="your-anon-key"
@ -16,7 +16,7 @@ AD_TENANT_ID="your-azure-tenant-id"
AD_CLIENT_ID="your-azure-client-id" AD_CLIENT_ID="your-azure-client-id"
AD_CLIENT_SECRET="your-azure-client-secret" AD_CLIENT_SECRET="your-azure-client-secret"
AD_REDIRECT_URI="http://localhost:3000/api/auth/callback" AD_REDIRECT_URI="http://localhost:3000/api/auth/callback"
JWT_SECRET="kinetix-super-jwt-secret-key-2026" JWT_SECRET="App-super-jwt-secret-key-2026"
# ========================================== # ==========================================
# FRONTEND (Angular) CONFIGURATION # FRONTEND (Angular) CONFIGURATION

View File

@ -1,4 +1,4 @@
# Kinetix Intranet - Environment Variables Template # App Intranet - Environment Variables Template
# ========================================== # ==========================================
# BACKEND (NestJS) CONFIGURATION # BACKEND (NestJS) CONFIGURATION
@ -7,7 +7,7 @@ PORT=3000
NODE_ENV=development NODE_ENV=development
# Database Settings (PostgreSQL / Supabase) # Database Settings (PostgreSQL / Supabase)
DATABASE_URL="postgresql://admin:supersecretpassword123@localhost:5432/kinetix_db?schema=public" DATABASE_URL="postgresql://admin:supersecretpassword123@localhost:5432/App_db?schema=public"
# SUPABASE_URL="https://your-project.supabase.co" # SUPABASE_URL="https://your-project.supabase.co"
# SUPABASE_KEY="your-anon-key" # SUPABASE_KEY="your-anon-key"
@ -16,7 +16,7 @@ AD_TENANT_ID="your-azure-tenant-id"
AD_CLIENT_ID="your-azure-client-id" AD_CLIENT_ID="your-azure-client-id"
AD_CLIENT_SECRET="your-azure-client-secret" AD_CLIENT_SECRET="your-azure-client-secret"
AD_REDIRECT_URI="http://localhost:3000/api/auth/callback" AD_REDIRECT_URI="http://localhost:3000/api/auth/callback"
JWT_SECRET="kinetix-super-jwt-secret-key-2026" JWT_SECRET="App-super-jwt-secret-key-2026"
# ========================================== # ==========================================
# FRONTEND (Angular) CONFIGURATION # FRONTEND (Angular) CONFIGURATION

View File

@ -14,7 +14,7 @@
## 2. Estructura del Monorepo ## 2. Estructura del Monorepo
``` ```
Kinetix/ App/
├── AGENT.md ← System Prompt principal e instrucciones de onboarding (no borrar) ├── AGENT.md ← System Prompt principal e instrucciones de onboarding (no borrar)
├── package.json ← Scripts del monorepo ├── package.json ← Scripts del monorepo
@ -77,7 +77,7 @@ Levanta el contenedor de PostgreSQL para desarrollo local:
```bash ```bash
docker-compose up -d docker-compose up -d
``` ```
Esto creará la base de datos `kinetix_db` expuesta en `localhost:5432` y pgAdmin en `http://localhost:5050` con las credenciales configuradas en el archivo `.env`. Esto creará la base de datos `App_db` expuesta en `localhost:5432` y pgAdmin en `http://localhost:5050` con las credenciales configuradas en el archivo `.env`.
--- ---

View File

@ -1,4 +1,4 @@
# Kinetix Intranet - Environment Variables Template # App Intranet - Environment Variables Template
# ========================================== # ==========================================
# BACKEND (NestJS) CONFIGURATION # BACKEND (NestJS) CONFIGURATION
@ -7,7 +7,7 @@ PORT=3000
NODE_ENV=development NODE_ENV=development
# Database Settings (PostgreSQL / Supabase) # Database Settings (PostgreSQL / Supabase)
DATABASE_URL="postgresql://admin:supersecretpassword123@localhost:5432/kinetix_db?schema=public" DATABASE_URL="postgresql://admin:supersecretpassword123@localhost:5432/App_db?schema=public"
# SUPABASE_URL="https://your-project.supabase.co" # SUPABASE_URL="https://your-project.supabase.co"
# SUPABASE_KEY="your-anon-key" # SUPABASE_KEY="your-anon-key"
@ -16,7 +16,7 @@ AD_TENANT_ID="your-azure-tenant-id"
AD_CLIENT_ID="your-azure-client-id" AD_CLIENT_ID="your-azure-client-id"
AD_CLIENT_SECRET="your-azure-client-secret" AD_CLIENT_SECRET="your-azure-client-secret"
AD_REDIRECT_URI="http://localhost:3000/api/auth/callback" AD_REDIRECT_URI="http://localhost:3000/api/auth/callback"
JWT_SECRET="kinetix-super-jwt-secret-key-2026" JWT_SECRET="App-super-jwt-secret-key-2026"
# ========================================== # ==========================================
# FRONTEND (Angular) CONFIGURATION # FRONTEND (Angular) CONFIGURATION

View File

@ -1,5 +1,5 @@
{ {
"name": "kinetix-backend", "name": "App-backend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -26,4 +26,4 @@
"@types/passport-jwt": "^4.0.0", "@types/passport-jwt": "^4.0.0",
"typescript": "^5.0.0" "typescript": "^5.0.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
# Propuesta Técnica y Comercial # Propuesta Técnica y Comercial
## Plataforma de Intranet Corporativa · **Kinetix** ## Plataforma de Intranet Corporativa · **App**
--- ---
@ -12,12 +12,12 @@
## Resumen Ejecutivo ## 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. **App** 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. 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] > [!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]. > App 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].
--- ---
@ -33,17 +33,17 @@ Las empresas modernas operan con herramientas fragmentadas: un sistema para rese
| Aprobaciones por correo | Sin trazabilidad, cuellos de botella operativos | | Aprobaciones por correo | Sin trazabilidad, cuellos de botella operativos |
| Directorio desactualizado | Información incorrecta, baja colaboración | | Directorio desactualizado | Información incorrecta, baja colaboración |
**Kinetix resuelve todos estos problemas en una sola plataforma integrada.** **App resuelve todos estos problemas en una sola plataforma integrada.**
--- ---
## 2. La Solución: Plataforma Kinetix ## 2. La Solución: Plataforma App
### 2.1 Visión General ### 2.1 Visión General
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
KINETIX INTRANET │ App INTRANET │
│ "Un ecosistema. Todos los procesos internos en uno." │ │ "Un ecosistema. Todos los procesos internos en uno." │
├──────────────┬──────────────┬──────────────┬────────────────┤ ├──────────────┬──────────────┬──────────────┬────────────────┤
│ Directorio │ Espacios │ Workflows │ Info Hub CMS │ │ Directorio │ Espacios │ Workflows │ Info Hub CMS │
@ -115,14 +115,14 @@ Usuario → App Angular → Azure Entra ID (login)
Acceso según rol: employee | manager | admin 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. **Ninguna contraseña se almacena en App.** La autenticación es 100% delegada a Active Directory de la empresa.
--- ---
## 4. Arquitectura del Sistema ## 4. Arquitectura del Sistema
``` ```
kinetix/ App/
├── frontend/ ← SPA Angular 18 (Standalone Components + Signals) ├── frontend/ ← SPA Angular 18 (Standalone Components + Signals)
│ └── src/app/ │ └── src/app/
│ ├── core/ ← Guards SSO, interceptores HTTP, auth services │ ├── core/ ← Guards SSO, interceptores HTTP, auth services
@ -147,7 +147,7 @@ kinetix/
## 5. Diseño e Interfaz de Usuario ## 5. Diseño e Interfaz de Usuario
Kinetix utiliza el **Executive Suite Design System**, un sistema de diseño corporativo premium desarrollado a medida: App 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` - **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 - **Tarjetas Joya (Blackjack System):** Fondos oscuros en Navy, Plum, Viridian, Espresso para los módulos principales
@ -227,7 +227,7 @@ Kinetix utiliza el **Executive Suite Design System**, un sistema de diseño corp
| Documentación técnica de operación | Runbook de DevOps | | Documentación técnica de operación | Runbook de DevOps |
| Soporte post-lanzamiento (30 días incluidos) | Canal de soporte dedicado | | Soporte post-lanzamiento (30 días incluidos) | Canal de soporte dedicado |
**Hito:** 🚀 Go-Live oficial de Kinetix para [Empresa]. **Hito:** 🚀 Go-Live oficial de App para [Empresa].
--- ---
@ -235,7 +235,7 @@ Kinetix utiliza el **Executive Suite Design System**, un sistema de diseño corp
```mermaid ```mermaid
gantt gantt
title Roadmap de Implementación Kinetix title Roadmap de Implementación App
dateFormat YYYY-MM-DD dateFormat YYYY-MM-DD
section Fase 0 — Diagnóstico section Fase 0 — Diagnóstico
Kickoff & Auditoría AD :2026-07-01, 7d Kickoff & Auditoría AD :2026-07-01, 7d
@ -266,7 +266,7 @@ gantt
| Concepto | Descripción | Estimado | | Concepto | Descripción | Estimado |
|---|---|---| |---|---|---|
| **Licencia de Plataforma** | Uso de la plataforma Kinetix base | A negociar | | **Licencia de Plataforma** | Uso de la plataforma App base | A negociar |
| **Implementación e Integración** | Fases 01: Setup, SSO, infraestructura | 46 semanas | | **Implementación e Integración** | Fases 01: Setup, SSO, infraestructura | 46 semanas |
| **Desarrollo de Módulos Core** | Fase 2: Directorio, Espacios, Workflow | 6 semanas | | **Desarrollo de Módulos Core** | Fase 2: Directorio, Espacios, Workflow | 6 semanas |
| **Módulos Secundarios** | Fase 3: CMS, Ticketing | 4 semanas | | **Módulos Secundarios** | Fase 3: CMS, Ticketing | 4 semanas |
@ -282,9 +282,9 @@ gantt
--- ---
## 10. Por Qué Kinetix sobre Alternativas ## 10. Por Qué App sobre Alternativas
| Criterio | Kinetix | SharePoint | Confluence | Desarrollo a medida desde cero | | Criterio | App | SharePoint | Confluence | Desarrollo a medida desde cero |
|---|---|---|---|---| |---|---|---|---|---|
| Integración AD nativa | ✅ | ✅ | ⚠️ Plugin | ✅ | | Integración AD nativa | ✅ | ✅ | ⚠️ Plugin | ✅ |
| Personalizable 100% | ✅ | ⚠️ Limitado | ⚠️ Limitado | ✅ | | Personalizable 100% | ✅ | ⚠️ Limitado | ⚠️ Limitado | ✅ |
@ -321,8 +321,8 @@ gantt
**Jess Miller** **Jess Miller**
Arquitecta de Software Senior · Full-Stack Engineer Arquitecta de Software Senior · Full-Stack Engineer
📧 jess@kinetix.dev 📧 jess@App.dev
🔗 github.com/kinetix 🔗 github.com/App
--- ---

View File

@ -1,20 +1,20 @@
# Walkthrough: Módulo de Autogestión de Espacios y Activos con Asistente IA # 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). Se ha completado la arquitectura e implementación del módulo de gestión de reservas en el frontend de App utilizando Angular 18 (Standalone Components, Signals y enrutamiento perezoso).
## Cambios Realizados ## Cambios Realizados
### Servicio de IA ### 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. - [ia-parser.service.ts](file:///d:/Jess/agent/App/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 ### 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.ts](file:///d:/Jess/agent/App/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). - [calendar-booking.component.html](file:///d:/Jess/agent/App/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.ts](file:///d:/Jess/agent/App/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. - [reserva-form.component.html](file:///d:/Jess/agent/App/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 ### 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`). - [app.routes.ts](file:///d:/Jess/agent/App/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`).
--- ---

View File

@ -3,12 +3,12 @@ version: '3.8'
services: services:
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: kinetix-postgres container_name: App-postgres
restart: always restart: always
environment: environment:
POSTGRES_USER: admin POSTGRES_USER: admin
POSTGRES_PASSWORD: supersecretpassword123 POSTGRES_PASSWORD: supersecretpassword123
POSTGRES_DB: kinetix_db POSTGRES_DB: App_db
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
@ -16,10 +16,10 @@ services:
pgadmin: pgadmin:
image: dpage/pgadmin4 image: dpage/pgadmin4
container_name: kinetix-pgadmin container_name: App-pgadmin
restart: always restart: always
environment: environment:
PGADMIN_DEFAULT_EMAIL: admin@kinetix.com PGADMIN_DEFAULT_EMAIL: admin@App.com
PGADMIN_DEFAULT_PASSWORD: adminpassword123 PGADMIN_DEFAULT_PASSWORD: adminpassword123
ports: ports:
- "5050:80" - "5050:80"

View File

@ -3,7 +3,7 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"kinetix-frontend": { "App-frontend": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@ -17,7 +17,7 @@
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "dist/kinetix-frontend", "outputPath": "dist/App-frontend",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "zone.js", "polyfills": "zone.js",
@ -55,10 +55,10 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "kinetix-frontend:build:production" "browserTarget": "App-frontend:build:production"
}, },
"development": { "development": {
"browserTarget": "kinetix-frontend:build:development" "browserTarget": "App-frontend:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

View File

@ -1,5 +1,5 @@
{ {
"name": "kinetix-frontend", "name": "App-frontend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -28,4 +28,4 @@
"@types/node": "^18.11.12", "@types/node": "^18.11.12",
"typescript": "~5.4.2" "typescript": "~5.4.2"
} }
} }

View File

@ -1,17 +1,17 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { RouterOutlet, Router } from '@angular/router'; import { RouterOutlet, Router, RouterLink } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [RouterOutlet, CommonModule], imports: [RouterOutlet, CommonModule, RouterLink],
styleUrls: ['./app.component.css'], styleUrls: ['./app.component.css'],
template: ` template: `
<main class="app-shell" [class.login-mode]="isLoginView()"> <main class="app-shell" [class.login-mode]="isLoginView()">
<header *ngIf="!isLoginView()" class="app-header"> <header *ngIf="!isLoginView()" class="app-header">
<div class="header-brand"> <a routerLink="/dashboard" class="header-brand" style="text-decoration: none; cursor: pointer;">
<div class="header-logo"> <div class="header-logo">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4L13 12L6 20" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 4L13 12L6 20" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
@ -20,12 +20,10 @@ import { CommonModule } from '@angular/common';
</svg> </svg>
</div> </div>
<div class="header-wordmark"> <div class="header-wordmark">
<a routerLink="/dashboard"> <span class="header-name">App</span>
<span class="header-name">Kinetix</span> <span class="header-sub">Intranet Corporativa</span>
<span class="header-sub">Intranet Corporativa</span>
</a>
</div> </div>
</div> </a>
<div class="header-meta">Consorcio · 2026</div> <div class="header-meta">Consorcio · 2026</div>
</header> </header>
@ -34,7 +32,7 @@ import { CommonModule } from '@angular/common';
</section> </section>
<footer *ngIf="!isLoginView()" class="app-footer"> <footer *ngIf="!isLoginView()" class="app-footer">
<span>© 2026 Kinetix</span> <span>© 2026 App</span>
<span class="footer-dot"></span> <span class="footer-dot"></span>
<span>Todos los derechos reservados</span> <span>Todos los derechos reservados</span>
<span class="footer-dot"></span> <span class="footer-dot"></span>

View File

@ -3,7 +3,7 @@ import { Router, CanActivateFn } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => { export const authGuard: CanActivateFn = (route, state) => {
const router = inject(Router); const router = inject(Router);
const token = localStorage.getItem('kinetix_token'); const token = localStorage.getItem('App_token');
if (token) { if (token) {
return true; return true;

View File

@ -10,13 +10,13 @@
<div class="login-logo-wrap"> <div class="login-logo-wrap">
<!-- K logotipo SVG --> <!-- K logotipo SVG -->
<svg class="login-logo-icon" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="login-logo-icon" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 6L17 17L8 28" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8 6L17 17L8 28" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M17 6H26" stroke="white" stroke-width="3.5" stroke-linecap="round"/> <path d="M17 6H26" stroke="white" stroke-width="3.5" stroke-linecap="round" />
<path d="M17 28H26" stroke="white" stroke-width="3.5" stroke-linecap="round"/> <path d="M17 28H26" stroke="white" stroke-width="3.5" stroke-linecap="round" />
</svg> </svg>
</div> </div>
<p class="login-eyebrow">Portal Corporativo</p> <p class="login-eyebrow">Portal Corporativo</p>
<h1 class="login-title">KINETIX <span class="login-title-gold">ECO</span></h1> <h1 class="login-title">App <span class="login-title-gold">ECO</span></h1>
<p class="login-sub">Consorcio Empresarial · Intranet 2026</p> <p class="login-sub">Consorcio Empresarial · Intranet 2026</p>
</div> </div>
@ -28,67 +28,55 @@
<div class="field"> <div class="field">
<label class="field-lbl"> <label class="field-lbl">
<svg class="field-lbl-icon" viewBox="0 0 16 16" fill="none"> <svg class="field-lbl-icon" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="5" r="3" stroke="currentColor" stroke-width="1.5"/> <circle cx="8" cy="5" r="3" stroke="currentColor" stroke-width="1.5" />
<path d="M2 14c0-3.314 2.686-6 6-6s6 2.686 6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M2 14c0-3.314 2.686-6 6-6s6 2.686 6 6" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" />
</svg> </svg>
Usuario corporativo Usuario corporativo
</label> </label>
<input <input id="login-email" type="email" class="field-input" placeholder="nombre@App.com" [value]="email()"
id="login-email" (input)="email.set(getVal($event))" autocomplete="email" />
type="email"
class="field-input"
placeholder="nombre@kinetix.com"
[value]="email()"
(input)="email.set(getVal($event))"
autocomplete="email"
/>
</div> </div>
<div class="field"> <div class="field">
<label class="field-lbl"> <label class="field-lbl">
<svg class="field-lbl-icon" viewBox="0 0 16 16" fill="none"> <svg class="field-lbl-icon" viewBox="0 0 16 16" fill="none">
<rect x="3" y="7" width="10" height="8" rx="2" stroke="currentColor" stroke-width="1.5"/> <rect x="3" y="7" width="10" height="8" rx="2" stroke="currentColor" stroke-width="1.5" />
<path d="M5.5 7V5a2.5 2.5 0 015 0v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M5.5 7V5a2.5 2.5 0 015 0v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<circle cx="8" cy="11" r="1" fill="currentColor"/> <circle cx="8" cy="11" r="1" fill="currentColor" />
</svg> </svg>
Contraseña Contraseña
</label> </label>
<input <input id="login-password" type="password" class="field-input" placeholder="••••••••••" [value]="password()"
id="login-password" (input)="password.set(getVal($event))" autocomplete="current-password" />
type="password"
class="field-input"
placeholder="••••••••••"
[value]="password()"
(input)="password.set(getVal($event))"
autocomplete="current-password"
/>
</div> </div>
<button id="btn-login" type="submit" class="btn-cta" [disabled]="loading()"> <button id="btn-login" type="submit" class="btn-cta" [disabled]="loading()">
@if (loading()) { @if (loading()) {
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="animation: spin 0.8s linear infinite;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="animation: spin 0.8s linear infinite;">
<circle cx="12" cy="12" r="10" stroke="rgba(255,255,255,0.3)" stroke-width="3"/> <circle cx="12" cy="12" r="10" stroke="rgba(255,255,255,0.3)" stroke-width="3" />
<path d="M12 2a10 10 0 0110 10" stroke="#fff" stroke-width="3" stroke-linecap="round"/> <path d="M12 2a10 10 0 0110 10" stroke="#fff" stroke-width="3" stroke-linecap="round" />
</svg> </svg>
Autenticando... Autenticando...
} @else { } @else {
<svg width="14" height="14" viewBox="0 0 20 20" fill="none"> <svg width="14" height="14" viewBox="0 0 20 20" fill="none">
<path d="M10 3v14M3 10l7 7 7-7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="transform:rotate(-90deg);transform-origin:center"/> <path d="M10 3v14M3 10l7 7 7-7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
</svg> stroke-linejoin="round" style="transform:rotate(-90deg);transform-origin:center" />
Ingresar a la Intranet </svg>
Ingresar a la Intranet
} }
</button> </button>
</form> </form>
<!-- Error --> <!-- Error -->
@if (errorMsg()) { @if (errorMsg()) {
<div class="login-error" role="alert"> <div class="login-error" role="alert">
<svg viewBox="0 0 16 16" fill="none"> <svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/> <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5" />
<path d="M8 5v3M8 10.5h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M8 5v3M8 10.5h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg> </svg>
{{ errorMsg() }} {{ errorMsg() }}
</div> </div>
} }
<!-- SSO Divider --> <!-- SSO Divider -->
@ -102,25 +90,28 @@
<button id="btn-sso" type="button" class="btn-sso" (click)="loginSSO()" [disabled]="loading()"> <button id="btn-sso" type="button" class="btn-sso" (click)="loginSSO()" [disabled]="loading()">
<!-- Microsoft logo --> <!-- Microsoft logo -->
<svg class="ms-logo" viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg"> <svg class="ms-logo" viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="10.5" height="10.5" fill="#f25022"/> <rect x="0" y="0" width="10.5" height="10.5" fill="#f25022" />
<rect x="12.5" y="0" width="10.5" height="10.5" fill="#7fba00"/> <rect x="12.5" y="0" width="10.5" height="10.5" fill="#7fba00" />
<rect x="0" y="12.5" width="10.5" height="10.5" fill="#00a4ef"/> <rect x="0" y="12.5" width="10.5" height="10.5" fill="#00a4ef" />
<rect x="12.5" y="12.5" width="10.5" height="10.5" fill="#ffb900"/> <rect x="12.5" y="12.5" width="10.5" height="10.5" fill="#ffb900" />
</svg> </svg>
<div class="sso-text"> <div class="sso-text">
<span class="sso-provider">Microsoft Entra ID</span> <span class="sso-provider">Microsoft Entra ID</span>
<span class="sso-hint">Active Directory SSO · Inicio único</span> <span class="sso-hint">Active Directory SSO · Inicio único</span>
</div> </div>
<svg class="sso-arr" viewBox="0 0 16 16" fill="none"> <svg class="sso-arr" viewBox="0 0 16 16" fill="none">
<path d="M3 8h10M9 5l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 8h10M9 5l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" />
</svg> </svg>
</button> </button>
<!-- Security footer --> <!-- Security footer -->
<div class="login-security"> <div class="login-security">
<svg viewBox="0 0 16 16" fill="none"> <svg viewBox="0 0 16 16" fill="none">
<path d="M8 1.5L13.5 4v4c0 3-2 5-5.5 6.5C2.5 13 .5 11 .5 8V4L8 1.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/> <path d="M8 1.5L13.5 4v4c0 3-2 5-5.5 6.5C2.5 13 .5 11 .5 8V4L8 1.5z" stroke="currentColor" stroke-width="1.2"
<path d="M5 8l2 2 4-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> stroke-linejoin="round" />
<path d="M5 8l2 2 4-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"
stroke-linejoin="round" />
</svg> </svg>
Acceso restringido · SSL/TLS · Políticas Entra ID Acceso restringido · SSL/TLS · Políticas Entra ID
</div> </div>
@ -130,6 +121,8 @@
<style> <style>
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
</style> </style>

View File

@ -12,10 +12,10 @@ import { CommonModule } from '@angular/common';
export class LoginComponent { export class LoginComponent {
private router = inject(Router); private router = inject(Router);
email = signal(''); email = signal('');
password = signal(''); password = signal('');
errorMsg = signal<string>(''); errorMsg = signal<string>('');
loading = signal(false); loading = signal(false);
getVal(event: Event): string { getVal(event: Event): string {
return (event.target as HTMLInputElement).value; return (event.target as HTMLInputElement).value;
@ -39,12 +39,12 @@ export class LoginComponent {
// Simulate authentication delay // Simulate authentication delay
setTimeout(() => { setTimeout(() => {
if (em.includes('admin') || em.includes('kinetix') || em.includes('@')) { if (em.includes('admin') || em.includes('App') || em.includes('@')) {
const userName = em.split('@')[0]; const userName = em.split('@')[0];
const displayName = userName.charAt(0).toUpperCase() + userName.slice(1); const displayName = userName.charAt(0).toUpperCase() + userName.slice(1);
localStorage.setItem('kinetix_token', 'local-mock-jwt-token-998822'); localStorage.setItem('App_token', 'local-mock-jwt-token-998822');
localStorage.setItem('kinetix_user', JSON.stringify({ localStorage.setItem('App_user', JSON.stringify({
name: displayName, name: displayName,
email: em, email: em,
role: 'Consortium Manager', role: 'Consortium Manager',
@ -63,10 +63,10 @@ export class LoginComponent {
this.loading.set(true); this.loading.set(true);
setTimeout(() => { setTimeout(() => {
localStorage.setItem('kinetix_token', 'ad-jwt-token-microsoft-entra-id-2026'); localStorage.setItem('App_token', 'ad-jwt-token-microsoft-entra-id-2026');
localStorage.setItem('kinetix_user', JSON.stringify({ localStorage.setItem('App_user', JSON.stringify({
name: 'Jess Miller', name: 'Jess Miller',
email: 'jess.miller@kinetix-consortium.com', email: 'jess.miller@App-consortium.com',
role: 'Chief Architect & Director', role: 'Chief Architect & Director',
avatar: 'JM' avatar: 'JM'
})); }));

View File

@ -307,7 +307,7 @@ interface UserProfile {
<span class="text-neon-cyan font-bold"></span> <span class="text-neon-cyan font-bold"></span>
<div> <div>
<strong class="text-white block text-sm">Social</strong> <strong class="text-white block text-sm">Social</strong>
<span class="text-xs text-slate-400">Posteo automatizado e inmediato en las redes de comunicación interna de Kinetix.</span> <span class="text-xs text-slate-400">Posteo automatizado e inmediato en las redes de comunicación interna de App.</span>
</div> </div>
</div> </div>
</div> </div>
@ -441,7 +441,7 @@ interface UserProfile {
<!-- SVG chart visualizer --> <!-- SVG chart visualizer -->
<div class="glass-panel p-6 rounded-2xl lg:col-span-2 flex flex-col gap-4"> <div class="glass-panel p-6 rounded-2xl lg:col-span-2 flex flex-col gap-4">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h3 class="text-sm font-bold uppercase tracking-widest text-slate-400">Proyección de Adopción Kinetix</h3> <h3 class="text-sm font-bold uppercase tracking-widest text-slate-400">Proyección de Adopción App</h3>
<span class="text-xs text-emerald-400 font-bold bg-emerald-500/10 px-2.5 py-0.5 rounded-full">ROI +85%</span> <span class="text-xs text-emerald-400 font-bold bg-emerald-500/10 px-2.5 py-0.5 rounded-full">ROI +85%</span>
</div> </div>
@ -503,13 +503,13 @@ interface UserProfile {
<!-- Incident response efficiency comparison (Slide 9) --> <!-- Incident response efficiency comparison (Slide 9) -->
<div class="glass-panel p-6 rounded-2xl flex flex-col gap-4"> <div class="glass-panel p-6 rounded-2xl flex flex-col gap-4">
<h3 class="text-sm font-bold uppercase tracking-widest text-slate-400">Eficiencia en Resolución de Incidencias</h3> <h3 class="text-sm font-bold uppercase tracking-widest text-slate-400">Eficiencia en Resolución de Incidencias</h3>
<p class="text-xs text-slate-400">Reducción drástica del tiempo de respuesta mediante el módulo de ticketing inteligente de Kinetix.</p> <p class="text-xs text-slate-400">Reducción drástica del tiempo de respuesta mediante el módulo de ticketing inteligente de App.</p>
<!-- Custom styled CSS bar chart --> <!-- Custom styled CSS bar chart -->
<div class="flex flex-col gap-4 mt-2"> <div class="flex flex-col gap-4 mt-2">
<!-- Kinetix V2 Bar --> <!-- App V2 Bar -->
<div class="flex flex-col md:flex-row md:items-center gap-2"> <div class="flex flex-col md:flex-row md:items-center gap-2">
<span class="w-32 text-xs font-bold text-white">Kinetix V2 (Actual)</span> <span class="w-32 text-xs font-bold text-white">App V2 (Actual)</span>
<div class="flex-grow bg-white/5 rounded-full overflow-hidden h-7 border border-white/5 relative"> <div class="flex-grow bg-white/5 rounded-full overflow-hidden h-7 border border-white/5 relative">
<div class="bg-gradient-to-r from-cyan-400 to-indigo-500 h-full rounded-full transition-all duration-1000" style="width: 26%"></div> <div class="bg-gradient-to-r from-cyan-400 to-indigo-500 h-full rounded-full transition-all duration-1000" style="width: 26%"></div>
<span class="absolute right-3 top-1 text-xs font-bold text-cyan-400">12 min</span> <span class="absolute right-3 top-1 text-xs font-bold text-cyan-400">12 min</span>
@ -587,10 +587,10 @@ interface UserProfile {
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex justify-between items-center text-xs p-3 rounded-lg bg-white/5 border border-white/5"> <div class="flex justify-between items-center text-xs p-3 rounded-lg bg-white/5 border border-white/5">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="font-bold text-white">Lanzamiento Kinetix V2 Intranet</span> <span class="font-bold text-white">Lanzamiento App V2 Intranet</span>
<span class="text-[10px] text-slate-400">Autor: Director de Tecnología (SSO AD)</span> <span class="text-[10px] text-slate-400">Autor: Director de Tecnología (SSO AD)</span>
</div> </div>
<button (click)="approveItem('Lanzamiento Kinetix V2')" class="px-3 py-1.5 rounded-lg bg-purple-600 text-white font-extrabold text-[10px] uppercase hover:bg-purple-500 transition"> <button (click)="approveItem('Lanzamiento App V2')" class="px-3 py-1.5 rounded-lg bg-purple-600 text-white font-extrabold text-[10px] uppercase hover:bg-purple-500 transition">
Aprobar y Postear Aprobar y Postear
</button> </button>
</div> </div>
@ -671,7 +671,7 @@ export class DashboardComponent implements OnInit {
// Sincronized Active Directory Active Profile signal // Sincronized Active Directory Active Profile signal
currentUser = signal<UserProfile>({ currentUser = signal<UserProfile>({
name: 'Invitado', name: 'Invitado',
email: 'guest@kinetix.com', email: 'guest@App.com',
role: 'Employee', role: 'Employee',
avatar: 'G' avatar: 'G'
}); });
@ -749,7 +749,7 @@ export class DashboardComponent implements OnInit {
ngOnInit() { ngOnInit() {
// Load Active Directory profile from local storage // Load Active Directory profile from local storage
const userStr = localStorage.getItem('kinetix_user'); const userStr = localStorage.getItem('App_user');
if (userStr) { if (userStr) {
try { try {
const user = JSON.parse(userStr); const user = JSON.parse(userStr);
@ -815,8 +815,8 @@ export class DashboardComponent implements OnInit {
} }
logout() { logout() {
localStorage.removeItem('kinetix_token'); localStorage.removeItem('App_token');
localStorage.removeItem('kinetix_user'); localStorage.removeItem('App_user');
this.router.navigate(['/login']); this.router.navigate(['/login']);
} }
} }

View File

@ -25,71 +25,6 @@ interface Employee {
template: ` template: `
<div class="dir-shell"> <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> &nbsp;|&nbsp; 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 PANEL ═══════════════════════════════ -->
<main class="dir-main"> <main class="dir-main">
@ -246,7 +181,6 @@ interface Employee {
export class DirectorioComponent implements OnInit { export class DirectorioComponent implements OnInit {
readonly router = inject(Router); readonly router = inject(Router);
sidebarCollapsed = signal(false);
contactToast = signal(''); contactToast = signal('');
searchQuery = ''; searchQuery = '';
@ -255,12 +189,6 @@ export class DirectorioComponent implements OnInit {
private _searchSignal = signal(''); private _searchSignal = signal('');
private _deptSignal = signal('ALL'); 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[]>([]); employees = signal<Employee[]>([]);
filteredEmployees = computed(() => { filteredEmployees = computed(() => {
@ -282,14 +210,6 @@ export class DirectorioComponent implements OnInit {
ngOnInit(): void { } 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 { onSearch(value: string): void {
this._searchSignal.set(value); this._searchSignal.set(value);
} }

View File

@ -24,16 +24,13 @@
<span class="mini-day-name">J</span> <span class="mini-day-name">J</span>
<span class="mini-day-name">V</span> <span class="mini-day-name">V</span>
<span class="mini-day-name">S</span> <span class="mini-day-name">S</span>
<!-- Celdas --> <!-- Celdas -->
@for (day of calendarDays().slice(0, 35); track day.getTime()) { @for (day of calendarDays().slice(0, 35); track day.getTime()) {
<span <span (click)="selectDay(day)" [class.today]="isToday(day)" [class.other-month]="!isCurrentMonth(day)"
(click)="selectDay(day)" class="mini-day-cell">
[class.today]="isToday(day)" {{ day.getDate() }}
[class.other-month]="!isCurrentMonth(day)" </span>
class="mini-day-cell">
{{ day.getDate() }}
</span>
} }
</div> </div>
</div> </div>
@ -43,19 +40,23 @@
<h4 class="filter-title">Categorías</h4> <h4 class="filter-title">Categorías</h4>
<div class="filter-list"> <div class="filter-list">
<label class="filter-item"> <label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'all'" (change)="setFilter('all')" class="filter-checkbox"> <input type="radio" name="resType" [checked]="selectedFilter() === 'all'" (change)="setFilter('all')"
class="filter-checkbox">
<span>Ver Todo</span> <span>Ver Todo</span>
</label> </label>
<label class="filter-item"> <label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'room'" (change)="setFilter('room')" class="filter-checkbox"> <input type="radio" name="resType" [checked]="selectedFilter() === 'room'" (change)="setFilter('room')"
class="filter-checkbox">
<span>Salas de Juntas</span> <span>Salas de Juntas</span>
</label> </label>
<label class="filter-item"> <label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'desk'" (change)="setFilter('desk')" class="filter-checkbox"> <input type="radio" name="resType" [checked]="selectedFilter() === 'desk'" (change)="setFilter('desk')"
class="filter-checkbox">
<span>Escritorios</span> <span>Escritorios</span>
</label> </label>
<label class="filter-item"> <label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'asset'" (change)="setFilter('asset')" class="filter-checkbox"> <input type="radio" name="resType" [checked]="selectedFilter() === 'asset'" (change)="setFilter('asset')"
class="filter-checkbox">
<span>Activos</span> <span>Activos</span>
</label> </label>
</div> </div>
@ -67,7 +68,7 @@
<header class="main-header"> <header class="main-header">
<div class="main-header-left"> <div class="main-header-left">
<div class="header-title-group"> <div class="header-title-group">
<div class="header-app-logo">KINETIX <span>Intranet</span></div> <div class="header-app-logo">App <span>Intranet</span></div>
</div> </div>
<button (click)="today()" class="btn-today">Hoy</button> <button (click)="today()" class="btn-today">Hoy</button>
<button (click)="prev()" class="nav-arrow">&lt;</button> <button (click)="prev()" class="nav-arrow">&lt;</button>
@ -85,159 +86,149 @@
<!-- Calendario Contenido --> <!-- Calendario Contenido -->
<div class="calendar-view-container"> <div class="calendar-view-container">
<!-- 1. VISTA SEMANAL --> <!-- 1. VISTA SEMANAL -->
@if (currentView() === 'week') { @if (currentView() === 'week') {
<div class="week-view animate-fadeIn"> <div class="week-view animate-fadeIn">
<!-- Encabezado de la semana --> <!-- Encabezado de la semana -->
<div class="week-header"> <div class="week-header">
<div class="timezone-label">GMT-04</div> <div class="timezone-label">GMT-04</div>
@for (day of currentWeekDays(); track day.getTime()) { @for (day of currentWeekDays(); track day.getTime()) {
<div (click)="selectDay(day)" class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors"> <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="day-name">{{ weekDays[day.getDay()] }}</span>
<span [class.today]="isToday(day)" class="day-number"> <span [class.today]="isToday(day)" class="day-number">
{{ day.getDate() }} {{ day.getDate() }}
</span> </span>
</div>
}
</div>
<!-- Cuerpo con cuadrícula de horas -->
<div class="week-grid-body">
<div class="hour-labels">
@for (hour of hours; track hour) {
<div class="hour-label-cell">
{{ formatHour(hour) }}
</div>
}
</div>
<!-- Columnas por día -->
@for (day of currentWeekDays(); track day.getTime()) {
<div class="day-column">
@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>
<!-- Cuerpo con cuadrícula de horas -->
<div class="week-grid-body">
<div class="hour-labels">
@for (hour of hours; track hour) {
<div class="hour-label-cell">
{{ formatHour(hour) }}
</div>
}
</div>
<!-- Columnas por día -->
@for (day of currentWeekDays(); track day.getTime()) {
<div class="day-column">
@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>
} }
<!-- 2. VISTA MENSUAL --> <!-- 2. VISTA MENSUAL -->
@if (currentView() === 'month') { @if (currentView() === 'month') {
<div class="animate-fadeIn" style="display: flex; flex-direction: column; height: 100%; width: 100%;"> <div class="animate-fadeIn" style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<!-- Fila de nombres de días --> <!-- Fila de nombres de días -->
<div class="day-names-row"> <div class="day-names-row">
@for (dayName of weekDays; track $index) { @for (dayName of weekDays; track $index) {
<div class="day-name-cell"> <div class="day-name-cell">
{{ dayName }} {{ 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>
}
</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 --> <!-- 3. VISTA DIARIA -->
@if (currentView() === 'day') { @if (currentView() === 'day') {
<div class="week-view animate-fadeIn"> <div class="week-view animate-fadeIn">
<!-- Encabezado de la semana --> <!-- Encabezado de la semana -->
<div class="week-header" style="grid-template-cols: 64px 1fr;"> <div class="week-header" style="grid-template-cols: 64px 1fr;">
<div class="timezone-label">GMT-04</div> <div class="timezone-label">GMT-04</div>
<div (click)="selectDay(currentDate())" class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors"> <div (click)="selectDay(currentDate())"
<span class="day-name">{{ weekDays[currentDate().getDay()] }}</span> class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors">
<span [class.today]="isToday(currentDate())" class="day-number"> <span class="day-name">{{ weekDays[currentDate().getDay()] }}</span>
{{ currentDate().getDate() }} <span [class.today]="isToday(currentDate())" class="day-number">
</span> {{ currentDate().getDate() }}
</div> </span>
</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>
<!-- 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>

View File

@ -1,6 +1,6 @@
<div class="form-container"> <div class="form-container">
<div class="dialog-card animate-fadeIn"> <div class="dialog-card animate-fadeIn">
<!-- Cabecera del diálogo con botón de cerrar --> <!-- Cabecera del diálogo con botón de cerrar -->
<header class="dialog-header"> <header class="dialog-header">
<a [routerLink]="['/reservas/calendario']" class="btn-close" title="Cerrar"> <a [routerLink]="['/reservas/calendario']" class="btn-close" title="Cerrar">
@ -10,18 +10,14 @@
<!-- Formulario Principal --> <!-- Formulario Principal -->
<form [formGroup]="bookingForm" (ngSubmit)="onSubmit()" class="dialog-body"> <form [formGroup]="bookingForm" (ngSubmit)="onSubmit()" class="dialog-body">
<!-- Fila 1: Añadir Título --> <!-- Fila 1: Añadir Título -->
<div class="title-input-container"> <div class="title-input-container">
<input <input type="text" formControlName="title" placeholder="Añade un título" class="title-input" />
type="text"
formControlName="title"
placeholder="Añade un título"
class="title-input" />
@if (bookingForm.get('title')?.touched && bookingForm.get('title')?.invalid) { @if (bookingForm.get('title')?.touched && bookingForm.get('title')?.invalid) {
<span style="font-size: 10px; color: #EA4335; font-weight: 600; margin-top: 4px; display: block;"> <span style="font-size: 10px; color: #EA4335; font-weight: 600; margin-top: 4px; display: block;">
El título es obligatorio. El título es obligatorio.
</span> </span>
} }
</div> </div>
@ -35,10 +31,11 @@
<!-- Mensaje de éxito de pre-llenado de la IA --> <!-- Mensaje de éxito de pre-llenado de la IA -->
@if (aiSuccessMessage()) { @if (aiSuccessMessage()) {
<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;"> <div
<span class="material-icons" style="font-size: 16px;">check_circle</span> 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>{{ aiSuccessMessage() }}</span> <span class="material-icons" style="font-size: 16px;">check_circle</span>
</div> <span>{{ aiSuccessMessage() }}</span>
</div>
} }
<!-- Fila 3: Horarios (Icono de Reloj) --> <!-- Fila 3: Horarios (Icono de Reloj) -->
@ -59,24 +56,20 @@
<span class="material-icons row-icon" style="color: var(--gold-dark);">smart_toy</span> <span class="material-icons row-icon" style="color: var(--gold-dark);">smart_toy</span>
<div class="row-content"> <div class="row-content">
<div class="ai-assistant-container"> <div class="ai-assistant-container">
<span class="row-label" style="color: var(--gold-dark); font-weight: bold; display: flex; align-items: center; gap: 4px;"> <span class="row-label"
<span>Asistente Inteligente Kinetix</span> style="color: var(--gold-dark); font-weight: bold; display: flex; align-items: center; gap: 4px;">
<span>Asistente Inteligente App</span>
</span> </span>
<textarea <textarea #promptInput rows="2"
#promptInput
rows="2"
placeholder="Escribe: Reserva sala Alpha para 5 personas mañana a las 11:30 am..." placeholder="Escribe: Reserva sala Alpha para 5 personas mañana a las 11:30 am..."
class="ai-assistant-textarea"></textarea> class="ai-assistant-textarea"></textarea>
<button <button type="button" (click)="processAiPrompt(promptInput.value)" [disabled]="isLoadingIa()"
type="button"
(click)="processAiPrompt(promptInput.value)"
[disabled]="isLoadingIa()"
class="ai-assistant-btn"> class="ai-assistant-btn">
@if (isLoadingIa()) { @if (isLoadingIa()) {
<span>Procesando...</span> <span>Procesando...</span>
} @else { } @else {
<span class="material-icons" style="font-size: 11px;">auto_awesome</span> <span class="material-icons" style="font-size: 11px;">auto_awesome</span>
<span>Autocompletar</span> <span>Autocompletar</span>
} }
</button> </button>
</div> </div>
@ -94,12 +87,8 @@
<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>
<input <input type="text" formControlName="resourceName" placeholder="Ej: Sala Alpha, Escritorio 4B"
type="text" class="dialog-input" style="flex: 2; min-width: 180px;" />
formControlName="resourceName"
placeholder="Ej: Sala Alpha, Escritorio 4B"
class="dialog-input"
style="flex: 2; min-width: 180px;" />
</div> </div>
</div> </div>
</div> </div>
@ -114,9 +103,11 @@
<span style="font-size: 12px; font-weight: 600;">Personas:</span> <span style="font-size: 12px; font-weight: 600;">Personas:</span>
<input type="number" formControlName="attendees" class="dialog-input" style="width: 70px;" min="1" /> <input type="number" formControlName="attendees" class="dialog-input" style="width: 70px;" min="1" />
</div> </div>
<label style="display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; cursor: pointer;"> <label
<input type="checkbox" formControlName="hasProjector" style="width: 16px; height: 16px; cursor: pointer;" /> style="display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; cursor: pointer;">
<input type="checkbox" formControlName="hasProjector"
style="width: 16px; height: 16px; cursor: pointer;" />
<span>Requiere Proyector</span> <span>Requiere Proyector</span>
</label> </label>
</div> </div>
@ -128,10 +119,7 @@
<span class="material-icons row-icon">notes</span> <span class="material-icons row-icon">notes</span>
<div class="row-content"> <div class="row-content">
<label class="row-label">Descripción</label> <label class="row-label">Descripción</label>
<textarea <textarea formControlName="notes" rows="3" placeholder="Añadir descripción o especificaciones adicionales..."
formControlName="notes"
rows="3"
placeholder="Añadir descripción o especificaciones adicionales..."
class="dialog-textarea"></textarea> class="dialog-textarea"></textarea>
</div> </div>
</div> </div>
@ -148,4 +136,4 @@
</form> </form>
</div> </div>
</div> </div>

View File

@ -115,7 +115,7 @@ export class IaParserService {
} }
result.title = `Reserva de ${result.resourceType === 'room' ? 'Sala' : result.resourceType === 'desk' ? 'Escritorio' : 'Activo'}`; result.title = `Reserva de ${result.resourceType === 'room' ? 'Sala' : result.resourceType === 'desk' ? 'Escritorio' : 'Activo'}`;
result.notes = `Sugerencia de reserva generada inteligentemente por el Asistente IA Kinetix.`; result.notes = `Sugerencia de reserva generada inteligentemente por el Asistente IA App.`;
return result; return result;
} }

View File

@ -1,14 +1,17 @@
<!doctype html> <!doctype html>
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Kinetix Intranet</title> <title>App Intranet</title>
<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"> <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>
</body> </body>
</html>
</html>

View File

@ -1,5 +1,5 @@
/* ============================================================ /* ============================================================
KINETIX INTRANET Executive Suite Design System App INTRANET Executive Suite Design System
Paleta: Fondo blanco, tarjetas joya sofisticadas Paleta: Fondo blanco, tarjetas joya sofisticadas
============================================================ */ ============================================================ */
@ -9,53 +9,72 @@
/* ── Design Tokens ─────────────────────────────────────── */ /* ── Design Tokens ─────────────────────────────────────── */
:root { :root {
/* ── Fondo y superficies (LIGHT MODE) ── */ /* ── Fondo y superficies (LIGHT MODE) ── */
--bg-page: #F5F4F1; /* Blanco cálido / ivory */ --bg-page: #F5F4F1;
--bg-surface: #FFFFFF; /* Superficie limpia */ /* Blanco cálido / ivory */
--bg-surface-2: #F0EEE9; /* Superficies secundarias */ --bg-surface: #FFFFFF;
/* Superficie limpia */
--bg-surface-2: #F0EEE9;
/* Superficies secundarias */
/* ── Colores joya para tarjetas sofisticadas ── */ /* ── Colores joya para tarjetas sofisticadas ── */
--card-navy: #0D1B2A; /* Azul marino profundo */ --card-navy: #0D1B2A;
--card-plum: #1A0E2E; /* Ciruela / Violeta oscuro */ /* Azul marino profundo */
--card-viridian: #0A2520; /* Verde Viridian / Bosque profundo */ --card-plum: #1A0E2E;
--card-espresso: #1C0E08; /* Espresso / Marrón ébano */ /* Ciruela / Violeta oscuro */
--card-viridian: #0A2520;
/* Verde Viridian / Bosque profundo */
--card-espresso: #1C0E08;
/* Espresso / Marrón ébano */
/* ── Acentos elegantes ── */ /* ── Acentos elegantes ── */
--gold-luxe: #C9973C; /* Oro antiguo (visible en blanco) */ --gold-luxe: #C9973C;
--gold-dark: #A67C28; /* Oro antiguo (visible en blanco) */
--gold-light: #F0C060; /* Destellos dorados en tarjetas */ --gold-dark: #A67C28;
--indigo-rich: #3730A3; /* Índigo profundo */ --gold-light: #F0C060;
--sapphire: #1E3A8A; /* Zafiro */ /* Destellos dorados en tarjetas */
--emerald-deep: #065F46; /* Esmeralda profundo */ --indigo-rich: #3730A3;
/* Índigo profundo */
--sapphire: #1E3A8A;
/* Zafiro */
--emerald-deep: #065F46;
/* Esmeralda profundo */
/* ── Neon (login / accents) ── */ /* ── Neon (login / accents) ── */
--neon-blue: #0EA5E9; --neon-blue: #0EA5E9;
--neon-cyan: #06B6D4; --neon-cyan: #06B6D4;
/* ── Textos (dark on white) ── */ /* ── Textos (dark on white) ── */
--text-primary: #0F0D0A; /* Negro cálido */ --text-primary: #0F0D0A;
--text-secondary: #44403C; /* Marrón oscuro */ /* Negro cálido */
--text-muted: #78716C; /* Gris cálido */ --text-secondary: #44403C;
/* Marrón oscuro */
--text-muted: #78716C;
/* Gris cálido */
--text-placeholder: #A8A29E; --text-placeholder: #A8A29E;
/* ── Bordes / divisores ── */ /* ── Bordes / divisores ── */
--border-light: rgba(15, 13, 10, 0.08); --border-light: rgba(15, 13, 10, 0.08);
--border-medium: rgba(15, 13, 10, 0.14); --border-medium: rgba(15, 13, 10, 0.14);
--border-gold: rgba(201, 151, 60, 0.30); --border-gold: rgba(201, 151, 60, 0.30);
/* Legacy aliases */ /* Legacy aliases */
--bg-dark: #0D1B2A; --bg-dark: #0D1B2A;
--bg-card: rgba(17, 22, 41, 0.70); --bg-card: rgba(17, 22, 41, 0.70);
--bg-card-solid: #111629; --bg-card-solid: #111629;
--accent-cyan: #0EA5E9; --accent-cyan: #0EA5E9;
--accent-magenta: hsl(320, 100%, 50%); --accent-magenta: hsl(320, 100%, 50%);
--accent-purple: hsl(262, 80%, 60%); --accent-purple: hsl(262, 80%, 60%);
--bg-deep-dark: #02040A; --bg-deep-dark: #02040A;
--text-light: #FFFFFF; --text-light: #FFFFFF;
--text-muted-old: #858FA3; --text-muted-old: #858FA3;
} }
/* ── Base ──────────────────────────────────────────────── */ /* ── Base ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; } *,
*::before,
*::after {
box-sizing: border-box;
}
html { html {
font-size: 16px; font-size: 16px;
@ -74,7 +93,11 @@ body {
} }
/* ── Typography ─────────────────────────────────────────── */ /* ── Typography ─────────────────────────────────────────── */
h1, h2, h3, h4, .font-display { h1,
h2,
h3,
h4,
.font-display {
font-family: 'Inter', 'Outfit', sans-serif; font-family: 'Inter', 'Outfit', sans-serif;
font-weight: 900; font-weight: 900;
margin: 0; margin: 0;
@ -154,17 +177,31 @@ h1, h2, h3, h4, .font-display {
.neon-border-gold { .neon-border-gold {
border: 1px solid rgba(201, 151, 60, 0.25); border: 1px solid rgba(201, 151, 60, 0.25);
} }
.neon-border-gold:hover { border-color: var(--gold-luxe); }
.neon-border-gold:hover {
border-color: var(--gold-luxe);
}
.neon-border-blue { .neon-border-blue {
border: 1px solid rgba(59, 130, 246, 0.20); border: 1px solid rgba(59, 130, 246, 0.20);
} }
.neon-border-blue:hover { border-color: var(--sapphire); }
.neon-border-blue:hover {
border-color: var(--sapphire);
}
/* Legacy borders */ /* Legacy borders */
.neon-border-cyan { border: 1px solid rgba(14, 165, 233, 0.20); } .neon-border-cyan {
.neon-border-magenta { border: 1px solid rgba(240, 12, 147, 0.15); } border: 1px solid rgba(14, 165, 233, 0.20);
.neon-border-purple { border: 1px solid rgba(124, 58, 237, 0.15); } }
.neon-border-magenta {
border: 1px solid rgba(240, 12, 147, 0.15);
}
.neon-border-purple {
border: 1px solid rgba(124, 58, 237, 0.15);
}
/* ── Typography Colors ──────────────────────────────────── */ /* ── Typography Colors ──────────────────────────────────── */
.text-gold { .text-gold {
@ -205,7 +242,10 @@ h1, h2, h3, h4, .font-display {
box-sizing: border-box; box-sizing: border-box;
} }
.input-luxe::placeholder { color: var(--text-placeholder); } .input-luxe::placeholder {
color: var(--text-placeholder);
}
.input-luxe:focus { .input-luxe:focus {
border-color: var(--gold-luxe); border-color: var(--gold-luxe);
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12); box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
@ -227,7 +267,10 @@ h1, h2, h3, h4, .font-display {
box-sizing: border-box; box-sizing: border-box;
} }
.input-cyberpunk::placeholder { color: var(--text-placeholder); } .input-cyberpunk::placeholder {
color: var(--text-placeholder);
}
.input-cyberpunk:focus { .input-cyberpunk:focus {
border-color: var(--gold-luxe); border-color: var(--gold-luxe);
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.10); box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.10);
@ -249,7 +292,10 @@ h1, h2, h3, h4, .font-display {
box-sizing: border-box; box-sizing: border-box;
} }
.input-dark::placeholder { color: rgba(255, 255, 255, 0.35); } .input-dark::placeholder {
color: rgba(255, 255, 255, 0.35);
}
.input-dark:focus { .input-dark:focus {
border-color: rgba(201, 151, 60, 0.55); border-color: rgba(201, 151, 60, 0.55);
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12); box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
@ -304,7 +350,10 @@ h1, h2, h3, h4, .font-display {
box-shadow: 0 4px 16px rgba(201, 151, 60, 0.30); box-shadow: 0 4px 16px rgba(201, 151, 60, 0.30);
} }
.btn-cyan:hover { transform: translateY(-2px); box-shadow: 0 6px 22px rgba(201, 151, 60, 0.45); } .btn-cyan:hover {
transform: translateY(-2px);
box-shadow: 0 6px 22px rgba(201, 151, 60, 0.45);
}
/* Indigo solid */ /* Indigo solid */
.btn-indigo { .btn-indigo {
@ -328,7 +377,9 @@ h1, h2, h3, h4, .font-display {
box-shadow: 0 4px 16px rgba(55, 48, 163, 0.30); box-shadow: 0 4px 16px rgba(55, 48, 163, 0.30);
} }
.btn-indigo:hover { transform: translateY(-2px); } .btn-indigo:hover {
transform: translateY(-2px);
}
/* Ghost on white */ /* Ghost on white */
.btn-ghost { .btn-ghost {
@ -373,16 +424,29 @@ h1, h2, h3, h4, .font-display {
cursor: pointer; cursor: pointer;
transition: transform 0.2s; transition: transform 0.2s;
} }
.btn-purple:hover { transform: translateY(-1px); }
.btn-purple:hover {
transform: translateY(-1px);
}
/* ── Scrollbar ─────────────────────────────────────────── */ /* ── Scrollbar ─────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar {
::-webkit-scrollbar-track { background: var(--bg-surface-2); } width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-surface-2);
}
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(15, 13, 10, 0.15); background: rgba(15, 13, 10, 0.15);
border-radius: 4px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { background: rgba(201, 151, 60, 0.45); }
::-webkit-scrollbar-thumb:hover {
background: rgba(201, 151, 60, 0.45);
}
/* ── Utilities ─────────────────────────────────────────── */ /* ── Utilities ─────────────────────────────────────────── */
.animate-fadeIn { .animate-fadeIn {
@ -390,12 +454,25 @@ h1, h2, h3, h4, .font-display {
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.sr-only { .sr-only {
position: absolute; width: 1px; height: 1px; position: absolute;
padding: 0; margin: -1px; overflow: hidden; width: 1px;
clip: rect(0,0,0,0); white-space: nowrap; border: 0; height: 1px;
} padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

14
package-lock.json generated
View File

@ -1,11 +1,11 @@
{ {
"name": "kinetix-monorepo", "name": "App-monorepo",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kinetix-monorepo", "name": "App-monorepo",
"version": "1.0.0", "version": "1.0.0",
"workspaces": [ "workspaces": [
"frontend", "frontend",
@ -16,7 +16,7 @@
} }
}, },
"backend": { "backend": {
"name": "kinetix-backend", "name": "App-backend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
@ -38,7 +38,7 @@
} }
}, },
"frontend": { "frontend": {
"name": "kinetix-frontend", "name": "App-frontend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@angular/animations": "^18.0.0", "@angular/animations": "^18.0.0",
@ -9582,11 +9582,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/kinetix-backend": { "node_modules/App-backend": {
"resolved": "backend", "resolved": "backend",
"link": true "link": true
}, },
"node_modules/kinetix-frontend": { "node_modules/App-frontend": {
"resolved": "frontend", "resolved": "frontend",
"link": true "link": true
}, },
@ -14086,4 +14086,4 @@
"integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==" "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ=="
} }
} }
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "kinetix-monorepo", "name": "App-monorepo",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [
@ -17,4 +17,4 @@
"devDependencies": { "devDependencies": {
"concurrently": "^8.2.2" "concurrently": "^8.2.2"
} }
} }

View File

@ -1,5 +1,5 @@
# setup.ps1 # setup.ps1
Write-Output "=== Iniciando configuración de Kinetix Intranet Monorepo ===" Write-Output "=== Iniciando configuración de App Intranet Monorepo ==="
# 1. Copiar archivo de entorno si no existe # 1. Copiar archivo de entorno si no existe
if (!(Test-Path ".env")) { if (!(Test-Path ".env")) {

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# setup.sh # setup.sh
echo "=== Iniciando configuración de Kinetix Intranet Monorepo ===" echo "=== Iniciando configuración de App Intranet Monorepo ==="
# 1. Copiar archivo de entorno si no existe # 1. Copiar archivo de entorno si no existe
if [ ! -f .env ]; then if [ ! -f .env ]; then

View File

@ -14,7 +14,7 @@ El proyecto utiliza **PostgreSQL** para persistencia estructurada y transacciona
## 2. Esquema Conceptual (Prisma Schema Reference) ## 2. Esquema Conceptual (Prisma Schema Reference)
```prisma ```prisma
// Esquema de Base de Datos Kinetix Intranet // Esquema de Base de Datos App Intranet
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())

View File

@ -31,7 +31,7 @@ El backend utiliza `passport-jwt` o `passport-azure-ad` para descifrar y validar
### Claims esperados en el JWT decodificado: ### Claims esperados en el JWT decodificado:
- `sub` (Identificador único de usuario) - `sub` (Identificador único de usuario)
- `email` (Correo electrónico corporativo, ej: `jess@kinetix.com`) - `email` (Correo electrónico corporativo, ej: `jess@App.com`)
- `name` (Nombre del empleado) - `name` (Nombre del empleado)
- `roles` (Roles asignados, ej: `['Employee', 'Manager']`) - `roles` (Roles asignados, ej: `['Employee', 'Manager']`)
- `department` (Departamento de la organización) - `department` (Departamento de la organización)

View File

@ -1,6 +1,6 @@
# Estándares - UI/UX Diseño Executive Suite / Kinetix # Estándares - UI/UX Diseño Executive Suite / App
Este documento establece la guía de estilos visuales e interactivos de la intranet corporativa Kinetix, adaptada al **Executive Suite Design System**. Este documento establece la guía de estilos visuales e interactivos de la intranet corporativa App, adaptada al **Executive Suite Design System**.
--- ---