feat: implement dashboard and authentication architecture with auth guard and login component

This commit is contained in:
Jessica Orozco 2026-06-16 11:40:38 -04:00
parent 2baebda412
commit a83a901bf9
57 changed files with 2294 additions and 252 deletions

8
.gitignore vendored
View File

@ -1 +1,9 @@
node_modules node_modules
dist
/backend/dist
/backend/node_modules
/frontend/dist
/frontend/node_modules
/frontend/.angular/cache
/.vscode

View File

@ -0,0 +1 @@
{"ast":null,"code":"import { inject } from '@angular/core';\nimport { Router } from '@angular/router';\nexport const authGuard = (route, state) => {\n const router = inject(Router);\n const token = localStorage.getItem('kinetix_token');\n if (token) {\n return true;\n }\n // Redirect to login if not authenticated\n router.navigate(['/login']);\n return false;\n};","map":{"version":3,"names":["inject","Router","authGuard","route","state","router","token","localStorage","getItem","navigate"],"sources":["D:\\Jess\\agent\\Kinetix\\frontend\\src\\app\\core\\guards\\auth.guard.ts"],"sourcesContent":["import { inject } from '@angular/core';\nimport { Router, CanActivateFn } from '@angular/router';\n\nexport const authGuard: CanActivateFn = (route, state) => {\n const router = inject(Router);\n const token = localStorage.getItem('kinetix_token');\n\n if (token) {\n return true;\n }\n\n // Redirect to login if not authenticated\n router.navigate(['/login']);\n return false;\n};\n"],"mappings":"AAAA,SAASA,MAAM,QAAQ,eAAe;AACtC,SAASC,MAAM,QAAuB,iBAAiB;AAEvD,OAAO,MAAMC,SAAS,GAAkBA,CAACC,KAAK,EAAEC,KAAK,KAAI;EACvD,MAAMC,MAAM,GAAGL,MAAM,CAACC,MAAM,CAAC;EAC7B,MAAMK,KAAK,GAAGC,YAAY,CAACC,OAAO,CAAC,eAAe,CAAC;EAEnD,IAAIF,KAAK,EAAE;IACT,OAAO,IAAI;EACb;EAEA;EACAD,MAAM,CAACI,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;EAC3B,OAAO,KAAK;AACd,CAAC","ignoreList":[]},"metadata":{},"sourceType":"module","externalDependencies":[]}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"ast":null,"code":"import { inject } from '@angular/core';\nimport { Router } from '@angular/router';\nexport const authGuard = (route, state) => {\n const router = inject(Router);\n const token = localStorage.getItem('kinetix_token');\n if (token) {\n return true;\n }\n // Redirect to login if not authenticated\n router.navigate(['/login']);\n return false;\n};","map":null,"metadata":{},"sourceType":"module","externalDependencies":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"ast":null,"code":"import { authGuard } from './core/guards/auth.guard';\nexport const routes = [{\n path: '',\n redirectTo: 'dashboard',\n pathMatch: 'full'\n}, {\n path: 'login',\n loadComponent: () => import('./features/auth/login.component').then(m => m.LoginComponent)\n}, {\n path: 'dashboard',\n canActivate: [authGuard],\n loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent)\n}];\n// Modules for each subagent will be lazily loaded here.","map":null,"metadata":{},"sourceType":"module","externalDependencies":[]}

View File

@ -0,0 +1 @@
{"ast":null,"code":"import { authGuard } from './core/guards/auth.guard';\nexport const routes = [{\n path: '',\n redirectTo: 'dashboard',\n pathMatch: 'full'\n}, {\n path: 'login',\n loadComponent: () => import('./features/auth/login.component').then(m => m.LoginComponent)\n}, {\n path: 'dashboard',\n canActivate: [authGuard],\n loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent)\n}];\n// Modules for each subagent will be lazily loaded here.","map":{"version":3,"names":["authGuard","routes","path","redirectTo","pathMatch","loadComponent","then","m","LoginComponent","canActivate","DashboardComponent"],"sources":["D:\\Jess\\agent\\Kinetix\\frontend\\src\\app\\app.routes.ts"],"sourcesContent":["import { Routes } from '@angular/router';\nimport { authGuard } from './core/guards/auth.guard';\n\nexport const routes: Routes = [\n {\n path: '',\n redirectTo: 'dashboard',\n pathMatch: 'full'\n },\n {\n path: 'login',\n loadComponent: () => import('./features/auth/login.component').then(m => m.LoginComponent)\n },\n {\n path: 'dashboard',\n canActivate: [authGuard],\n loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent)\n }\n];\n// Modules for each subagent will be lazily loaded here.\n"],"mappings":"AACA,SAASA,SAAS,QAAQ,0BAA0B;AAEpD,OAAO,MAAMC,MAAM,GAAW,CAC5B;EACEC,IAAI,EAAE,EAAE;EACRC,UAAU,EAAE,WAAW;EACvBC,SAAS,EAAE;CACZ,EACD;EACEF,IAAI,EAAE,OAAO;EACbG,aAAa,EAAEA,CAAA,KAAM,MAAM,CAAC,iCAAiC,CAAC,CAACC,IAAI,CAACC,CAAC,IAAIA,CAAC,CAACC,cAAc;CAC1F,EACD;EACEN,IAAI,EAAE,WAAW;EACjBO,WAAW,EAAE,CAACT,SAAS,CAAC;EACxBK,aAAa,EAAEA,CAAA,KAAM,MAAM,CAAC,0CAA0C,CAAC,CAACC,IAAI,CAACC,CAAC,IAAIA,CAAC,CAACG,kBAAkB;CACvG,CACF;AACD","ignoreList":[]},"metadata":{},"sourceType":"module","externalDependencies":[]}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"ast":null,"code":"\"use strict\";","map":{"version":3,"names":[],"sources":["D:\\Jess\\agent\\Kinetix\\frontend\\src\\app\\features\\dashboard\\dashboard.component.ts"],"sourcesContent":[""],"mappings":"","ignoreList":[]},"metadata":{},"sourceType":"script","externalDependencies":[]}

View File

@ -0,0 +1 @@
{"ast":null,"code":"import { inject } from '@angular/core';\nimport { RouterOutlet, Router } from '@angular/router';\nimport { CommonModule } from '@angular/common';\nimport * as i0 from \"@angular/core\";\nimport * as i1 from \"@angular/common\";\nfunction AppComponent_header_1_Template(rf, ctx) {\n if (rf & 1) {\n i0.ɵɵelementStart(0, \"header\", 4)(1, \"h1\", 5);\n i0.ɵɵtext(2, \" KINETIX INTRANET \");\n i0.ɵɵelementEnd();\n i0.ɵɵelementStart(3, \"div\", 6);\n i0.ɵɵtext(4, \"MODULAR AGENT ARCHITECTURE\");\n i0.ɵɵelementEnd()();\n }\n}\nfunction AppComponent_footer_4_Template(rf, ctx) {\n if (rf & 1) {\n i0.ɵɵelementStart(0, \"footer\", 7);\n i0.ɵɵtext(1, \" \\u00A9 2026 Kinetix Corp. All rights reserved. \");\n i0.ɵɵelementEnd();\n }\n}\nexport let AppComponent = /*#__PURE__*/(() => {\n class AppComponent {\n constructor() {\n this.router = inject(Router);\n }\n isLoginView() {\n return this.router.url.includes('/login');\n }\n static {\n this.ɵfac = function AppComponent_Factory(t) {\n return new (t || AppComponent)();\n };\n }\n static {\n this.ɵcmp = /*@__PURE__*/i0.ɵɵdefineComponent({\n type: AppComponent,\n selectors: [[\"app-root\"]],\n standalone: true,\n features: [i0.ɵɵStandaloneFeature],\n decls: 5,\n vars: 2,\n consts: [[1, \"min-h-screen\", \"flex\", \"flex-col\", \"justify-between\", \"p-6\", \"bg-[#0a0a0c]\"], [\"class\", \"flex justify-between items-center border-b border-white/10 pb-4 mb-6\", 4, \"ngIf\"], [1, \"flex-grow\"], [\"class\", \"border-t border-white/5 pt-4 mt-6 text-center text-xs text-white/30\", 4, \"ngIf\"], [1, \"flex\", \"justify-between\", \"items-center\", \"border-b\", \"border-white/10\", \"pb-4\", \"mb-6\"], [1, \"text-2xl\", \"font-extrabold\", \"tracking-wider\", \"text-transparent\", \"bg-clip-text\", \"bg-gradient-to-r\", \"from-cyan-400\", \"to-blue-500\"], [1, \"text-xs\", \"text-white/50\"], [1, \"border-t\", \"border-white/5\", \"pt-4\", \"mt-6\", \"text-center\", \"text-xs\", \"text-white/30\"]],\n template: function AppComponent_Template(rf, ctx) {\n if (rf & 1) {\n i0.ɵɵelementStart(0, \"main\", 0);\n i0.ɵɵtemplate(1, AppComponent_header_1_Template, 5, 0, \"header\", 1);\n i0.ɵɵelementStart(2, \"section\", 2);\n i0.ɵɵelement(3, \"router-outlet\");\n i0.ɵɵelementEnd();\n i0.ɵɵtemplate(4, AppComponent_footer_4_Template, 2, 0, \"footer\", 3);\n i0.ɵɵelementEnd();\n }\n if (rf & 2) {\n i0.ɵɵadvance();\n i0.ɵɵproperty(\"ngIf\", !ctx.isLoginView());\n i0.ɵɵadvance(3);\n i0.ɵɵproperty(\"ngIf\", !ctx.isLoginView());\n }\n },\n dependencies: [RouterOutlet, CommonModule, i1.NgIf],\n encapsulation: 2\n });\n }\n }\n return AppComponent;\n})();","map":null,"metadata":{},"sourceType":"module","externalDependencies":[]}

File diff suppressed because one or more lines are too long

View File

@ -41,8 +41,8 @@
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
"maximumWarning": "2kb", "maximumWarning": "6kb",
"maximumError": "4kb" "maximumError": "8kb"
} }
], ],
"outputHashing": "all" "outputHashing": "all"

View File

@ -4,6 +4,9 @@ MIT
@angular/core @angular/core
MIT MIT
@angular/forms
MIT
@angular/platform-browser @angular/platform-browser
MIT MIT
@ -216,6 +219,21 @@ Apache-2.0
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 zone.js
MIT MIT
The MIT License The MIT License

File diff suppressed because one or more lines are too long

View File

@ -6,8 +6,8 @@
<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">
<style>@import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap";:root{font-family:Inter,sans-serif;color-scheme:dark;--bg-deep-dark:HSL(240, 15%, 4%);--bg-card:HSL(240, 12%, 8%);--accent-cyan:HSL(180, 100%, 50%);--accent-magenta:HSL(320, 100%, 50%);--accent-purple:HSL(262, 80%, 60%);--accent-green:HSL(140, 100%, 50%)}body{margin:0;background-color:var(--bg-deep-dark);background-image:radial-gradient(circle at 50% -20%,rgba(0,242,254,.07) 0%,rgba(240,12,147,.03) 50%,transparent 100%);color:#f3f4f6;overflow-x:hidden;min-height:100vh}</style><link rel="stylesheet" href="styles.c99b507124da15c4.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.c99b507124da15c4.css"></noscript></head> <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"> <body class="bg-black text-white antialiased">
<app-root></app-root> <app-root></app-root>
<script src="runtime.16fe48dffc610f9c.js" type="module"></script><script src="polyfills.db3ad031cea79ddb.js" type="module"></script><script src="main.80fea3cd03abcfb4.js" type="module"></script></body> <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> </html>

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},m={};function r(e){var o=m[e];if(void 0!==o)return o.exports;var t=m[e]={exports:{}};return v[e](t,t.exports,r),t.exports}r.m=v,e=[],r.O=(o,t,i,f)=>{if(!t){var a=1/0;for(n=0;n<e.length;n++){for(var[t,i,f]=e[n],s=!0,u=0;u<t.length;u++)(!1&f||a>=f)&&Object.keys(r.O).every(b=>r.O[b](t[u]))?t.splice(u--,1):(s=!1,f<a&&(a=f));if(s){e.splice(n--,1);var l=i();void 0!==l&&(o=l)}}return o}f=f||0;for(var n=e.length;n>0&&e[n-1][2]>f;n--)e[n]=e[n-1];e[n]=[t,i,f]},r.d=(e,o)=>{for(var t in o)r.o(o,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((o,t)=>(r.f[t](e,o),o),[])),r.u=e=>e+".d3df5d187c1cfe3b.js",r.miniCssF=e=>{},r.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),(()=>{var e={},o="kinetix-frontend:";r.l=(t,i,f,n)=>{if(e[t])e[t].push(i);else{var a,s;if(void 0!==f)for(var u=document.getElementsByTagName("script"),l=0;l<u.length;l++){var d=u[l];if(d.getAttribute("src")==t||d.getAttribute("data-webpack")==o+f){a=d;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",o+f),a.src=r.tu(t)),e[t]=[i];var c=(g,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(_=>_(b)),g)return g(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:o=>o},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={121:0};r.f.j=(i,f)=>{var n=r.o(e,i)?e[i]:void 0;if(0!==n)if(n)f.push(n[2]);else if(121!=i){var a=new Promise((d,c)=>n=e[i]=[d,c]);f.push(n[2]=a);var s=r.p+r.u(i),u=new Error;r.l(s,d=>{if(r.o(e,i)&&(0!==(n=e[i])&&(e[i]=void 0),n)){var c=d&&("load"===d.type?"missing":d.type),p=d&&d.target&&d.target.src;u.message="Loading chunk "+i+" failed.\n("+c+": "+p+")",u.name="ChunkLoadError",u.type=c,u.request=p,n[1](u)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var o=(i,f)=>{var u,l,[n,a,s]=f,d=0;if(n.some(p=>0!==e[p])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(s)var c=s(r)}for(i&&i(f);d<n.length;d++)r.o(e,l=n[d])&&e[l]&&e[l][0](),e[l]=0;return r.O(c)},t=self.webpackChunkkinetix_frontend=self.webpackChunkkinetix_frontend||[];t.forEach(o.bind(null,0)),t.push=o.bind(null,t.push.bind(t))})()})();

View File

@ -1 +0,0 @@
@import"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap";:root{font-family:Inter,sans-serif;color-scheme:dark;--bg-deep-dark: HSL(240, 15%, 4%);--bg-card: HSL(240, 12%, 8%);--accent-cyan: HSL(180, 100%, 50%);--accent-magenta: HSL(320, 100%, 50%);--accent-purple: HSL(262, 80%, 60%);--accent-green: HSL(140, 100%, 50%)}body{margin:0;background-color:var(--bg-deep-dark);background-image:radial-gradient(circle at 50% -20%,rgba(0,242,254,.07) 0%,rgba(240,12,147,.03) 50%,transparent 100%);color:#f3f4f6;overflow-x:hidden;min-height:100vh}.glass-panel{background:#0f0f16a6;backdrop-filter:blur(14px);border:1px solid rgba(255,255,255,.06);box-shadow:0 12px 40px #00000080;transition:all .3s cubic-bezier(.4,0,.2,1)}.glass-panel-hover:hover{transform:translateY(-2px);border-color:#ffffff1f;box-shadow:0 16px 48px #0009}.neon-border-cyan{border:1px solid rgba(0,242,254,.15)}.neon-border-cyan:hover{border-color:var(--accent-cyan);box-shadow:0 0 15px #00f2fe33}.neon-border-magenta{border:1px solid rgba(240,12,147,.15)}.neon-border-magenta:hover{border-color:var(--accent-magenta);box-shadow:0 0 15px #f00c9333}.neon-border-purple{border:1px solid rgba(124,58,237,.15)}.neon-border-purple:hover{border-color:var(--accent-purple);box-shadow:0 0 15px #7c3aed33}.text-neon-cyan{color:var(--accent-cyan);text-shadow:0 0 10px rgba(0,242,254,.3)}.text-neon-magenta{color:var(--accent-magenta);text-shadow:0 0 10px rgba(240,12,147,.3)}.text-neon-purple{color:var(--accent-purple);text-shadow:0 0 10px rgba(124,58,237,.3)}.input-cyberpunk{background:#0a0a0fcc;border:1px solid rgba(255,255,255,.1);color:#fff;transition:all .3s ease}.input-cyberpunk:focus{outline:none;border-color:var(--accent-cyan);box-shadow:0 0 8px #00f2fe40;background:#0a0a0ff2}.btn-cyan{background:var(--accent-cyan);color:#000;font-weight:700;letter-spacing:.05em;transition:all .3s ease;box-shadow:0 4px 14px #00f2fe4d}.btn-cyan:hover{transform:translateY(-1px);box-shadow:0 6px 20px #00f2fe80}.btn-purple{background:var(--accent-purple);color:#fff;font-weight:700;letter-spacing:.05em;transition:all .3s ease;box-shadow:0 4px 14px #7c3aed4d}.btn-purple:hover{transform:translateY(-1px);box-shadow:0 6px 20px #7c3aed80}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-deep-dark)}::-webkit-scrollbar-thumb{background:#ffffff1a;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--accent-cyan)}

View File

@ -0,0 +1,104 @@
/* =========================================================
APP SHELL Executive Suite / Light Mode
========================================================= */
.app-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #F5F4F1;
padding: 0 32px 32px;
box-sizing: border-box;
}
.app-shell.login-mode {
padding: 0;
background: #F5F4F1; /* ivory white — same as page */
}
/* ── Header ── */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 0;
border-bottom: 1px solid rgba(15, 13, 10, 0.08);
margin-bottom: 32px;
}
.header-brand {
display: flex;
align-items: center;
gap: 12px;
}
.header-logo {
width: 38px;
height: 38px;
border-radius: 10px;
background: linear-gradient(135deg, #0D1B2A, #1A0E2E);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(13, 27, 42, 0.25);
flex-shrink: 0;
}
.header-logo svg { width: 22px; height: 22px; }
.header-wordmark {
display: flex;
flex-direction: column;
gap: 1px;
}
.header-name {
font-family: 'Inter', 'Outfit', sans-serif;
font-size: 14px;
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #0F0D0A;
line-height: 1;
}
.header-sub {
font-size: 9px;
font-weight: 500;
color: #78716C;
letter-spacing: 0.10em;
text-transform: uppercase;
}
.header-meta {
font-size: 11px;
color: #78716C;
letter-spacing: 0.05em;
font-weight: 500;
}
/* ── Content ── */
.app-content { flex: 1; display: flex; flex-direction: column; }
/* ── Footer ── */
.app-footer {
margin-top: 32px;
padding-top: 18px;
border-top: 1px solid rgba(15, 13, 10, 0.07);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 10px;
font-weight: 500;
color: #A8A29E;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.footer-dot {
width: 3px;
height: 3px;
border-radius: 50%;
background: #D6D3D1;
}

View File

@ -1,27 +1,51 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet, Router } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [RouterOutlet], imports: [RouterOutlet, CommonModule],
styleUrls: ['./app.component.css'],
template: ` template: `
<main class="min-h-screen flex flex-col justify-between p-6 bg-[#0a0a0c]"> <main class="app-shell" [class.login-mode]="isLoginView()">
<header class="flex justify-between items-center border-b border-white/10 pb-4 mb-6">
<h1 class="text-2xl font-extrabold tracking-wider text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500"> <header *ngIf="!isLoginView()" class="app-header">
KINETIX INTRANET <div class="header-brand">
</h1> <div class="header-logo">
<div class="text-xs text-white/50">MODULAR AGENT ARCHITECTURE</div> <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="M13 4H18" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
<path d="M13 20H18" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
<div class="header-wordmark">
<span class="header-name">Kinetix</span>
<span class="header-sub">Intranet Corporativa</span>
</div>
</div>
<div class="header-meta">Consorcio · 2026</div>
</header> </header>
<section class="flex-grow"> <section class="app-content">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</section> </section>
<footer class="border-t border-white/5 pt-4 mt-6 text-center text-xs text-white/30"> <footer *ngIf="!isLoginView()" class="app-footer">
© 2026 Kinetix Corp. All rights reserved. <span>© 2026 Kinetix</span>
<span class="footer-dot"></span>
<span>Todos los derechos reservados</span>
<span class="footer-dot"></span>
<span>SSL/TLS Activo</span>
</footer> </footer>
</main> </main>
` `
}) })
export class AppComponent {} export class AppComponent {
private router = inject(Router);
isLoginView(): boolean {
return this.router.url.includes('/login');
}
}

View File

@ -1,4 +1,5 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { authGuard } from './core/guards/auth.guard';
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -6,9 +7,33 @@ export const routes: Routes = [
redirectTo: 'dashboard', redirectTo: 'dashboard',
pathMatch: 'full' pathMatch: 'full'
}, },
{
path: 'login',
loadComponent: () => import('./features/auth/login.component').then(m => m.LoginComponent)
},
{ {
path: 'dashboard', path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent) loadComponent: () => import('./features/dashboard/dashboard.component').then(m => m.DashboardComponent)
},
{
path: 'reservas',
canActivate: [authGuard],
children: [
{
path: '',
redirectTo: 'calendario',
pathMatch: 'full'
},
{
path: 'calendario',
loadComponent: () => import('./features/reservas/calendar-booking/calendar-booking.component').then(m => m.CalendarBookingComponent)
},
{
path: 'nuevo',
loadComponent: () => import('./features/reservas/reserva-form/reserva-form.component').then(m => m.ReservaFormComponent)
}
]
} }
]; ];
// Modules for each subagent will be lazily loaded here. // Modules for each subagent will be lazily loaded here.

