feat: implement calendar booking component with Google Calendar-style UI and layout infrastructure

This commit is contained in:
Jessica Orozco 2026-06-16 15:49:10 -04:00
parent 705f263de5
commit 23af9b33da
8 changed files with 1455 additions and 790 deletions

View File

@ -3,17 +3,20 @@
========================================================= */ ========================================================= */
.app-shell { .app-shell {
min-height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #F5F4F1; background: #F5F4F1;
padding: 0 32px 32px; padding: 0 16px;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden;
} }
.app-shell.login-mode { .app-shell.login-mode {
padding: 0; padding: 0;
background: #F5F4F1; /* ivory white — same as page */ background: #F5F4F1; /* ivory white — same as page */
height: auto;
overflow: visible;
} }
/* ── Header ── */ /* ── Header ── */
@ -23,7 +26,7 @@
justify-content: space-between; justify-content: space-between;
padding: 18px 0; padding: 18px 0;
border-bottom: 1px solid rgba(15, 13, 10, 0.08); border-bottom: 1px solid rgba(15, 13, 10, 0.08);
margin-bottom: 32px; flex-shrink: 0;
} }
.header-brand { .header-brand {
@ -78,12 +81,18 @@
} }
/* ── Content ── */ /* ── Content ── */
.app-content { flex: 1; display: flex; flex-direction: column; } .app-content {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
padding: 12px 0;
}
/* ── Footer ── */ /* ── Footer ── */
.app-footer { .app-footer {
margin-top: 32px; padding: 18px 0;
padding-top: 18px;
border-top: 1px solid rgba(15, 13, 10, 0.07); border-top: 1px solid rgba(15, 13, 10, 0.07);
display: flex; display: flex;
align-items: center; align-items: center;
@ -94,6 +103,7 @@
color: #A8A29E; color: #A8A29E;
letter-spacing: 0.05em; letter-spacing: 0.05em;
text-transform: uppercase; text-transform: uppercase;
flex-shrink: 0;
} }
.footer-dot { .footer-dot {

View File

@ -25,7 +25,7 @@ interface UserProfile {
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
template: ` template: `
<div class="flex flex-col lg:flex-row gap-6 p-2 lg:p-6 min-h-[80vh]"> <div class="flex flex-col lg:flex-row gap-4 p-2 lg:p-4 h-full overflow-y-auto">
<!-- Sidebar Navigation Menu (Cyberpunk theme) --> <!-- Sidebar Navigation Menu (Cyberpunk theme) -->
@ -644,6 +644,13 @@ interface UserProfile {
</div> </div>
`, `,
styles: [` styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
}
.animate-fadeIn { .animate-fadeIn {
animation: fadeIn 0.4s ease-out forwards; animation: fadeIn 0.4s ease-out forwards;
} }

View File

@ -2,6 +2,14 @@
APP PEOPLE Employee Directory Stylesheet APP PEOPLE Employee Directory Stylesheet
========================================================================== */ ========================================================================== */
:host {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
}
.dir-shell { .dir-shell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -10,8 +18,10 @@
overflow: hidden; overflow: hidden;
border: 1px solid rgba(15, 13, 10, 0.06); border: 1px solid rgba(15, 13, 10, 0.06);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
min-height: 85vh; height: 100%;
font-family: 'Outfit', 'Inter', system-ui, sans-serif; font-family: 'Outfit', 'Inter', system-ui, sans-serif;
flex: 1;
min-height: 0;
} }
/* ── TIMI PEOPLE NAVBAR ── */ /* ── TIMI PEOPLE NAVBAR ── */
@ -137,11 +147,14 @@
/* ── MAIN CONTENT ── */ /* ── MAIN CONTENT ── */
.dir-main { .dir-main {
padding: 32px; padding: 12px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 16px;
flex-grow: 1; flex-grow: 1;
height: 100%;
overflow: hidden;
min-height: 0;
} }
/* Header block */ /* Header block */
@ -370,7 +383,11 @@
.dir-grid { .dir-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 24px; gap: 16px;
flex: 1;
overflow-y: auto;
min-height: 0;
padding: 4px; /* avoid box-shadow clipping */
} }
/* Card details */ /* Card details */

View File

@ -262,62 +262,38 @@ import { Router } from '@angular/router';
</div> </div>
`, `,
styles: [` styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
}
/* ── LAYOUT PRINCIPAL ── */ /* ── LAYOUT PRINCIPAL ── */
.ih-shell { .ih-shell {
display: flex; display: flex;
background: #f4f3ef; /* Light beige/grey background matching workflow */ background: #f4f3ef; /* Light beige/grey background matching workflow */
min-height: 90vh; height: 100%;
border-radius: 24px; border-radius: 24px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(15, 13, 10, 0.08); border: 1px solid rgba(15, 13, 10, 0.08);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
font-family: 'Outfit', 'Inter', system-ui, sans-serif; font-family: 'Outfit', 'Inter', system-ui, sans-serif;
} flex: 1;
min-height: 0;
/* Sidebar */
.ih-sidebar {
width: 90px;
background: #000000;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
flex-shrink: 0;
}
.ih-sidebar__brand {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 24px;
}
.ih-sidebar__logo-container {
width: 44px;
height: 44px;
border-radius: 12px;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.ih-sidebar__logo-btn {
font-size: 18px;
font-weight: 900;
color: #000000;
} }
/* Content Area */ /* Content Area */
.ih-content { .ih-content {
flex-grow: 1; flex-grow: 1;
padding: 24px; padding: 12px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 12px;
min-width: 0; min-width: 0;
height: 100%;
overflow: hidden;
} }
/* Topbar */ /* Topbar */
@ -421,7 +397,11 @@ import { Router } from '@angular/router';
.ih-grid { .ih-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 24px; gap: 16px;
flex: 1;
overflow-y: auto;
min-height: 0;
padding: 4px; /* avoid box-shadow clipping */
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {

View File

@ -1,201 +1,320 @@
<div class="wf-shell"> <div class="gc-container">
<!-- ═══════════════════════════════ HEADER (GOOGLE CALENDAR TOPBAR) ═══════════════════════════════ -->
<!-- ═══════════════════════════════ CONTENIDO PRINCIPAL ═══════════════════════════════ --> <header class="gc-header">
<main class="wf-content"> <div class="gc-header__left">
<button class="gc-header__menu-btn" title="Menú principal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
</button>
<div class="gc-header__logo">
<div class="gc-header__logo-icon">
<span>16</span>
</div>
<span class="gc-header__logo-text">Calendar</span>
</div>
<button class="gc-header__btn gc-header__btn--rounded" (click)="selectedSlot.set(null)" title="Volver al día de hoy">Hoy</button>
<div class="gc-header__nav">
<button class="gc-header__nav-btn" title="Anterior"></button>
<button class="gc-header__nav-btn" title="Siguiente"></button>
</div>
<h2 class="gc-header__title">Junio de 2026</h2>
</div>
<!-- Barra superior oscura --> <div class="gc-header__right">
<header class="wf-topbar"> <!-- Search Input aligned inside Header -->
<div class="wf-topbar__header-row"> <div class="gc-header__search-container">
<h1 class="wf-topbar__title">RESERVAS DE ESPACIOS</h1> <svg class="gc-header__search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" placeholder="Buscar recursos..." (input)="onSearch($event)" class="gc-header__search-input" />
</div>
<button class="gc-header__icon-btn" title="Asistencia"><span class="material-icons">help_outline</span></button>
<button class="gc-header__icon-btn" title="Menú Configuración"><span class="material-icons">settings</span></button>
<div class="gc-header__view-select">
<button class="gc-header__view-trigger">
<span>Semana</span>
<span class="gc-header__view-arrow"></span>
</button>
</div>
<button class="gc-header__icon-btn" title="Aplicaciones de Google"><span class="material-icons">apps</span></button>
<div class="gc-header__user-avatar">JM</div>
</div>
</header>
<!-- ═══════════════════════════════ MAIN LAYOUT BODY ═══════════════════════════════ -->
<div class="gc-body">
<!-- ── SIDEBAR IZQUIERDO (256px) ── -->
<aside class="gc-left-sidebar">
<!-- Pill style '+ Crear' Button -->
<div class="gc-sidebar-create">
<button class="gc-btn-create" (click)="navigateToNuevo()">
<svg class="gc-btn-create__icon" viewBox="0 0 36 36">
<path fill="#34A853" d="M16 16v14h4V20z"/>
<path fill="#4285F4" d="M30 16H20l-4 4h14z"/>
<path fill="#FBBC05" d="M6 16v4h10l4-4z"/>
<path fill="#EA4335" d="M20 16V6h-4v10z"/>
</svg>
<span class="gc-btn-create__text">Crear</span>
<span class="gc-btn-create__arrow"></span>
</button>
</div>
<!-- Mini monthly calendar widget -->
<div class="gc-mini-calendar">
<div class="gc-mini-calendar__header">
<span class="gc-mini-calendar__title">Junio de 2026</span>
<div class="gc-mini-calendar__nav">
<button class="gc-mini-calendar__nav-btn"></button>
<button class="gc-mini-calendar__nav-btn"></button>
</div>
</div>
<!-- Controles a la derecha --> <div class="gc-mini-calendar__days-header">
<div class="wf-topbar__controls"> <span>D</span><span>L</span><span>M</span><span>M</span><span>J</span><span>V</span><span>S</span>
<div class="wf-search"> </div>
<input type="text" placeholder="Reservar Espacios" (input)="onSearch($event)" class="wf-search__field" />
<button class="wf-search__btn"> <div class="gc-mini-calendar__days-grid">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> @for (d of miniCalendarDays; track d) {
<circle cx="11" cy="11" r="8"></circle> <span class="gc-mini-calendar__day"
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> [ngClass]="{
</svg> 'gc-mini-calendar__day--inactive': !d.currentMonth,
</button> 'gc-mini-calendar__day--active': d.active
</div> }">
{{ d.day }}
<div class="wf-filter-dropdown"> </span>
<button class="wf-filter-dropdown__trigger"> }
Tipo
<span class="wf-filter-dropdown__arrow"></span>
</button>
</div>
<button class="wf-btn-filter">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
Filter
</button>
</div> </div>
</div> </div>
<!-- Categorías de Espacios --> <!-- Search Box inside Sidebar -->
<div class="ih-category-filters"> <div class="gc-sidebar-search">
<button (click)="setCategory('Sala Reuniones')" <div class="gc-search-box">
[class.ih-category-btn--active]="activeCategory() === 'Sala Reuniones'" <span class="material-icons gc-search-box__icon">people_outline</span>
class="ih-category-btn">Sala Reuniones</button> <input type="text" placeholder="Buscar a gente" class="gc-search-box__input" />
</div>
<button (click)="setCategory('Oficina Flexible')"
[class.ih-category-btn--active]="activeCategory() === 'Oficina Flexible'"
class="ih-category-btn">Oficina Flexible</button>
<button (click)="setCategory('Zona Relax')"
[class.ih-category-btn--active]="activeCategory() === 'Zona Relax'"
class="ih-category-btn">Zona Relax</button>
<button (click)="setCategory('Evento')"
[class.ih-category-btn--active]="activeCategory() === 'Evento'"
class="ih-category-btn">Evento</button>
</div> </div>
</header>
<!-- Grid de Calendario de Espacios y Mis Reservas -->
<div class="ih-grid">
<!-- Columna Izquierda: Calendario de Espacios --> <!-- Accordion: Mis Calendarios / Categorías de Reservas -->
<div class="ih-news-section"> <div class="gc-accordion">
<div class="wf-card wf-card--calendar"> <div class="gc-accordion__header">
<span class="gc-accordion__title">Mis Espacios</span>
<span class="material-icons gc-accordion__chevron">keyboard_arrow_up</span>
</div>
<div class="gc-accordion__content">
<label class="gc-checkbox-item">
<input type="checkbox" [checked]="isCategorySelected('Sala Reuniones')" (change)="toggleCategory('Sala Reuniones')" class="gc-checkbox-item__input gc-checkbox-item__input--blue" />
<span class="gc-checkbox-item__label">Sala Reuniones</span>
</label>
<div class="wf-card__header"> <label class="gc-checkbox-item">
<h3 class="wf-card__title">Calendario de Espacios</h3> <input type="checkbox" [checked]="isCategorySelected('Oficina Flexible')" (change)="toggleCategory('Oficina Flexible')" class="gc-checkbox-item__input gc-checkbox-item__input--green" />
<span class="gc-checkbox-item__label">Oficina Flexible</span>
<!-- Vista toggle --> </label>
<div class="wf-view-toggle">
<button class="wf-view-toggle__btn wf-view-toggle__btn--active">Día</button> <label class="gc-checkbox-item">
<button class="wf-view-toggle__btn">Semana</button> <input type="checkbox" [checked]="isCategorySelected('Zona Relax')" (change)="toggleCategory('Zona Relax')" class="gc-checkbox-item__input gc-checkbox-item__input--yellow" />
<button class="wf-view-toggle__btn">Mes</button> <span class="gc-checkbox-item__label">Zona Relax</span>
</div> </label>
</div>
<label class="gc-checkbox-item">
<input type="checkbox" [checked]="isCategorySelected('Evento')" (change)="toggleCategory('Evento')" class="gc-checkbox-item__input gc-checkbox-item__input--purple" />
<span class="gc-checkbox-item__label">Eventos</span>
</label>
</div>
</div>
<!-- Cuadrícula Horaria por Recursos --> <!-- Accordion: Otros Calendarios / Mis Reservas Activas -->
<div class="res-calendar-container"> <div class="gc-accordion gc-accordion--reservas">
<div class="res-grid-header" [style.grid-template-columns]="'60px repeat(' + filteredResources().length + ', 1fr)'"> <div class="gc-accordion__header">
<div class="res-time-column-label"></div> <span class="gc-accordion__title">Actividad de Reservas</span>
<span class="material-icons gc-accordion__chevron">keyboard_arrow_up</span>
<!-- Columnas de recursos dinámicas --> </div>
@for (res of filteredResources(); track res.id) { <div class="gc-accordion__content gc-accordion__content--scroll">
<div class="res-resource-column"> @for (act of recentActivities(); track act.title) {
<div class="res-resource-header"> <div class="gc-activity-item">
<span class="res-resource-icon" [ngClass]="{ <img [src]="act.avatar" alt="user" class="gc-activity-item__avatar" />
'res-resource-icon--yellow': res.icon === '☀️', <div class="gc-activity-item__body">
'res-resource-icon--office': res.icon === '🏢' <span class="gc-activity-item__title"><strong>{{ act.title }}</strong></span>
}">{{ res.icon }}</span> <span class="gc-activity-item__desc">{{ act.detail }}</span>
<div class="res-resource-info"> <span class="gc-activity-item__time">{{ act.time }}</span>
<strong class="res-resource-name">{{ res.name }}</strong> </div>
<span class="res-resource-desc">{{ res.desc }}</span>
</div>
</div>
</div>
}
</div>
<!-- Cuerpo de la cuadrícula -->
<div class="res-grid-body">
@for (time of timeSlots; track time) {
<div class="res-grid-row" [style.grid-template-columns]="'60px repeat(' + filteredResources().length + ', 1fr)'">
<!-- Columna horaria -->
<div class="res-time-cell">{{ time }}</div>
<!-- Celdas para cada recurso -->
@for (res of filteredResources(); track res.id) {
<div class="res-slot-cell">
@if (isSlotOccupied(res.name, time)) {
<!-- Celda Reservada/Ocupada -->
<div class="res-booking-block res-booking-block--filled flex items-center justify-between px-3 text-[10px] font-bold text-white">
<span>Ocupado</span>
</div>
} @else {
<!-- Caso especial Oficina Flex A1 a las 12:00 y 13:30 (mostrando avatares ocupados en un slot flexible) -->
@if (res.name === 'OFICINA FLEX A1' && time === '12:00') {
<div class="res-booking-block flex justify-center items-center gap-1 cursor-pointer" (click)="onSlotClick(res, time)">
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=40&q=80" alt="emp" class="res-mini-avatar" />
<img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=40&q=80" alt="emp" class="res-mini-avatar" />
</div>
} @else if (res.name === 'OFICINA FLEX A1' && time === '13:30') {
<div class="res-booking-block flex justify-center items-center cursor-pointer" (click)="onSlotClick(res, time)">
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=40&q=80" alt="emp" class="res-mini-avatar" />
</div>
} @else {
<!-- Celda Disponible -->
<div class="res-booking-block cursor-pointer hover:bg-slate-200/80" (click)="onSlotClick(res, time)"></div>
}
}
<!-- Popup flotante interactivo de reserva dinámico -->
@if (selectedSlot()?.resource?.id === res.id && selectedSlot()?.time === time) {
<div class="res-popup-card animate-fadeIn" (click)="$event.stopPropagation()">
<div class="flex justify-between items-center">
<h4 class="res-popup-title">{{ res.name }}</h4>
<button class="text-slate-400 hover:text-slate-600 text-xs" (click)="closePopup()"></button>
</div>
<p class="res-popup-detail">Available <strong>{{ time }} - {{ getEndTime(time) }}</strong></p>
<span class="res-popup-capacity">Capacidad {{ res.capacity }}</span>
<div class="res-popup-actions">
<button (click)="confirmReserva()" class="res-popup-btn-confirm">Confirmar Reserva</button>
<button (click)="navigateToNuevo()" class="res-popup-btn-details">Ver Detalles</button>
</div>
<span class="res-popup-status">
<span class="res-popup-status-dot"></span>
Disponibilidad, disponible
</span>
</div>
}
</div>
}
</div>
}
</div>
</div>
@if (filteredResources().length === 0) {
<div class="text-center p-8 text-slate-400 font-semibold bg-slate-50 border border-dashed rounded-2xl">
No se encontraron recursos disponibles en esta sección.
</div> </div>
} }
</div> </div>
</div> </div>
<!-- Columna Derecha: Mis Reservas y Notificaciones (Dinámicas) --> </aside>
<div class="ih-activity-section">
<div class="ih-card-activity"> <!-- ── CENTRAL CALENDAR VIEW (GRID WEEK VIEW STYLE) ── -->
<h3 class="ih-card-activity__title">Mis Reservas y Notificaciones</h3> <main class="gc-main-calendar">
<!-- Columns header (equivalent to days header) -->
<div class="gc-grid-header">
<!-- Corner empty label for alignment with time column -->
<div class="gc-grid-header__corner">
<span class="gc-corner-gmt">GMT-04</span>
</div>
<!-- Headers columns representing resources -->
<div class="gc-grid-header__columns">
@for (res of filteredResources(); track res.id; let idx = $index) {
<div class="gc-col-header" [class.gc-col-header--active]="idx === 0">
<span class="gc-col-header__day">{{ res.type }}</span>
<div class="gc-col-header__day-circle" [class.gc-col-header__day-circle--active]="idx === 0">
<span class="gc-col-header__day-num">{{ res.icon }}</span>
</div>
<strong class="gc-col-header__name">{{ res.name }}</strong>
</div>
}
</div>
</div>
<!-- Scrollable time cells grid -->
<div class="gc-grid-scroll-area">
<div class="gc-grid-body">
<div class="ih-activity-list"> <!-- Column of hours scale (left side) -->
@for (act of recentActivities(); track act.title) { <div class="gc-hours-column">
<div class="ih-activity-item animate-fadeIn"> @for (time of timeSlots; track time) {
@if (act.type === 'reserva') { <div class="gc-hour-cell">
<img [src]="act.avatar" alt="res" class="ih-activity-avatar" /> <span class="gc-hour-label">{{ time }}</span>
<div class="ih-activity-body">
<p class="ih-activity-text"><strong>{{ act.title }}</strong><br><strong>{{ act.detail }}</strong></p>
<span class="ih-activity-time">{{ act.time }}</span>
</div>
} @else {
<img [src]="act.avatar" alt="notif" class="ih-activity-avatar" />
<div class="ih-activity-body">
<p class="ih-activity-text"><strong>David Chen</strong> compartió <strong>Resultados del Trimestre</strong></p>
<span class="ih-activity-time">{{ act.time }}</span>
</div>
}
</div> </div>
} }
</div> </div>
<!-- Columns of Grid scheduler -->
<div class="gc-columns-grid" [style.grid-template-columns]="'repeat(' + filteredResources().length + ', 1fr)'">
<!-- Red Horizontal Line: Current Time Indicator -->
<div class="gc-current-time-line" style="top: 52%;" title="Hora actual aproximada">
<div class="gc-current-time-line__circle"></div>
</div>
<!-- Generate columns per resource -->
@for (res of filteredResources(); track res.id; let colIdx = $index) {
<div class="gc-resource-column">
@for (time of timeSlots; track time) {
<div class="gc-grid-slot-cell">
@if (isSlotOccupied(res.name, time)) {
<!-- Event Busy Block (pastel styles styled exactly as Google Calendar events) -->
<div class="gc-event gc-event--busy gc-event--color-{{ colIdx % 4 }}">
<div class="gc-event__title">Reservado / Ocupado</div>
<div class="gc-event__time">{{ time }} - {{ getEndTime(time) }}</div>
</div>
} @else {
<!-- Flex space placeholder mock showing avatars on Oficina Flex -->
@if (res.name === 'OFICINA FLEX A1' && time === '12:00') {
<div class="gc-event-flex-avatars" (click)="onSlotClick(res, time)">
<img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=40&q=80" alt="emp" class="gc-mini-avatar" />
<img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=40&q=80" alt="emp" class="gc-mini-avatar" />
</div>
} @else if (res.name === 'OFICINA FLEX A1' && time === '13:30') {
<div class="gc-event-flex-avatars" (click)="onSlotClick(res, time)">
<img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=40&q=80" alt="emp" class="gc-mini-avatar" />
</div>
} @else {
<!-- Empty Available Cell click to add -->
<div class="gc-slot-click-zone" (click)="onSlotClick(res, time)" title="Haga clic para reservar"></div>
}
}
<!-- Google Calendar style interactive Event creation popup -->
@if (selectedSlot()?.resource?.id === res.id && selectedSlot()?.time === time) {
<div class="gc-popup-card" (click)="$event.stopPropagation()">
<div class="gc-popup-card__header">
<span class="material-icons gc-popup-card__drag">drag_handle</span>
<button class="gc-popup-card__close-btn" (click)="closePopup()" title="Cerrar"></button>
</div>
<div class="gc-popup-card__body">
<h4 class="gc-popup-card__title">{{ res.name }}</h4>
<div class="gc-popup-card__row">
<span class="material-icons gc-popup-card__icon">schedule</span>
<div class="gc-popup-card__details">
<span>Martes, 16 de Junio</span>
<span class="gc-popup-card__time">{{ time }} {{ getEndTime(time) }}</span>
</div>
</div>
<div class="gc-popup-card__row">
<span class="material-icons gc-popup-card__icon">room</span>
<span>Capacidad máxima: {{ res.capacity }} personas</span>
</div>
<div class="gc-popup-card__row">
<span class="material-icons gc-popup-card__icon">info_outline</span>
<span class="gc-popup-card__tag">{{ res.type }}</span>
</div>
</div>
<div class="gc-popup-card__actions">
<button (click)="navigateToNuevo()" class="gc-popup-btn-secondary">Más opciones</button>
<button (click)="confirmReserva()" class="gc-popup-btn-primary">Guardar</button>
</div>
</div>
}
</div>
}
</div>
}
</div>
</div> </div>
</div> </div>
@if (filteredResources().length === 0) {
<div class="gc-empty-state">
<span class="material-icons gc-empty-state__icon">calendar_today</span>
<p>No hay recursos seleccionados. Marque las categorías en el panel izquierdo.</p>
</div>
}
</div> </main>
<!-- ── SIDEBAR DERECHO (48px) ── -->
<aside class="gc-right-sidebar">
<div class="gc-right-sidebar__icons">
<button class="gc-right-sidebar__btn gc-right-sidebar__btn--keep" title="Keep">
<img src="https://www.gstatic.com/companion/icon_assets/keep_2020q4_v3.png" alt="Keep" />
</button>
<button class="gc-right-sidebar__btn gc-right-sidebar__btn--tasks" title="Tasks">
<img src="https://www.gstatic.com/companion/icon_assets/tasks_2021_2v.png" alt="Tasks" />
</button>
<button class="gc-right-sidebar__btn gc-right-sidebar__btn--contacts" title="Contactos">
<img src="https://www.gstatic.com/companion/icon_assets/contacts_2022_3.png" alt="Contacts" />
</button>
<button class="gc-right-sidebar__btn gc-right-sidebar__btn--maps" title="Maps">
<img src="https://www.gstatic.com/companion/icon_assets/maps_v2.png" alt="Maps" />
</button>
<div class="gc-right-sidebar__divider"></div>
<button class="gc-right-sidebar__btn gc-right-sidebar__btn--plus" title="Obtener complementos">
<span class="material-icons">add</span>
</button>
</div>
<button class="gc-right-sidebar__btn gc-right-sidebar__btn--chevron" title="Ocultar panel lateral">
<span class="material-icons">chevron_right</span>
</button>
</aside>
</main> </div>
</div> </div>

View File

@ -37,7 +37,7 @@ export class CalendarBookingComponent implements OnInit {
]); ]);
// Signals para gestionar estado interactivo // Signals para gestionar estado interactivo
activeCategory = signal<string>('Sala Reuniones'); selectedCategories = signal<string[]>(['Sala Reuniones', 'Oficina Flexible', 'Zona Relax', 'Evento']);
searchQuery = signal<string>(''); searchQuery = signal<string>('');
// Slot seleccionado para el popup dinámico // Slot seleccionado para el popup dinámico
@ -59,12 +59,58 @@ export class CalendarBookingComponent implements OnInit {
{ title: 'David Chen', detail: 'compartió Resultados del Trimestre', time: 'Hace 15m', type: 'notif', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=120&q=80' } { title: 'David Chen', detail: 'compartió Resultados del Trimestre', time: 'Hace 15m', type: 'notif', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=120&q=80' }
]; ];
// Mini calendar data for June 2026
miniCalendarDays = [
{ day: 31, currentMonth: false },
{ day: 1, currentMonth: true },
{ day: 2, currentMonth: true },
{ day: 3, currentMonth: true },
{ day: 4, currentMonth: true },
{ day: 5, currentMonth: true },
{ day: 6, currentMonth: true },
{ day: 7, currentMonth: true },
{ day: 8, currentMonth: true },
{ day: 9, currentMonth: true },
{ day: 10, currentMonth: true },
{ day: 11, currentMonth: true },
{ day: 12, currentMonth: true },
{ day: 13, currentMonth: true },
{ day: 14, currentMonth: true },
{ day: 15, currentMonth: true },
{ day: 16, currentMonth: true, active: true },
{ day: 17, currentMonth: true },
{ day: 18, currentMonth: true },
{ day: 19, currentMonth: true },
{ day: 20, currentMonth: true },
{ day: 21, currentMonth: true },
{ day: 22, currentMonth: true },
{ day: 23, currentMonth: true },
{ day: 24, currentMonth: true },
{ day: 25, currentMonth: true },
{ day: 26, currentMonth: true },
{ day: 27, currentMonth: true },
{ day: 28, currentMonth: true },
{ day: 29, currentMonth: true },
{ day: 30, currentMonth: true },
{ day: 1, currentMonth: false },
{ day: 2, currentMonth: false },
{ day: 3, currentMonth: false },
{ day: 4, currentMonth: false },
{ day: 5, currentMonth: false },
{ day: 6, currentMonth: false },
{ day: 7, currentMonth: false },
{ day: 8, currentMonth: false },
{ day: 9, currentMonth: false },
{ day: 10, currentMonth: false },
{ day: 11, currentMonth: false }
];
// Computed signals // Computed signals
filteredResources = computed(() => { filteredResources = computed(() => {
const cat = this.activeCategory(); const selectedCats = this.selectedCategories();
const query = this.searchQuery().toLowerCase().trim(); const query = this.searchQuery().toLowerCase().trim();
return this.resources().filter(r => return this.resources().filter(r =>
r.type === cat && selectedCats.includes(r.type) &&
(!query || r.name.toLowerCase().includes(query) || r.desc.toLowerCase().includes(query)) (!query || r.name.toLowerCase().includes(query) || r.desc.toLowerCase().includes(query))
); );
}); });
@ -95,9 +141,19 @@ export class CalendarBookingComponent implements OnInit {
} }
} }
setCategory(cat: string): void { toggleCategory(cat: string): void {
this.activeCategory.set(cat); this.selectedCategories.update(prev => {
this.selectedSlot.set(null); // Reset popup al cambiar pestaña if (prev.includes(cat)) {
return prev.filter(c => c !== cat);
} else {
return [...prev, cat];
}
});
this.selectedSlot.set(null); // Reset popup al cambiar filtros
}
isCategorySelected(cat: string): boolean {
return this.selectedCategories().includes(cat);
} }
onSearch(event: Event): void { onSearch(event: Event): void {

View File

@ -237,63 +237,39 @@ import { Router } from '@angular/router';
</div> </div>
`, `,
styles: [` styles: [`
:host {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
}
/* ── LAYOUT PRINCIPAL ── */ /* ── LAYOUT PRINCIPAL ── */
.wf-shell { .wf-shell {
display: flex; display: flex;
background: #f4f3ef; /* Light beige/grey background */ background: #f4f3ef; /* Light beige/grey background */
min-height: 90vh; height: 100%;
border-radius: 24px; border-radius: 24px;
overflow: hidden; overflow: hidden;
border: 1px solid rgba(15, 13, 10, 0.08); border: 1px solid rgba(15, 13, 10, 0.08);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
font-family: 'Outfit', 'Inter', system-ui, sans-serif; font-family: 'Outfit', 'Inter', system-ui, sans-serif;
} flex: 1;
min-height: 0;
/* Sidebar */
.wf-sidebar {
width: 90px;
background: #000000;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
flex-shrink: 0;
}
.wf-sidebar__brand {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 24px;
}
.wf-sidebar__logo-container {
width: 44px;
height: 44px;
border-radius: 12px;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.wf-sidebar__logo-btn {
font-size: 18px;
font-weight: 900;
color: #000000;
} }
/* Content Area */ /* Content Area */
.wf-content { .wf-content {
flex-grow: 1; flex-grow: 1;
padding: 24px; padding: 12px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 12px;
min-width: 0; min-width: 0;
position: relative; position: relative;
height: 100%;
overflow: hidden;
} }
/* Topbar */ /* Topbar */
@ -396,7 +372,11 @@ import { Router } from '@angular/router';
.wf-grid { .wf-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 24px; gap: 16px;
flex: 1;
overflow-y: auto;
min-height: 0;
padding: 4px; /* avoid box-shadow clipping */
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {