feat: implement dashboard and authentication architecture with auth guard and login component
This commit is contained in:
parent
2baebda412
commit
a83a901bf9
8
.gitignore
vendored
8
.gitignore
vendored
@ -1 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
/backend/dist
|
||||
/backend/node_modules
|
||||
/frontend/dist
|
||||
/frontend/node_modules
|
||||
/frontend/.angular/cache
|
||||
/.vscode
|
||||
|
||||
|
||||
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/0.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/0.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/1.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/1.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/2.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/2.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/index.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/2b050c6ebdcbed732473c2c288113e53d6f794bb/index.pack
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3e780800b46bc0d4741420d2ad9c5d24a62ffcc0/4.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3e780800b46bc0d4741420d2ad9c5d24a62ffcc0/4.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3e780800b46bc0d4741420d2ad9c5d24a62ffcc0/5.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3e780800b46bc0d4741420d2ad9c5d24a62ffcc0/5.pack
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3ec86829ba397a0dacb71e905f2d158501da8fb9/0.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3ec86829ba397a0dacb71e905f2d158501da8fb9/0.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3ec86829ba397a0dacb71e905f2d158501da8fb9/index.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/3ec86829ba397a0dacb71e905f2d158501da8fb9/index.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/0.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/0.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/1.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/1.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/2.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/2.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/3.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/3.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/4.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/4.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/5.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/5.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/6.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/6.pack
vendored
Normal file
Binary file not shown.
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/index.pack
vendored
Normal file
BIN
frontend/.angular/cache/18.2.21/angular-webpack/675a08689f6127cf79b79a209ffe92c70b7fa769/index.pack
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -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
@ -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
@ -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":[]}
|
||||
@ -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
@ -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":[]}
|
||||
@ -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
@ -41,8 +41,8 @@
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
||||
@ -4,6 +4,9 @@ MIT
|
||||
@angular/core
|
||||
MIT
|
||||
|
||||
@angular/forms
|
||||
MIT
|
||||
|
||||
@angular/platform-browser
|
||||
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
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
File diff suppressed because one or more lines are too long
4
frontend/dist/kinetix-frontend/index.html
vendored
4
frontend/dist/kinetix-frontend/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<style>@import"https://fonts.googleapis.com/css2?family=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">
|
||||
<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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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))})()})();
|
||||
@ -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)}
|
||||
104
frontend/src/app/app.component.css
Normal file
104
frontend/src/app/app.component.css
Normal 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;
|
||||
}
|
||||
@ -1,27 +1,51 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet, Router } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
imports: [RouterOutlet, CommonModule],
|
||||
styleUrls: ['./app.component.css'],
|
||||
template: `
|
||||
<main class="min-h-screen flex flex-col justify-between p-6 bg-[#0a0a0c]">
|
||||
<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">
|
||||
KINETIX INTRANET
|
||||
</h1>
|
||||
<div class="text-xs text-white/50">MODULAR AGENT ARCHITECTURE</div>
|
||||
<main class="app-shell" [class.login-mode]="isLoginView()">
|
||||
|
||||
<header *ngIf="!isLoginView()" class="app-header">
|
||||
<div class="header-brand">
|
||||
<div class="header-logo">
|
||||
<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>
|
||||
|
||||
<section class="flex-grow">
|
||||
<section class="app-content">
|
||||
<router-outlet></router-outlet>
|
||||
</section>
|
||||
|
||||
<footer class="border-t border-white/5 pt-4 mt-6 text-center text-xs text-white/30">
|
||||
© 2026 Kinetix Corp. All rights reserved.
|
||||
<footer *ngIf="!isLoginView()" class="app-footer">
|
||||
<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>
|
||||
|
||||
</main>
|
||||
`
|
||||
})
|
||||
export class AppComponent {}
|
||||
export class AppComponent {
|
||||
private router = inject(Router);
|
||||
|
||||
isLoginView(): boolean {
|
||||
return this.router.url.includes('/login');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { authGuard } from './core/guards/auth.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@ -6,9 +7,33 @@ export const routes: Routes = [
|
||||
redirectTo: 'dashboard',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
loadComponent: () => import('./features/auth/login.component').then(m => m.LoginComponent)
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
canActivate: [authGuard],
|
||||
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.
|
||||
|
||||
15
frontend/src/app/core/guards/auth.guard.ts
Normal file
15
frontend/src/app/core/guards/auth.guard.ts
Normal 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;
|
||||
};
|
||||
340
frontend/src/app/features/auth/login.component.css
Normal file
340
frontend/src/app/features/auth/login.component.css
Normal 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; }
|
||||
}
|
||||
135
frontend/src/app/features/auth/login.component.html
Normal file
135
frontend/src/app/features/auth/login.component.html
Normal 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>
|
||||
76
frontend/src/app/features/auth/login.component.ts
Normal file
76
frontend/src/app/features/auth/login.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
interface Employee {
|
||||
@ -12,6 +13,13 @@ interface Employee {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface UserProfile {
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
standalone: true,
|
||||
@ -19,137 +27,139 @@ interface Employee {
|
||||
template: `
|
||||
<div class="flex flex-col lg:flex-row gap-6 p-2 lg:p-6 min-h-[80vh]">
|
||||
<!-- 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 class="flex-grow flex flex-col gap-6">
|
||||
|
||||
<!-- ================= PAGE 1: INICIO ================= -->
|
||||
<!-- ================= PAGE 1: INICIO (CONSORCIO) ================= -->
|
||||
@if (activeTab() === 'inicio') {
|
||||
<div class="flex flex-col gap-8 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>
|
||||
<div style="display:flex;flex-direction:column;gap:32px;" class="animate-fadeIn">
|
||||
|
||||
<span class="text-neon-cyan text-xs font-bold uppercase tracking-widest">BIENVENIDO AL FUTURO</span>
|
||||
<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>
|
||||
<!-- Hero Banner -->
|
||||
|
||||
<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">
|
||||
Explorar Directorio
|
||||
</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">
|
||||
Reservar una Sala
|
||||
</button>
|
||||
|
||||
<!-- KPI Row (light cards) -->
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;">
|
||||
|
||||
<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;">
|
||||
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.14em;color:#78716C;">Sedes Integradas</span>
|
||||
<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>
|
||||
|
||||
<!-- Page 3: Módulos de Nueva Generación -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h3 class="text-xl font-bold tracking-tight text-neon-cyan uppercase tracking-wide">Módulos de Nueva Generación</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Info block -->
|
||||
<div class="glass-panel glass-panel-hover p-6 rounded-2xl neon-border-cyan flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-cyan-500/10 flex items-center justify-center text-2xl text-cyan-400">📚</div>
|
||||
<h4 class="text-lg font-bold text-white">Bloque de Info</h4>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Gestión de contenidos corporativos con arquitectura robusta de microservicios y publicación de artículos en tiempo récord.
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<!-- AD Directories -->
|
||||
<div class="glass-panel glass-panel-hover p-6 rounded-2xl neon-border-magenta flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-magenta-500/10 flex items-center justify-center text-2xl text-magenta-400">👤</div>
|
||||
<h4 class="text-lg font-bold text-white">Directorios AD</h4>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Sincronización nativa instantánea con Active Directory y perfiles dinámicos inteligentes para el equipo de desarrollo.
|
||||
</p>
|
||||
</div>
|
||||
<!-- 4 Jewel-Tone Blackjack Cards -->
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:20px;">
|
||||
|
||||
<!-- Self management -->
|
||||
<div class="glass-panel glass-panel-hover p-6 rounded-2xl neon-border-purple flex flex-col gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center text-2xl text-purple-400">📅</div>
|
||||
<h4 class="text-lg font-bold text-white">Autogestión</h4>
|
||||
<p class="text-sm text-slate-400 leading-relaxed">
|
||||
Reserva fluida de salas de conferencias, oficinas temporales e incidencias con disponibilidad y alertas en tiempo real.
|
||||
</p>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Card 2: Directorio — Deep Plum #1A0E2E -->
|
||||
<div (click)="setActiveTab('directory')"
|
||||
style="position:relative;background:#1A0E2E;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(26,14,46,0.30);">
|
||||
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
|
||||
<div style="position:absolute;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>
|
||||
<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));">
|
||||
<path d="M8 16V8H16" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40 16V8H32" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 32V40H16" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40 32V40H32" stroke="#A78BFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<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>
|
||||
|
||||
<!-- Card 3: Workflows — Deep Viridian #0A2520 -->
|
||||
<div (click)="setActiveTab('workflow')"
|
||||
style="position:relative;background:#0A2520;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(10,37,32,0.30);">
|
||||
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
|
||||
<div style="position:absolute;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>
|
||||
<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));">
|
||||
<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>
|
||||
|
||||
<!-- Card 4: Espacios — Deep Espresso #1C0E08 -->
|
||||
<div (click)="setActiveTab('spaces')"
|
||||
style="position:relative;background:#1C0E08;border:none;border-radius:20px;padding:40px 24px;cursor:pointer;display:flex;flex-direction:column;align-items:center;text-align:center;gap:22px;transition:all 0.22s cubic-bezier(0.34,1.56,0.64,1);overflow:hidden;min-height:270px;justify-content:center;box-shadow:0 8px 32px rgba(28,14,8,0.32);">
|
||||
<div style="position:absolute;inset:10px;border:1px solid rgba(255,255,255,0.08);border-radius:12px;pointer-events:none;"></div>
|
||||
<div style="position:absolute;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>
|
||||
<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));">
|
||||
<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>
|
||||
}
|
||||
|
||||
|
||||
<!-- ================= PAGE 2: DIRECTORY ================= -->
|
||||
@if (activeTab() === 'directory') {
|
||||
<div class="flex flex-col gap-6 animate-fadeIn">
|
||||
@ -606,7 +616,203 @@ interface Employee {
|
||||
</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>
|
||||
</div>
|
||||
`,
|
||||
@ -620,7 +826,9 @@ interface Employee {
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class DashboardComponent {
|
||||
export class DashboardComponent implements OnInit {
|
||||
private router = inject(Router);
|
||||
|
||||
// Navigation active tab signal
|
||||
activeTab = signal<'inicio' | 'directory' | 'workflow' | 'spaces' | 'metrics' | 'admin'>('inicio');
|
||||
|
||||
@ -633,6 +841,14 @@ export class DashboardComponent {
|
||||
simulationStatus = 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
|
||||
employees = signal<Employee[]>([
|
||||
{
|
||||
@ -685,8 +901,8 @@ export class DashboardComponent {
|
||||
return this.employees().filter(emp => {
|
||||
// Basic text search filter
|
||||
const matchesSearch = emp.name.toLowerCase().includes(query) ||
|
||||
emp.role.toLowerCase().includes(query) ||
|
||||
emp.tags.some(t => t.toLowerCase().includes(query));
|
||||
emp.role.toLowerCase().includes(query) ||
|
||||
emp.tags.some(t => t.toLowerCase().includes(query));
|
||||
|
||||
// Basic department match
|
||||
let matchesDept = true;
|
||||
@ -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') {
|
||||
this.activeTab.set(tab);
|
||||
// Reset secondary flags
|
||||
@ -749,4 +978,10 @@ export class DashboardComponent {
|
||||
this.approvalSuccess.set('');
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('kinetix_token');
|
||||
localStorage.removeItem('kinetix_user');
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"><</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">></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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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">
|
||||
← 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>
|
||||
@ -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']);
|
||||
}
|
||||
}
|
||||
122
frontend/src/app/features/reservas/services/ia-parser.service.ts
Normal file
122
frontend/src/app/features/reservas/services/ia-parser.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
font-family: 'Inter', sans-serif;
|
||||
color-scheme: dark;
|
||||
/* ── Fondo y superficies (LIGHT MODE) ── */
|
||||
--bg-page: #F5F4F1; /* Blanco cálido / ivory */
|
||||
--bg-surface: #FFFFFF; /* Superficie limpia */
|
||||
--bg-surface-2: #F0EEE9; /* Superficies secundarias */
|
||||
|
||||
/* HSL Neon Color Palette */
|
||||
--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%);
|
||||
/* ── Colores joya para tarjetas sofisticadas ── */
|
||||
--card-navy: #0D1B2A; /* Azul marino profundo */
|
||||
--card-plum: #1A0E2E; /* Ciruela / Violeta oscuro */
|
||||
--card-viridian: #0A2520; /* Verde Viridian / Bosque profundo */
|
||||
--card-espresso: #1C0E08; /* Espresso / Marrón ébano */
|
||||
|
||||
/* ── 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 {
|
||||
margin: 0;
|
||||
background-color: var(--bg-deep-dark);
|
||||
background-image: radial-gradient(circle at 50% -20%, rgba(0, 242, 254, 0.07) 0%, rgba(240, 12, 147, 0.03) 50%, transparent 100%);
|
||||
color: #f3f4f6;
|
||||
overflow-x: hidden;
|
||||
font-family: 'Outfit', 'Inter', sans-serif;
|
||||
font-weight: 400;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
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 {
|
||||
background: rgba(15, 15, 22, 0.65);
|
||||
backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.30s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 16px;
|
||||
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);
|
||||
}
|
||||
|
||||
.glass-panel-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 16px 48px 0 rgba(0, 0, 0, 0.6);
|
||||
border-color: var(--border-medium);
|
||||
box-shadow:
|
||||
0 2px 6px rgba(15, 13, 10, 0.08),
|
||||
0 12px 32px rgba(15, 13, 10, 0.10);
|
||||
}
|
||||
|
||||
/* Neon Borders and Accents */
|
||||
.neon-border-cyan {
|
||||
border: 1px solid rgba(0, 242, 254, 0.15);
|
||||
}
|
||||
.neon-border-cyan:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 15px rgba(0, 242, 254, 0.2);
|
||||
/* ── Tarjetas Joya (Blackjack) ─────────────────────────── */
|
||||
/* Clase base compartida — el color de fondo se pasa por variable */
|
||||
.card-blackjack {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.neon-border-magenta {
|
||||
border: 1px solid rgba(240, 12, 147, 0.15);
|
||||
}
|
||||
.neon-border-magenta:hover {
|
||||
border-color: var(--accent-magenta);
|
||||
box-shadow: 0 0 15px rgba(240, 12, 147, 0.2);
|
||||
.card-blackjack::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.neon-border-purple {
|
||||
border: 1px solid rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
.neon-border-purple:hover {
|
||||
border-color: var(--accent-purple);
|
||||
box-shadow: 0 0 15px rgba(124, 58, 237, 0.2);
|
||||
.card-blackjack:hover {
|
||||
transform: translateY(-5px) scale(1.02);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.35),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.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 {
|
||||
color: var(--accent-cyan);
|
||||
text-shadow: 0 0 10px rgba(0, 242, 254, 0.3);
|
||||
color: var(--indigo-rich);
|
||||
}
|
||||
|
||||
.text-neon-magenta {
|
||||
color: var(--accent-magenta);
|
||||
text-shadow: 0 0 10px rgba(240, 12, 147, 0.3);
|
||||
color: hsl(320, 70%, 45%);
|
||||
}
|
||||
|
||||
.text-neon-purple {
|
||||
color: var(--accent-purple);
|
||||
text-shadow: 0 0 10px rgba(124, 58, 237, 0.3);
|
||||
color: var(--indigo-rich);
|
||||
}
|
||||
|
||||
/* Inputs & Forms (rounded-full per slide 4) */
|
||||
.input-cyberpunk {
|
||||
background: rgba(10, 10, 15, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
.text-muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.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;
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 8px rgba(0, 242, 254, 0.25);
|
||||
background: rgba(10, 10, 15, 0.95);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Buttons (Cyan/Purple Glow) */
|
||||
.btn-cyan {
|
||||
background: var(--accent-cyan);
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 14px rgba(0, 242, 254, 0.3);
|
||||
.input-luxe::placeholder { color: var(--text-placeholder); }
|
||||
.input-luxe:focus {
|
||||
border-color: var(--gold-luxe);
|
||||
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
|
||||
}
|
||||
|
||||
.btn-cyan:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(0, 242, 254, 0.5);
|
||||
/* Dashboard search inputs (light) */
|
||||
.input-cyberpunk {
|
||||
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 {
|
||||
background: var(--accent-purple);
|
||||
.input-cyberpunk::placeholder { color: var(--text-placeholder); }
|
||||
.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;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 14px rgba(124, 58, 237, 0.3);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn-purple:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(124, 58, 237, 0.5);
|
||||
.input-dark::placeholder { color: rgba(255, 255, 255, 0.35); }
|
||||
.input-dark:focus {
|
||||
border-color: rgba(201, 151, 60, 0.55);
|
||||
box-shadow: 0 0 0 3px rgba(201, 151, 60, 0.12);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
/* ── Buttons ─────────────────────────────────────────────── */
|
||||
/* Gold gradient (primary) */
|
||||
.btn-gold {
|
||||
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 {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(15, 13, 10, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-cyan);
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(201, 151, 60, 0.45); }
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
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.
|
||||
## 1. Paleta de Colores (Diseño Elegante y Sofisticado)
|
||||
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 Secundario (Card Background):** HSL(240, 10%, 9%)
|
||||
- **Acento Primario (Cian Neón):** HSL(180, 100%, 50%)
|
||||
- **Acento Secundario (Magenta Neón):** HSL(320, 100%, 50%)
|
||||
- **Acento de Éxito (Verde Neón):** HSL(140, 100%, 50%)
|
||||
### Fondo y Superficies (Light Mode)
|
||||
- **Fondo Principal (Ivory):** `#F5F4F1`
|
||||
- **Superficie Principal (White):** `#FFFFFF`
|
||||
- **Superficie Secundaria (Warm Grey):** `#F0EEE9`
|
||||
|
||||
### 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)
|
||||
Utilizar bordes ultra finos semitransparentes combinados con desenfoque de fondo (`backdrop-filter`) para crear profundidad tridimensional en los paneles:
|
||||
## 2. Componentes y Efecto Glassmorphism
|
||||
El efecto de cristal se adapta para ofrecer limpieza y legibilidad sobre el fondo claro, utilizando bordes sutiles y sombras suaves.
|
||||
|
||||
```css
|
||||
.glass-panel {
|
||||
background: rgba(15, 15, 20, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 16px;
|
||||
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
|
||||
- **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.
|
||||
- **Transición de Rutas:** Usar animaciones fluidas en Angular al alternar vistas.
|
||||
- **Efectos de Foco:** Utilizar anillos de foco cian neón en entradas de texto y controles interactivos para garantizar accesibilidad.
|
||||
## 3. Botones, Entradas e Interactividad
|
||||
- **Botón Primario (Gold):** Gradiente de oro (`#C9973C` a `#A67C28`) con sombras proyectadas.
|
||||
- **Botón Secundario (Indigo):** Sólido índigo (`#3730A3`) para acciones de flujo secundario.
|
||||
- **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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user