View File

@ -0,0 +1,15 @@
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
const token = localStorage.getItem('kinetix_token');
if (token) {
return true;
}
// Redirect to login if not authenticated
router.navigate(['/login']);
return false;
};

View File

@ -0,0 +1,340 @@
/* =========================================================
LOGIN COMPONENT Executive Suite (Light Mode)
Fondo blanco marfil, card blanca con sombras suaves,
acentos joya, tipografía oscura elegante.
========================================================= */
.login-page {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #F5F4F1;
background-image:
radial-gradient(ellipse at 25% 15%, rgba(201, 151, 60, 0.06) 0%, transparent 55%),
radial-gradient(ellipse at 75% 85%, rgba(55, 48, 163, 0.05) 0%, transparent 60%);
padding: 24px;
position: relative;
overflow: hidden;
box-sizing: border-box;
}
/* Subtle ambient shapes */
.glow-tl {
position: absolute;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(201, 151, 60, 0.05) 0%, transparent 70%);
top: -180px;
left: -180px;
pointer-events: none;
}
.glow-br {
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(55, 48, 163, 0.04) 0%, transparent 70%);
bottom: -140px;
right: -140px;
pointer-events: none;
}
/* ── Login Card ── */
.login-card {
position: relative;
z-index: 1;
width: 100%;
max-width: 440px;
background: #FFFFFF;
border: 1px solid rgba(15, 13, 10, 0.08);
border-radius: 24px;
padding: 48px 42px;
box-shadow:
0 1px 3px rgba(15, 13, 10, 0.04),
0 16px 48px rgba(15, 13, 10, 0.08),
0 0 0 1px rgba(15, 13, 10, 0.02);
display: flex;
flex-direction: column;
gap: 22px;
}
/* Inner decorative border — gold accent */
.login-card::before {
content: '';
position: absolute;
inset: 10px;
border: 1px solid rgba(201, 151, 60, 0.12);
border-radius: 16px;
pointer-events: none;
}
/* ── Brand ── */
.login-brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
}
.login-logo-wrap {
width: 60px;
height: 60px;
border-radius: 16px;
background: linear-gradient(135deg, #0D1B2A, #1A0E2E);
border: 1px solid rgba(201, 151, 60, 0.18);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 20px rgba(13, 27, 42, 0.22);
margin-bottom: 4px;
}
.login-logo-icon { width: 34px; height: 34px; }
.login-eyebrow {
font-family: 'Outfit', sans-serif;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #A8A29E;
margin: 0;
}
.login-title {
font-family: 'Inter', 'Outfit', sans-serif;
font-size: 28px;
font-weight: 900;
letter-spacing: 2px;
text-transform: uppercase;
color: #0F0D0A;
margin: 0;
line-height: 1;
}
.login-title-gold {
background: linear-gradient(90deg, #C9973C, #A67C28);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.login-sub {
font-size: 11px;
color: #A8A29E;
margin: 0;
font-weight: 400;
}
.login-sep {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(15, 13, 10, 0.08), transparent);
}
/* ── Form ── */
.login-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.field {
display: flex;
flex-direction: column;
gap: 7px;
}
.field-lbl {
display: flex;
align-items: center;
gap: 6px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #78716C;
}
.field-lbl-icon {
width: 12px;
height: 12px;
color: #A8A29E;
flex-shrink: 0;
}
.field-input {
height: 54px;
width: 100%;
padding: 0 22px;
background: #F5F4F1;
border: 1px solid rgba(15, 13, 10, 0.12);
border-radius: 9999px;
color: #0F0D0A;
font-family: 'Outfit', sans-serif;
font-size: 14px;
font-weight: 400;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.field-input::placeholder { color: #A8A29E; }
.field-input:focus {
border-color: #C9973C;
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
background: #FFFFFF;
}
/* ── CTA Button ── */
.btn-cta {
width: 100%;
height: 54px;
border-radius: 9999px;
background: linear-gradient(135deg, #0D1B2A, #1A0E2E);
border: none;
color: #fff;
font-family: 'Inter', sans-serif;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.16em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 20px rgba(13, 27, 42, 0.25);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 4px;
}
.btn-cta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(13, 27, 42, 0.35);
}
.btn-cta:active { transform: translateY(0); }
.btn-cta:disabled { opacity: 0.65; cursor: not-allowed; transform: none; }
/* ── Error ── */
.login-error {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: rgba(220, 38, 38, 0.06);
border: 1px solid rgba(220, 38, 38, 0.14);
border-radius: 12px;
color: #B91C1C;
font-size: 12px;
font-weight: 500;
animation: fadeUp 0.3s ease;
}
.login-error svg { width: 15px; height: 15px; flex-shrink: 0; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ── SSO Divider ── */
.sso-divider {
display: flex;
align-items: center;
gap: 10px;
}
.sso-line {
flex: 1;
height: 1px;
background: rgba(15, 13, 10, 0.08);
}
.sso-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #A8A29E;
white-space: nowrap;
}
/* ── SSO Button ── */
.btn-sso {
width: 100%;
height: 52px;
border-radius: 9999px;
background: #F5F4F1;
border: 1px solid rgba(15, 13, 10, 0.10);
display: flex;
align-items: center;
gap: 14px;
padding: 0 20px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
font-family: 'Outfit', sans-serif;
}
.btn-sso:hover {
background: #EEECE7;
border-color: rgba(201, 151, 60, 0.25);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(15, 13, 10, 0.08);
}
.btn-sso:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.ms-logo { width: 20px; height: 20px; flex-shrink: 0; }
.sso-text { flex: 1; text-align: left; }
.sso-provider {
display: block;
font-size: 13px;
font-weight: 600;
color: #0F0D0A;
}
.sso-hint {
display: block;
font-size: 10px;
color: #A8A29E;
}
.sso-arr {
width: 16px;
height: 16px;
color: #A8A29E;
flex-shrink: 0;
}
/* ── Security Footer ── */
.login-security {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #D6D3D1;
}
.login-security svg { width: 11px; height: 11px; color: #D6D3D1; }
@media (max-width: 480px) {
.login-card { padding: 28px 22px; }
.login-title { font-size: 22px; letter-spacing: 1px; }
}

View File

@ -0,0 +1,135 @@
<div class="login-page">
<!-- Ambient glows -->
<div class="glow-tl"></div>
<div class="glow-br"></div>
<div class="login-card">
<!-- Brand -->
<div class="login-brand">
<div class="login-logo-wrap">
<!-- K logotipo 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="M17 6H26" stroke="white" stroke-width="3.5" stroke-linecap="round"/>
<path d="M17 28H26" stroke="white" stroke-width="3.5" stroke-linecap="round"/>
</svg>
</div>
<p class="login-eyebrow">Portal Corporativo</p>
<h1 class="login-title">KINETIX <span class="login-title-gold">ECO</span></h1>
<p class="login-sub">Consorcio Empresarial · Intranet 2026</p>
</div>
<div class="login-sep"></div>
<!-- Form -->
<form class="login-form" (submit)="onSubmit($event)" novalidate>
<div class="field">
<label class="field-lbl">
<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"/>
<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>
Usuario corporativo
</label>
<input
id="login-email"
type="email"
class="field-input"
placeholder="nombre@kinetix.com"
[value]="email()"
(input)="email.set(getVal($event))"
autocomplete="email"
/>
</div>
<div class="field">
<label class="field-lbl">
<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"/>
<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"/>
</svg>
Contraseña
</label>
<input
id="login-password"
type="password"
class="field-input"
placeholder="••••••••••"
[value]="password()"
(input)="password.set(getVal($event))"
autocomplete="current-password"
/>
</div>
<button id="btn-login" type="submit" class="btn-cta" [disabled]="loading()">
@if (loading()) {
<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"/>
<path d="M12 2a10 10 0 0110 10" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
</svg>
Autenticando...
} @else {
<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"/>
</svg>
Ingresar a la Intranet
}
</button>
</form>
<!-- Error -->
@if (errorMsg()) {
<div class="login-error" role="alert">
<svg viewBox="0 0 16 16" fill="none">
<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"/>
</svg>
{{ errorMsg() }}
</div>
}
<!-- SSO Divider -->
<div class="sso-divider">
<span class="sso-line"></span>
<span class="sso-label">O acceder con</span>
<span class="sso-line"></span>
</div>
<!-- Microsoft SSO -->
<button id="btn-sso" type="button" class="btn-sso" (click)="loginSSO()" [disabled]="loading()">
<!-- Microsoft logo -->
<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="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="12.5" y="12.5" width="10.5" height="10.5" fill="#ffb900"/>
</svg>
<div class="sso-text">
<span class="sso-provider">Microsoft Entra ID</span>
<span class="sso-hint">Active Directory SSO · Inicio único</span>
</div>
<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"/>
</svg>
</button>
<!-- Security footer -->
<div class="login-security">
<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="M5 8l2 2 4-4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Acceso restringido · SSL/TLS · Políticas Entra ID
</div>
</div>
</div>
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>

View File

@ -0,0 +1,76 @@
import { Component, signal, inject } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule],
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
private router = inject(Router);
email = signal('');
password = signal('');
errorMsg = signal<string>('');
loading = signal(false);
getVal(event: Event): string {
return (event.target as HTMLInputElement).value;
}
onSubmit(event: Event) {
event.preventDefault();
this.loginLocal();
}
loginLocal() {
const em = this.email();
const pw = this.password();
if (!em || !pw) {
this.errorMsg.set('Por favor ingresa tu correo y contraseña corporativos.');
return;
}
this.loading.set(true);
// Simulate authentication delay
setTimeout(() => {
if (em.includes('admin') || em.includes('kinetix') || em.includes('@')) {
const userName = em.split('@')[0];
const displayName = userName.charAt(0).toUpperCase() + userName.slice(1);
localStorage.setItem('kinetix_token', 'local-mock-jwt-token-998822');
localStorage.setItem('kinetix_user', JSON.stringify({
name: displayName,
email: em,
role: 'Consortium Manager',
avatar: displayName.substring(0, 2).toUpperCase()
}));
this.router.navigate(['/dashboard']);
} else {
this.errorMsg.set('Credenciales no válidas. Prueba con Active Directory SSO.');
this.loading.set(false);
}
}, 600);
}
loginSSO() {
this.loading.set(true);
setTimeout(() => {
localStorage.setItem('kinetix_token', 'ad-jwt-token-microsoft-entra-id-2026');
localStorage.setItem('kinetix_user', JSON.stringify({
name: 'Jess Miller',
email: 'jess.miller@kinetix-consortium.com',
role: 'Chief Architect & Director',
avatar: 'JM'
}));
this.router.navigate(['/dashboard']);
}, 500);
}
}

View File

@ -1,4 +1,5 @@
import { Component, signal, computed } from '@angular/core'; import { Component, signal, computed, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
interface Employee { interface Employee {
@ -12,6 +13,13 @@ interface Employee {
tags: string[]; tags: string[];
} }
interface UserProfile {
name: string;
email: string;
role: string;
avatar: string;
}
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
standalone: true, standalone: true,
@ -19,137 +27,139 @@ interface Employee {
template: ` template: `
<div class="flex flex-col lg:flex-row gap-6 p-2 lg:p-6 min-h-[80vh]"> <div class="flex flex-col lg:flex-row gap-6 p-2 lg:p-6 min-h-[80vh]">
<!-- Sidebar Navigation Menu (Cyberpunk theme) --> <!-- Sidebar Navigation Menu (Cyberpunk theme) -->
<aside class="w-full lg:w-64 flex flex-col gap-3">
<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.bg-white-5]="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> Ecosistema 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">Versión:</span>
<span class="text-white/30">v2.4.0 (Neon)</span>
</div>
</div>
</aside>
<!-- Main Workstation View Area --> <!-- Main Workstation View Area -->
<main class="flex-grow flex flex-col gap-6"> <main class="flex-grow flex flex-col gap-6">
<!-- ================= PAGE 1: INICIO ================= --> <!-- ================= PAGE 1: INICIO (CONSORCIO) ================= -->
@if (activeTab() === 'inicio') { @if (activeTab() === 'inicio') {
<div class="flex flex-col gap-8 animate-fadeIn"> <div style="display:flex;flex-direction:column;gap:32px;" class="animate-fadeIn">
<!-- Hero banner style layout -->
<div class="glass-panel p-8 rounded-3xl relative overflow-hidden flex flex-col gap-4 bg-gradient-to-br from-[#0c0f1d] to-[#080a12] border-white/5">
<div class="absolute -right-20 -top-20 w-64 h-64 bg-cyan-500/10 rounded-full blur-[100px]"></div>
<div class="absolute -left-20 -bottom-20 w-64 h-64 bg-magenta-500/10 rounded-full blur-[100px]"></div>
<span class="text-neon-cyan text-xs font-bold uppercase tracking-widest">BIENVENIDO AL FUTURO</span> <!-- Hero Banner -->
<h2 class="text-4xl lg:text-5xl font-black tracking-tight text-white leading-none">
Ecosistema <span class="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-500 to-indigo-500">Kinetix</span>
</h2>
<p class="text-slate-400 text-base max-w-2xl leading-relaxed">
La evolución de la intranet corporativa: Automatización de procesos, diseño premium Neón de alta fidelidad y autogestión corporativa integral para equipos ágiles.
</p>
<div class="flex flex-wrap gap-4 mt-2">
<button (click)="setActiveTab('directory')" class="px-5 py-2.5 rounded-full btn-cyan text-xs uppercase font-extrabold tracking-wider"> <!-- KPI Row (light cards) -->
Explorar Directorio <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;">
</button>
<button (click)="setActiveTab('spaces')" class="px-5 py-2.5 rounded-full border border-white/10 hover:border-white/20 transition text-xs uppercase font-extrabold tracking-wider bg-white/5 text-white"> <div style="background:#fff;border:1px solid rgba(15,13,10,0.08);border-radius:16px;padding:20px 22px;border-top:3px solid #C9973C;box-shadow:0 1px 6px rgba(15,13,10,0.06);display:flex;flex-direction:column;gap:4px;">
Reservar una Sala <span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;color:#78716C;">Sedes Integradas</span>
</button> <div style="font-family:'Inter',sans-serif;font-size:30px;font-weight:900;color:#0F0D0A;line-height:1.1;">3 Sedes</div>
<span style="font-size:10px;color:#C9973C;font-weight:600;">London · Tokyo · NY</span>
</div>
<div style="background:#fff;border:1px solid rgba(15,13,10,0.08);border-radius:16px;padding:20px 22px;border-top:3px solid #3730A3;box-shadow:0 1px 6px rgba(15,13,10,0.06);display:flex;flex-direction:column;gap:4px;">
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;color:#78716C;">Directorio Activo</span>
<div style="font-family:'Inter',sans-serif;font-size:30px;font-weight:900;color:#0F0D0A;line-height:1.1;">1,450+</div>
<span style="font-size:10px;color:#3730A3;font-weight:600;">Usuarios Sincronizados</span>
</div>
<div style="background:#fff;border:1px solid rgba(15,13,10,0.08);border-radius:16px;padding:20px 22px;border-top:3px solid #065F46;box-shadow:0 1px 6px rgba(15,13,10,0.06);display:flex;flex-direction:column;gap:4px;">
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;color:#78716C;">Salas Disponibles</span>
<div style="font-family:'Inter',sans-serif;font-size:30px;font-weight:900;color:#0F0D0A;line-height:1.1;">18 Salas</div>
<span style="font-size:10px;color:#065F46;font-weight:600;">Sistemas IoT Activos</span>
</div>
<div style="background:#fff;border:1px solid rgba(15,13,10,0.08);border-radius:16px;padding:20px 22px;border-top:3px solid #7C3AED;box-shadow:0 1px 6px rgba(15,13,10,0.06);display:flex;flex-direction:column;gap:4px;">
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;color:#78716C;">Flujos de Trabajo</span>
<div style="font-family:'Inter',sans-serif;font-size:30px;font-weight:900;color:#0F0D0A;line-height:1.1;">12 Activos</div>
<span style="font-size:10px;color:#7C3AED;font-weight:600;">Reglas AD Operativas</span>
</div>
</div>
<!-- Section header -->
<div>
<h3 style="font-family:'Inter',sans-serif;font-size:20px;font-weight:900;letter-spacing:1.5px;text-transform:uppercase;color:#0F0D0A;margin:0 0 4px;">Módulos del Sistema</h3>
<p style="font-size:13px;color:#78716C;margin:0;font-weight:400;">Selecciona un módulo para acceder a su gestión</p>
</div>
<!-- 4 Jewel-Tone Blackjack Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:20px;">
<!-- Card 1: Info Hub — Deep Navy #0D1B2A -->
<div (click)="setActiveTab('workflow')"
style="position:relative;background:#0D1B2A;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(13,27,42,0.28);">
<!-- Inner decorative border -->
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.10);border-radius:12px;pointer-events:none;"></div>
<!-- Ambient top glow -->
<div style="position:absolute;top:-40px;right:-40px;width:160px;height:160px;background:radial-gradient(circle,rgba(201,151,60,0.14) 0%,transparent 70%);pointer-events:none;"></div>
<!-- Gold icon -->
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" style="position:relative;z-index:1;filter:drop-shadow(0 0 20px rgba(201,151,60,0.55));">
<path d="M24 6L42 15L24 24L6 15L24 6Z" stroke="#C9973C" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M6 24L24 33L42 24" stroke="#C9973C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 33L24 42L42 33" stroke="#C9973C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div style="position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;">
<h4 style="font-family:'Inter',sans-serif;font-size:12px;font-weight:900;letter-spacing:0.16em;text-transform:uppercase;color:#fff;margin:0;">Info Hub</h4>
<p style="font-size:12px;color:rgba(255,255,255,0.45);line-height:1.65;margin:0;font-weight:300;">Gestión de contenidos corporativos y publicación de artículos.</p>
</div> </div>
</div> </div>
<!-- Page 3: Módulos de Nueva Generación --> <!-- Card 2: Directorio — Deep Plum #1A0E2E -->
<div class="flex flex-col gap-4"> <div (click)="setActiveTab('directory')"
<h3 class="text-xl font-bold tracking-tight text-neon-cyan uppercase tracking-wide">Módulos de Nueva Generación</h3> 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 class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
<!-- Info block --> <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 class="glass-panel glass-panel-hover p-6 rounded-2xl neon-border-cyan flex flex-col gap-4"> <svg width="48" height="48" viewBox="0 0 48 48" fill="none" style="position:relative;z-index:1;filter:drop-shadow(0 0 20px rgba(167,139,250,0.45));">
<div class="w-12 h-12 rounded-xl bg-cyan-500/10 flex items-center justify-center text-2xl text-cyan-400">📚</div> <path d="M8 16V8H16" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<h4 class="text-lg font-bold text-white">Bloque de Info</h4> <path d="M40 16V8H32" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<p class="text-sm text-slate-400 leading-relaxed"> <path d="M8 32V40H16" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
Gestión de contenidos corporativos con arquitectura robusta de microservicios y publicación de artículos en tiempo récord. <path d="M40 32V40H32" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</p> <circle cx="18" cy="22" r="4" stroke="#A78BFA" stroke-width="2"/>
<circle cx="30" cy="22" r="4" stroke="#A78BFA" stroke-width="2"/>
<path d="M11 33c0-3.866 3.134-7 7-7s7 3.134 7 7" stroke="#A78BFA" stroke-width="2" stroke-linecap="round"/>
<path d="M23 33c0-3.866 3.134-7 7-7s7 3.134 7 7" stroke="#A78BFA" stroke-width="2" stroke-linecap="round"/>
</svg>
<div style="position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;">
<h4 style="font-family:'Inter',sans-serif;font-size:12px;font-weight:900;letter-spacing:0.16em;text-transform:uppercase;color:#fff;margin:0;">Directorio AD</h4>
<p style="font-size:12px;color:rgba(255,255,255,0.40);line-height:1.65;margin:0;font-weight:300;">Sincronización con Active Directory y perfiles dinámicos.</p>
</div>
</div> </div>
<!-- AD Directories --> <!-- Card 3: Workflows — Deep Viridian #0A2520 -->
<div class="glass-panel glass-panel-hover p-6 rounded-2xl neon-border-magenta flex flex-col gap-4"> <div (click)="setActiveTab('workflow')"
<div class="w-12 h-12 rounded-xl bg-magenta-500/10 flex items-center justify-center text-2xl text-magenta-400">👤</div> style="position:relative;background:#0A2520;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(10,37,32,0.30);">
<h4 class="text-lg font-bold text-white">Directorios AD</h4> <div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
<p class="text-sm text-slate-400 leading-relaxed"> <div style="position:absolute;bottom:-40px;right:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(16,185,129,0.14) 0%,transparent 70%);pointer-events:none;"></div>
Sincronización nativa instantánea con Active Directory y perfiles dinámicos inteligentes para el equipo de desarrollo. <svg width="48" height="48" viewBox="0 0 48 48" fill="none" style="position:relative;z-index:1;filter:drop-shadow(0 0 20px rgba(52,211,153,0.45));">
</p> <circle cx="10" cy="12" r="5" stroke="#34D399" stroke-width="2.5"/>
<circle cx="38" cy="12" r="5" stroke="#34D399" stroke-width="2.5"/>
<circle cx="24" cy="36" r="5" stroke="#34D399" stroke-width="2.5"/>
<path d="M15 12H33" stroke="#34D399" stroke-width="2" stroke-linecap="round"/>
<path d="M10 17V28c0 2.5 2 4.5 4.5 4.5H19" stroke="#34D399" stroke-width="2" stroke-linecap="round"/>
<path d="M38 17V28c0 2.5-2 4.5-4.5 4.5H29" stroke="#34D399" stroke-width="2" stroke-linecap="round"/>
</svg>
<div style="position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;">
<h4 style="font-family:'Inter',sans-serif;font-size:12px;font-weight:900;letter-spacing:0.16em;text-transform:uppercase;color:#fff;margin:0;">Workflows</h4>
<p style="font-size:12px;color:rgba(255,255,255,0.40);line-height:1.65;margin:0;font-weight:300;">Líneas de aprobación, revisión y publicación de contenido.</p>
</div>
</div> </div>
<!-- Self management --> <!-- Card 4: Espacios — Deep Espresso #1C0E08 -->
<div class="glass-panel glass-panel-hover p-6 rounded-2xl neon-border-purple flex flex-col gap-4"> <div (click)="setActiveTab('spaces')"
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center text-2xl text-purple-400">📅</div> 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);">
<h4 class="text-lg font-bold text-white">Autogestión</h4> <div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
<p class="text-sm text-slate-400 leading-relaxed"> <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>
Reserva fluida de salas de conferencias, oficinas temporales e incidencias con disponibilidad y alertas en tiempo real. <svg width="48" height="48" viewBox="0 0 48 48" fill="none" style="position:relative;z-index:1;filter:drop-shadow(0 0 20px rgba(251,146,60,0.50));">
</p> <rect x="6" y="10" width="36" height="32" rx="4" stroke="#FB923C" stroke-width="2.5"/>
<path d="M6 20H42" stroke="#FB923C" stroke-width="2.5" stroke-linecap="round"/>
<path d="M16 6V14" stroke="#FB923C" stroke-width="2.5" stroke-linecap="round"/>
<path d="M32 6V14" stroke="#FB923C" stroke-width="2.5" stroke-linecap="round"/>
<path d="M17 31L22 36L32 26" stroke="#FB923C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div style="position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;">
<h4 style="font-family:'Inter',sans-serif;font-size:12px;font-weight:900;letter-spacing:0.16em;text-transform:uppercase;color:#fff;margin:0;">Espacios</h4>
<p style="font-size:12px;color:rgba(255,255,255,0.40);line-height:1.65;margin:0;font-weight:300;">Autogestión de salas, oficinas y activos de la empresa.</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
} }
<!-- ================= PAGE 2: DIRECTORY ================= --> <!-- ================= PAGE 2: DIRECTORY ================= -->
@if (activeTab() === 'directory') { @if (activeTab() === 'directory') {
<div class="flex flex-col gap-6 animate-fadeIn"> <div class="flex flex-col gap-6 animate-fadeIn">
@ -606,7 +616,203 @@ interface Employee {
</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">
<!-- 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>
</main> </main>
</div> </div>
`, `,
@ -620,7 +826,9 @@ interface Employee {
} }
`] `]
}) })
export class DashboardComponent { export class DashboardComponent implements OnInit {
private router = inject(Router);
// Navigation active tab signal // Navigation active tab signal
activeTab = signal<'inicio' | 'directory' | 'workflow' | 'spaces' | 'metrics' | 'admin'>('inicio'); activeTab = signal<'inicio' | 'directory' | 'workflow' | 'spaces' | 'metrics' | 'admin'>('inicio');
@ -633,6 +841,14 @@ export class DashboardComponent {
simulationStatus = signal<string>(''); simulationStatus = signal<string>('');
approvalSuccess = signal<string>(''); approvalSuccess = signal<string>('');
// Sincronized Active Directory Active Profile signal
currentUser = signal<UserProfile>({
name: 'Invitado',
email: 'guest@kinetix.com',
role: 'Employee',
avatar: 'G'
});
// Active Directory mock database of employees exactly styled as Slide 4 // Active Directory mock database of employees exactly styled as Slide 4
employees = signal<Employee[]>([ employees = signal<Employee[]>([
{ {
@ -704,6 +920,19 @@ export class DashboardComponent {
}); });
}); });
ngOnInit() {
// Load Active Directory profile from local storage
const userStr = localStorage.getItem('kinetix_user');
if (userStr) {
try {
const user = JSON.parse(userStr);
this.currentUser.set(user);
} catch (e) {
console.error('Error parsing user profile from local storage', e);
}
}
}
setActiveTab(tab: 'inicio' | 'directory' | 'workflow' | 'spaces' | 'metrics' | 'admin') { setActiveTab(tab: 'inicio' | 'directory' | 'workflow' | 'spaces' | 'metrics' | 'admin') {
this.activeTab.set(tab); this.activeTab.set(tab);
// Reset secondary flags // Reset secondary flags
@ -749,4 +978,10 @@ export class DashboardComponent {
this.approvalSuccess.set(''); this.approvalSuccess.set('');
}, 4000); }, 4000);
} }
logout() {
localStorage.removeItem('kinetix_token');
localStorage.removeItem('kinetix_user');
this.router.navigate(['/login']);
}
} }

View File

@ -0,0 +1,177 @@
<div class="flex flex-col md:flex-row h-screen w-full bg-[#F5F4F1] text-[#0F0D0A] font-sans overflow-hidden">
<!-- Sidebar de Filtros Dinámicos (Estilo Glassmorphism / Kinetix) -->
<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">
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3">
<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 class="material-icons font-bold text-lg">business_center</span>
</div>
<div>
<h2 class="text-lg font-bold tracking-tight text-[#0F0D0A]">Reservas</h2>
<p class="text-xs text-[#78716C]">Autogestión de Espacios e IA</p>
</div>
</div>
<!-- Filtros Rápidos -->
<div class="flex flex-col gap-2">
<label class="text-xs font-bold uppercase tracking-wider text-[#78716C] mb-1">Categorías</label>
<button
(click)="setFilter('all')"
[class.bg-[#0D1B2A]]="selectedFilter() === 'all'"
[class.text-white]="selectedFilter() === 'all'"
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-[#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>
<!-- Botón directo de Reserva IA -->
<button
[routerLink]="['/reservas/nuevo']"
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">
<span class="material-icons text-sm">smart_toy</span>
<span>Asistente IA</span>
</button>
</aside>
<!-- Panel del Calendario (Full Screen Layout) -->
<main class="flex-1 flex flex-col h-full overflow-hidden p-6 gap-6">
<!-- Cabecera del Calendario -->
<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="flex items-center gap-4">
<button (click)="prev()" 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">&lt;</span>
</button>
<h1 class="text-xl font-bold tracking-tight text-[#0F0D0A] capitalize min-w-[150px] text-center">
{{ monthYearLabel() }}
</h1>
<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">&gt;</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>
<!-- Vistas de visualización (Día, Semana, Mes) -->
<div class="flex bg-[#F0EEE9] p-1 rounded-full border border-[#0F0D0A]/5">
<button
(click)="setView('month')"
[class.bg-white]="currentView() === 'month'"
[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>
</header>
<!-- Grid de Calendario Full-Screen (Modo Mes) -->
<div class="flex-1 bg-white border border-[#0F0D0A]/10 rounded-2xl shadow-sm flex flex-col overflow-hidden">
<!-- Días de la semana -->
<div class="grid grid-cols-7 bg-[#F0EEE9]/50 border-b border-[#0F0D0A]/10 shrink-0">
@for (dayName of weekDays; track $index) {
<div class="py-3 text-center text-xs font-bold tracking-wider text-[#78716C] uppercase border-r border-[#0F0D0A]/10 last:border-r-0">
{{ dayName }}
</div>
}
</div>
<!-- Cuadrícula de Celdas -->
<div class="grid grid-cols-7 grid-rows-6 flex-1 divide-x divide-y divide-[#0F0D0A]/10">
@for (day of calendarDays(); track day.getTime()) {
<div
(click)="selectDay(day)"
[class.bg-slate-50]="!isCurrentMonth(day)"
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>
<!-- Hint de clic al pasar cursor -->
<span class="absolute bottom-1 right-2 text-[9px] text-[#C9973C] opacity-0 group-hover:opacity-100 transition-opacity font-semibold">
+ Reservar
</span>
</div>
}
</div>
</div>
</main>
</div>

View File

@ -0,0 +1,160 @@
import { Component, OnInit, signal, computed, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterModule } from '@angular/router';
export interface CalendarEvent {
id: string;
title: string;
start: Date;
end: Date;
type: 'room' | 'desk' | 'asset';
resourceName: string;
colorClass: string;
}
@Component({
selector: 'app-calendar-booking',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './calendar-booking.component.html',
styleUrls: []
})
export class CalendarBookingComponent implements OnInit {
private router = inject(Router);
// Signals para gestionar estado reactivamente
currentView = signal<'month' | 'week' | 'day'>('month');
currentDate = signal<Date>(new Date());
selectedFilter = signal<'all' | 'room' | 'desk' | 'asset'>('all');
// Eventos de prueba
events = signal<CalendarEvent[]>([
{
id: '1',
title: 'Reunión de Sincronización Mensual',
start: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 10, 0),
end: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate(), 11, 30),
type: 'room',
resourceName: 'Sala Alpha',
colorClass: 'border-l-4 border-indigo-600 bg-indigo-50/50 text-indigo-900 dark:border-indigo-500 dark:bg-indigo-950/20 dark:text-indigo-200'
},
{
id: '2',
title: 'Reserva de Escritorio Diario',
start: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() + 1, 9, 0),
end: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() + 1, 18, 0),
type: 'desk',
resourceName: 'Escritorio Standby 04',
colorClass: 'border-l-4 border-amber-600 bg-amber-50/50 text-amber-900 dark:border-amber-500 dark:bg-amber-950/20 dark:text-amber-200'
},
{
id: '3',
title: 'Préstamo MacBook Pro para Demo',
start: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - 1, 14, 0),
end: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - 1, 16, 0),
type: 'asset',
resourceName: 'MacBook Pro M3',
colorClass: 'border-l-4 border-emerald-600 bg-emerald-50/50 text-emerald-900 dark:border-emerald-500 dark:bg-emerald-950/20 dark:text-emerald-200'
}
]);
// Lista de días de la semana
weekDays = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
// Nombre legible del mes y año actual
monthYearLabel = computed(() => {
const date = this.currentDate();
return date.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' });
});
// Días a renderizar en la cuadrícula mensual del calendario
calendarDays = computed(() => {
const date = this.currentDate();
const year = date.getFullYear();
const month = date.getMonth();
const firstDayIndex = new Date(year, month, 1).getDay();
const totalDays = new Date(year, month + 1, 0).getDate();
const days: Date[] = [];
// Agregar días de la última semana del mes anterior
const prevMonthTotalDays = new Date(year, month, 0).getDate();
for (let i = firstDayIndex - 1; i >= 0; i--) {
days.push(new Date(year, month - 1, prevMonthTotalDays - i));
}
// Agregar días del mes actual
for (let i = 1; i <= totalDays; i++) {
days.push(new Date(year, month, i));
}
// Completar el grid hasta 42 posiciones (6 filas)
const nextDaysCount = 42 - days.length;
for (let i = 1; i <= nextDaysCount; i++) {
days.push(new Date(year, month + 1, i));
}
return days;
});
// Filtros aplicados
filteredEvents = computed(() => {
const filter = this.selectedFilter();
if (filter === 'all') return this.events();
return this.events().filter(e => e.type === filter);
});
ngOnInit(): void {}
prev(): void {
const d = this.currentDate();
this.currentDate.set(new Date(d.getFullYear(), d.getMonth() - 1, 1));
}
next(): void {
const d = this.currentDate();
this.currentDate.set(new Date(d.getFullYear(), d.getMonth() + 1, 1));
}
today(): void {
this.currentDate.set(new Date());
}
setFilter(filter: 'all' | 'room' | 'desk' | 'asset'): void {
this.selectedFilter.set(filter);
}
setView(view: 'month' | 'week' | 'day'): void {
this.currentView.set(view);
}
selectDay(day: Date): void {
const dateStr = day.toISOString().split('T')[0];
this.router.navigate(['/reservas/nuevo'], {
queryParams: {
fecha: dateStr,
tipo: this.selectedFilter() !== 'all' ? this.selectedFilter() : 'room'
}
});
}
getEventsForDay(day: Date): CalendarEvent[] {
return this.filteredEvents().filter(e => {
return e.start.getFullYear() === day.getFullYear() &&
e.start.getMonth() === day.getMonth() &&
e.start.getDate() === day.getDate();
});
}
isToday(day: Date): boolean {
const today = new Date();
return day.getDate() === today.getDate() &&
day.getMonth() === today.getMonth() &&
day.getFullYear() === today.getFullYear();
}
isCurrentMonth(day: Date): boolean {
return day.getMonth() === this.currentDate().getMonth();
}
}

View File

@ -0,0 +1,207 @@
<div class="min-h-screen bg-[#F5F4F1] text-[#0F0D0A] font-sans p-6 flex items-center justify-center overflow-y-auto">
<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">
<!-- Lado Izquierdo: Información y Asistente IA -->
<div class="w-full md:w-[40%] bg-[#0D1B2A] text-white p-8 flex flex-col justify-between relative">
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-[#C9973C] to-[#A67C28] flex items-center justify-center shadow-md">
<span class="material-icons text-white">smart_toy</span>
</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">
Agenda con lenguaje natural
</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 -->
<div class="flex flex-col gap-3 mt-6">
<label class="text-[10px] uppercase tracking-wider text-slate-400 font-bold">Prompt de IA</label>
<div class="relative">
<textarea
#promptInput
rows="4"
placeholder="Ej: Necesito reservar una sala para 6 personas mañana a las 3:00 pm con proyector..."
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>
</div>
<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>
<!-- Pie de Asistente -->
<div class="mt-8 text-[10px] text-slate-400 border-t border-white/5 pt-4">
Diseñado bajo el estándar de IA Corporativa de Kinetix.
</div>
</div>
<!-- Lado Derecho: Formulario Estructurado -->
<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">
&larr; Volver al Calendario
</a>
</header>
<!-- Mensaje de éxito de la IA -->
@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">
<span class="material-icons text-sm">check_circle</span>
<span>{{ aiSuccessMessage() }}</span>
</div>
}
<div class="flex flex-col gap-4">
<!-- Título de la reserva -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Asunto / Título de la Reunión</label>
<input
type="text"
formControlName="title"
placeholder="Ej: Sprint Planning Q3"
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('title')?.touched && bookingForm.get('title')?.invalid) {
<span class="text-[10px] text-red-500 font-semibold">El título es requerido (mínimo 3 caracteres)</span>
}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Tipo de Recurso -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Tipo de Recurso</label>
<select
formControlName="resourceType"
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">
<option value="room">Sala de Juntas</option>
<option value="desk">Escritorio / Oficina</option>
<option value="asset">Activo / Dispositivo</option>
</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
type="text"
formControlName="resourceName"
placeholder="Ej: Sala Alpha, Escritorio 4B, Laptop"
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('resourceName')?.touched && bookingForm.get('resourceName')?.invalid) {
<span class="text-[10px] text-red-500 font-semibold">El nombre del recurso es requerido</span>
}
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Fecha -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Fecha</label>
<input
type="date"
formControlName="date"
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('date')?.touched && bookingForm.get('date')?.invalid) {
<span class="text-[10px] text-red-500 font-semibold">Requerido</span>
}
</div>
<!-- Hora Inicio -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Hora Inicio</label>
<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>
</div>
</div>
<!-- Notas / Comentarios -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-[#78716C]">Notas Adicionales</label>
<textarea
formControlName="notes"
rows="3"
placeholder="Comentarios adicionales o solicitudes especiales..."
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>
</div>
</div>
<div class="flex gap-4 mt-4 shrink-0">
<a
[routerLink]="['/reservas/calendario']"
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">
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>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,100 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IaParserService, IaParsedBooking } from '../services/ia-parser.service';
@Component({
selector: 'app-reserva-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterModule],
templateUrl: './reserva-form.component.html',
styleUrls: []
})
export class ReservaFormComponent implements OnInit {
private fb = inject(FormBuilder);
private route = inject(ActivatedRoute);
private router = inject(Router);
private iaParser = inject(IaParserService);
// Exponer Signals del servicio IA a la plantilla
isLoadingIa = this.iaParser.isLoading;
// Reactivo Form
bookingForm!: FormGroup;
// Estado del asistente
aiPrompt = signal<string>('');
aiSuccessMessage = signal<string>('');
ngOnInit(): void {
this.initForm();
// Capturar QueryParams desde la URL
this.route.queryParams.subscribe(params => {
const fecha = params['fecha'] || '';
const tipo = params['tipo'] || 'room';
this.bookingForm.patchValue({
date: fecha,
resourceType: tipo
});
});
}
private initForm(): void {
this.bookingForm = this.fb.group({
title: ['', [Validators.required, Validators.minLength(3)]],
resourceType: ['room', [Validators.required]],
resourceName: ['', [Validators.required]],
date: ['', [Validators.required]],
startTime: ['', [Validators.required]],
endTime: ['', [Validators.required]],
attendees: [1, [Validators.required, Validators.min(1)]],
hasProjector: [false],
notes: ['']
});
}
/**
* Envía el prompt de lenguaje natural del Asistente IA para parsear y auto-llenar los campos.
*/
processAiPrompt(promptText: string): void {
if (!promptText.trim()) return;
this.iaParser.parseNaturalLanguage(promptText).subscribe({
next: (parsedData: IaParsedBooking) => {
// Rellenar formulario reactivamente con los datos estructurados devueltos por la IA
this.bookingForm.patchValue({
title: parsedData.title || this.bookingForm.get('title')?.value,
resourceType: parsedData.resourceType || this.bookingForm.get('resourceType')?.value,
resourceName: parsedData.resourceName || this.bookingForm.get('resourceName')?.value,
date: parsedData.date || this.bookingForm.get('date')?.value,
startTime: parsedData.startTime || this.bookingForm.get('startTime')?.value,
endTime: parsedData.endTime || this.bookingForm.get('endTime')?.value,
attendees: parsedData.attendees || this.bookingForm.get('attendees')?.value,
hasProjector: parsedData.hasProjector !== undefined ? parsedData.hasProjector : this.bookingForm.get('hasProjector')?.value,
notes: parsedData.notes || this.bookingForm.get('notes')?.value
});
this.aiSuccessMessage.set('¡Los campos del formulario se han pre-llenado automáticamente gracias al Asistente IA!');
setTimeout(() => this.aiSuccessMessage.set(''), 5000);
},
error: (err) => {
console.error('Error procesando prompt con IA:', err);
}
});
}
onSubmit(): void {
if (this.bookingForm.invalid) {
this.bookingForm.markAllAsTouched();
return;
}
console.log('Reserva creada exitosamente:', this.bookingForm.value);
// Regresar al calendario
this.router.navigate(['/reservas/calendario']);
}
}

View File

@ -0,0 +1,122 @@
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
export interface IaParsedBooking {
title?: string;
date?: string; // YYYY-MM-DD
startTime?: string; // HH:mm
endTime?: string; // HH:mm
attendees?: number;
resourceType?: 'room' | 'desk' | 'asset';
resourceName?: string;
hasProjector?: boolean;
notes?: string;
}
@Injectable({
providedIn: 'root'
})
export class IaParserService {
private http = inject(HttpClient);
private apiUrl = 'api/ai/parse-booking';
// Signals para gestionar el estado reactivamente (Angular 18)
isLoading = signal<boolean>(false);
lastParsedResult = signal<IaParsedBooking | null>(null);
/**
* Envía el prompt de lenguaje natural al backend o genera un mock en caso de fallo de red.
*/
parseNaturalLanguage(prompt: string): Observable<IaParsedBooking> {
this.isLoading.set(true);
return ((this.http.post<IaParsedBooking>(this.apiUrl, { prompt }) as any).pipe(
tap((result: any) => {
this.lastParsedResult.set(result);
this.isLoading.set(false);
}),
catchError((error: any) => {
console.warn('Conexión con el servidor backend IA simulada debido al entorno de desarrollo:', error.message);
// Fallback a un NLP simulado con alta fidelidad para el MVP
const mockResult = this.generateMockResponse(prompt);
this.lastParsedResult.set(mockResult);
this.isLoading.set(false);
return of(mockResult);
})
) as any);
}
/**
* Parser simulado de lenguaje natural basado en heurísticas de palabras clave.
*/
private generateMockResponse(prompt: string): IaParsedBooking {
const p = prompt.toLowerCase();
const result: IaParsedBooking = {};
// Determinar el tipo de recurso
if (p.includes('sala') || p.includes('reunión') || p.includes('juntas') || p.includes('boardroom')) {
result.resourceType = 'room';
result.resourceName = p.includes('alpha') ? 'Sala Alpha' : p.includes('beta') ? 'Sala Beta' : 'Sala Ejecutiva';
} else if (p.includes('escritorio') || p.includes('oficina') || p.includes('mesa') || p.includes('desk')) {
result.resourceType = 'desk';
result.resourceName = 'Escritorio Standby 04';
} else {
result.resourceType = 'asset';
result.resourceName = p.includes('proyector') ? 'Proyector Epson 4K' : p.includes('laptop') ? 'MacBook Pro M3' : 'Pizarra Inteligente';
}
if (p.includes('proyector')) {
result.hasProjector = true;
}
// Extraer número de asistentes / personas
const peopleMatch = p.match(/(\d+)\s*(personas|invitados|asistentes|miembros)/);
if (peopleMatch) {
result.attendees = parseInt(peopleMatch[1], 10);
} else {
result.attendees = p.includes('grupo') ? 8 : 1;
}
// Extraer fecha
const targetDate = new Date();
if (p.includes('mañana')) {
targetDate.setDate(targetDate.getDate() + 1);
} else if (p.includes('pasado mañana')) {
targetDate.setDate(targetDate.getDate() + 2);
} else if (p.includes('hoy')) {
// mantiene hoy
} else {
// Si menciona un día de la semana (ej. lunes, martes)
const daysOfWeek = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const foundDay = daysOfWeek.findIndex(day => p.includes(day));
if (foundDay !== -1) {
const currentDay = targetDate.getDay();
let distance = foundDay - currentDay;
if (distance <= 0) distance += 7;
targetDate.setDate(targetDate.getDate() + distance);
}
}
result.date = targetDate.toISOString().split('T')[0];
// Extraer horarios aproximados
if (p.includes('tarde') || p.includes('pm')) {
result.startTime = '15:00';
result.endTime = '16:30';
} else if (p.includes('mañana') && !p.includes('mañana por')) {
result.startTime = '09:30';
result.endTime = '11:00';
} else if (p.includes('noche')) {
result.startTime = '19:00';
result.endTime = '20:30';
} else {
result.startTime = '11:00';
result.endTime = '12:00';
}
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.`;
return result;
}
}

View File

@ -1,140 +1,401 @@
/* Root Styles for Kinetix Neon Theme */ /* ============================================================
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); KINETIX INTRANET Executive Suite Design System
Paleta: Fondo blanco, tarjetas joya sofisticadas
============================================================ */
/* Fonts: Inter (titulares) + Outfit (cuerpo) */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;900&family=Inter:wght@400;600;700;800;900&display=swap');
/* ── Design Tokens ─────────────────────────────────────── */
:root { :root {
font-family: 'Inter', sans-serif; /* ── Fondo y superficies (LIGHT MODE) ── */
color-scheme: dark; --bg-page: #F5F4F1; /* Blanco cálido / ivory */
--bg-surface: #FFFFFF; /* Superficie limpia */
--bg-surface-2: #F0EEE9; /* Superficies secundarias */
/* HSL Neon Color Palette */ /* ── Colores joya para tarjetas sofisticadas ── */
--bg-deep-dark: HSL(240, 15%, 4%); --card-navy: #0D1B2A; /* Azul marino profundo */
--bg-card: HSL(240, 12%, 8%); --card-plum: #1A0E2E; /* Ciruela / Violeta oscuro */
--accent-cyan: HSL(180, 100%, 50%); --card-viridian: #0A2520; /* Verde Viridian / Bosque profundo */
--accent-magenta: HSL(320, 100%, 50%); --card-espresso: #1C0E08; /* Espresso / Marrón ébano */
--accent-purple: HSL(262, 80%, 60%);
--accent-green: HSL(140, 100%, 50%); /* ── Acentos elegantes ── */
--gold-luxe: #C9973C; /* Oro antiguo (visible en blanco) */
--gold-dark: #A67C28;
--gold-light: #F0C060; /* Destellos dorados en tarjetas */
--indigo-rich: #3730A3; /* Índigo profundo */
--sapphire: #1E3A8A; /* Zafiro */
--emerald-deep: #065F46; /* Esmeralda profundo */
/* ── Neon (login / accents) ── */
--neon-blue: #0EA5E9;
--neon-cyan: #06B6D4;
/* ── Textos (dark on white) ── */
--text-primary: #0F0D0A; /* Negro cálido */
--text-secondary: #44403C; /* Marrón oscuro */
--text-muted: #78716C; /* Gris cálido */
--text-placeholder: #A8A29E;
/* ── Bordes / divisores ── */
--border-light: rgba(15, 13, 10, 0.08);
--border-medium: rgba(15, 13, 10, 0.14);
--border-gold: rgba(201, 151, 60, 0.30);
/* Legacy aliases */
--bg-dark: #0D1B2A;
--bg-card: rgba(17, 22, 41, 0.70);
--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;
}
/* ── Base ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
body { body {
margin: 0; margin: 0;
background-color: var(--bg-deep-dark); font-family: 'Outfit', 'Inter', sans-serif;
background-image: radial-gradient(circle at 50% -20%, rgba(0, 242, 254, 0.07) 0%, rgba(240, 12, 147, 0.03) 50%, transparent 100%); font-weight: 400;
color: #f3f4f6; background: var(--bg-page);
overflow-x: hidden; color: var(--text-primary);
min-height: 100vh; min-height: 100vh;
overflow-x: hidden;
} }
/* Glassmorphism Panel (Vercel/Neon style) */ /* ── Typography ─────────────────────────────────────────── */
h1, h2, h3, h4, .font-display {
font-family: 'Inter', 'Outfit', sans-serif;
font-weight: 900;
margin: 0;
}
/* ── Glass Panel (adaptado a fondo blanco) ─────────────── */
.glass-panel { .glass-panel {
background: rgba(15, 15, 22, 0.65); background: var(--bg-surface);
backdrop-filter: blur(14px); border: 1px solid var(--border-light);
border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 16px;
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5); box-shadow:
transition: all 0.30s cubic-bezier(0.4, 0, 0.2, 1); 0 1px 3px rgba(15, 13, 10, 0.06),
0 8px 24px rgba(15, 13, 10, 0.06);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
} }
.glass-panel-hover:hover { .glass-panel-hover:hover {
transform: translateY(-2px); transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.12); border-color: var(--border-medium);
box-shadow: 0 16px 48px 0 rgba(0, 0, 0, 0.6); box-shadow:
0 2px 6px rgba(15, 13, 10, 0.08),
0 12px 32px rgba(15, 13, 10, 0.10);
} }
/* Neon Borders and Accents */ /* ── Tarjetas Joya (Blackjack) ─────────────────────────── */
.neon-border-cyan { /* Clase base compartida — el color de fondo se pasa por variable */
border: 1px solid rgba(0, 242, 254, 0.15); .card-blackjack {
} position: relative;
.neon-border-cyan:hover { border-radius: 20px;
border-color: var(--accent-cyan); overflow: hidden;
box-shadow: 0 0 15px rgba(0, 242, 254, 0.2); transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
cursor: pointer;
} }
.neon-border-magenta { .card-blackjack::before {
border: 1px solid rgba(240, 12, 147, 0.15); content: '';
} position: absolute;
.neon-border-magenta:hover { inset: 10px;
border-color: var(--accent-magenta); border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 0 15px rgba(240, 12, 147, 0.2); border-radius: 12px;
pointer-events: none;
z-index: 0;
transition: border-color 0.2s;
} }
.neon-border-purple { .card-blackjack:hover {
border: 1px solid rgba(124, 58, 237, 0.15); transform: translateY(-5px) scale(1.02);
} box-shadow:
.neon-border-purple:hover { 0 20px 60px rgba(0, 0, 0, 0.35),
border-color: var(--accent-purple); 0 0 0 1px rgba(255, 255, 255, 0.08);
box-shadow: 0 0 15px rgba(124, 58, 237, 0.2); }
.card-blackjack:hover::before {
border-color: rgba(255, 255, 255, 0.22);
}
/* ── KPI Cards (light mode) ────────────────────────────── */
.kpi-card {
background: var(--bg-surface);
border: 1px solid var(--border-light);
border-radius: 14px;
padding: 20px 22px;
display: flex;
flex-direction: column;
gap: 4px;
box-shadow: 0 1px 4px rgba(15, 13, 10, 0.06);
transition: all 0.2s;
}
.kpi-card:hover {
border-color: var(--border-medium);
box-shadow: 0 4px 16px rgba(15, 13, 10, 0.10);
transform: translateY(-1px);
}
/* ── Borders ────────────────────────────────────────────── */
.neon-border-gold {
border: 1px solid rgba(201, 151, 60, 0.25);
}
.neon-border-gold:hover { border-color: var(--gold-luxe); }
.neon-border-blue {
border: 1px solid rgba(59, 130, 246, 0.20);
}
.neon-border-blue:hover { border-color: var(--sapphire); }
/* Legacy borders */
.neon-border-cyan { border: 1px solid rgba(14, 165, 233, 0.20); }
.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 ──────────────────────────────────── */
.text-gold {
color: var(--gold-luxe);
} }
/* Cyberpunk Typography */
.text-neon-cyan { .text-neon-cyan {
color: var(--accent-cyan); color: var(--indigo-rich);
text-shadow: 0 0 10px rgba(0, 242, 254, 0.3);
} }
.text-neon-magenta { .text-neon-magenta {
color: var(--accent-magenta); color: hsl(320, 70%, 45%);
text-shadow: 0 0 10px rgba(240, 12, 147, 0.3);
} }
.text-neon-purple { .text-neon-purple {
color: var(--accent-purple); color: var(--indigo-rich);
text-shadow: 0 0 10px rgba(124, 58, 237, 0.3);
} }
/* Inputs & Forms (rounded-full per slide 4) */ .text-muted {
.input-cyberpunk { color: var(--text-muted);
background: rgba(10, 10, 15, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
transition: all 0.3s ease;
} }
.input-cyberpunk:focus { /* ── Inputs ─────────────────────────────────────────────── */
/* Light mode input */
.input-luxe {
height: 52px;
width: 100%;
padding: 0 22px;
background: var(--bg-surface);
border: 1px solid var(--border-medium);
border-radius: 9999px;
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
font-size: 14px;
font-weight: 400;
outline: none; outline: none;
border-color: var(--accent-cyan); transition: border-color 0.2s, box-shadow 0.2s;
box-shadow: 0 0 8px rgba(0, 242, 254, 0.25); box-sizing: border-box;
background: rgba(10, 10, 15, 0.95);
} }
/* Buttons (Cyan/Purple Glow) */ .input-luxe::placeholder { color: var(--text-placeholder); }
.btn-cyan { .input-luxe:focus {
background: var(--accent-cyan); border-color: var(--gold-luxe);
color: #000; box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
font-weight: 700;
letter-spacing: 0.05em;
transition: all 0.3s ease;
box-shadow: 0 4px 14px rgba(0, 242, 254, 0.3);
} }
.btn-cyan:hover { /* Dashboard search inputs (light) */
transform: translateY(-1px); .input-cyberpunk {
box-shadow: 0 6px 20px rgba(0, 242, 254, 0.5); height: 44px;
width: 100%;
padding: 0 18px;
background: var(--bg-surface);
border: 1px solid var(--border-medium);
border-radius: 9999px;
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
font-size: 13px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
} }
.btn-purple { .input-cyberpunk::placeholder { color: var(--text-placeholder); }
background: var(--accent-purple); .input-cyberpunk:focus {
border-color: var(--gold-luxe);
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.10);
}
/* Dark input (for login, dark bg sections) */
.input-dark {
height: 54px;
width: 100%;
padding: 0 25px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 9999px;
color: #fff; color: #fff;
font-weight: 700; font-family: 'Outfit', sans-serif;
letter-spacing: 0.05em; font-size: 14px;
transition: all 0.3s ease; outline: none;
box-shadow: 0 4px 14px rgba(124, 58, 237, 0.3); transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
} }
.btn-purple:hover { .input-dark::placeholder { color: rgba(255, 255, 255, 0.35); }
transform: translateY(-1px); .input-dark:focus {
box-shadow: 0 6px 20px rgba(124, 58, 237, 0.5); border-color: rgba(201, 151, 60, 0.55);
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
} }
/* Custom Scrollbar */ /* ── Buttons ─────────────────────────────────────────────── */
::-webkit-scrollbar { /* Gold gradient (primary) */
width: 8px; .btn-gold {
height: 8px; display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 48px;
padding: 0 28px;
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;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 16px rgba(166, 124, 40, 0.30);
} }
::-webkit-scrollbar-track {
background: var(--bg-deep-dark); .btn-gold:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(166, 124, 40, 0.45);
} }
/* Dark (CTA on dark backgrounds, e.g. login) */
.btn-cyan {
display: inline-flex;
align-items: center;
justify-content: center;
height: 48px;
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: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
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); }
/* Indigo solid */
.btn-indigo {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 48px;
padding: 0 24px;
border-radius: 9999px;
background: var(--indigo-rich);
border: none;
color: #fff;
font-family: 'Inter', sans-serif;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 16px rgba(55, 48, 163, 0.30);
}
.btn-indigo:hover { transform: translateY(-2px); }
/* Ghost on white */
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 46px;
padding: 0 22px;
border-radius: 9999px;
background: transparent;
border: 1.5px solid var(--border-medium);
color: var(--text-secondary);
font-family: 'Outfit', sans-serif;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.btn-ghost:hover {
background: var(--bg-surface-2);
border-color: var(--border-medium);
color: var(--text-primary);
}
/* Legacy purple */
.btn-purple {
display: inline-flex;
align-items: center;
justify-content: center;
height: 44px;
padding: 0 20px;
border-radius: 9999px;
background: var(--indigo-rich);
border: none;
color: #fff;
font-weight: 800;
font-size: 11px;
letter-spacing: 0.10em;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s;
}
.btn-purple:hover { transform: translateY(-1px); }
/* ── Scrollbar ─────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg-surface-2); }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1); background: rgba(15, 13, 10, 0.15);
border-radius: 4px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover { background: rgba(201, 151, 60, 0.45); }
background: var(--accent-cyan);
/* ── Utilities ─────────────────────────────────────────── */
.animate-fadeIn {
animation: fadeIn 0.35s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.sr-only {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0,0,0,0); white-space: nowrap; border: 0;
} }

View File

@ -1,35 +1,63 @@
# Estándares - UI/UX Diseño Cyberpunk / Neon / Vercel # Estándares - UI/UX Diseño Executive Suite / Kinetix
Este documento establece la guía de estilos visuales de la intranet corporativa Kinetix. Este documento establece la guía de estilos visuales e interactivos de la intranet corporativa Kinetix, adaptada al **Executive Suite Design System**.
--- ---
## 1. Paleta de Colores (Estilo Neón) ## 1. Paleta de Colores (Diseño Elegante y Sofisticado)
Para lograr el estilo neón premium e interactivo, la interfaz debe utilizar un fondo oscuro profundo con bordes y brillos acentuados en colores cian, magenta y verde neón. El diseño evoluciona de un fondo oscuro a una combinación de fondos claros premium (Ivory/Blanco cálido) con tarjetas oscuras sofisticadas en tonos joya y acentos dorados/índigo.
- **Fondo Principal (Deep Dark):** HSL(240, 10%, 4%) ### Fondo y Superficies (Light Mode)
- **Fondo Secundario (Card Background):** HSL(240, 10%, 9%) - **Fondo Principal (Ivory):** `#F5F4F1`
- **Acento Primario (Cian Neón):** HSL(180, 100%, 50%) - **Superficie Principal (White):** `#FFFFFF`
- **Acento Secundario (Magenta Neón):** HSL(320, 100%, 50%) - **Superficie Secundaria (Warm Grey):** `#F0EEE9`
- **Acento de Éxito (Verde Neón):** HSL(140, 100%, 50%)
### Tarjetas Joya (Blackjack System)
Tarjetas de fondo oscuro profundo utilizadas para resaltar elementos clave:
- **Azul Marino (Navy):** `#0D1B2A`
- **Ciruela (Plum):** `#1A0E2E`
- **Verde Viridian (Forest):** `#0A2520`
- **Marrón Ébano (Espresso):** `#1C0E08`
### Acentos de Lujo y Elegancia
- **Oro Antiguo (Gold Luxe):** `#C9973C` (utilizado para botones primarios, bordes y foco)
- **Índigo Profundo (Indigo Rich):** `#3730A3` (acentos interactivos y botones secundarios)
- **Zafiro (Sapphire):** `#1E3A8A`
- **Esmeralda Profundo (Emerald Deep):** `#065F46`
--- ---
## 2. Efecto Glassmorphism (Vercel Style) ## 2. Componentes y Efecto Glassmorphism
Utilizar bordes ultra finos semitransparentes combinados con desenfoque de fondo (`backdrop-filter`) para crear profundidad tridimensional en los paneles: El efecto de cristal se adapta para ofrecer limpieza y legibilidad sobre el fondo claro, utilizando bordes sutiles y sombras suaves.
```css ```css
.glass-panel { .glass-panel {
background: rgba(15, 15, 20, 0.7); background: var(--bg-surface);
backdrop-filter: blur(12px); border: 1px solid var(--border-light);
border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); box-shadow:
0 1px 3px rgba(15, 13, 10, 0.06),
0 8px 24px rgba(15, 13, 10, 0.06);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
```
### Tarjetas Blackjack (`.card-blackjack`)
Tienen un borde interno fino, un leve zoom al pasar el cursor y sombras tridimensionales al ser interactivas:
```css
.card-blackjack {
position: relative;
border-radius: 20px;
overflow: hidden;
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
``` ```
--- ---
## 3. Micro-animaciones e Interactividad ## 3. Botones, Entradas e Interactividad
- **Hover Glow:** Los botones interactivos y tarjetas deben tener transiciones suaves (`transition: all 0.3s ease`) que agreguen un brillo sutil al pasar el cursor. - **Botón Primario (Gold):** Gradiente de oro (`#C9973C` a `#A67C28`) con sombras proyectadas.
- **Transición de Rutas:** Usar animaciones fluidas en Angular al alternar vistas. - **Botón Secundario (Indigo):** Sólido índigo (`#3730A3`) para acciones de flujo secundario.
- **Efectos de Foco:** Utilizar anillos de foco cian neón en entradas de texto y controles interactivos para garantizar accesibilidad. - **Entradas Luxe (`.input-luxe`):** Bordes completamente redondeados, foco en color oro con un anillo suave de resplandor.
- **Micro-animaciones:** Todas las transiciones de tarjetas, botones y campos interactivos deben durar entre `0.2s` y `0.35s` con curvas suavizadas (`ease` o `cubic-bezier`).
- **Tipografía Premium:** Títulos en fuente **Inter** (peso de 900 para máxima jerarquía) y texto de cuerpo en **Outfit** para asegurar elegancia y alta legibilidad.