feat: implement calendar booking component with Google Calendar-style UI and layout infrastructure
This commit is contained in:
parent
705f263de5
commit
23af9b33da
@ -3,17 +3,20 @@
|
||||
========================================================= */
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #F5F4F1;
|
||||
padding: 0 32px 32px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell.login-mode {
|
||||
padding: 0;
|
||||
background: #F5F4F1; /* ivory white — same as page */
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
@ -23,7 +26,7 @@
|
||||
justify-content: space-between;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid rgba(15, 13, 10, 0.08);
|
||||
margin-bottom: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
@ -78,12 +81,18 @@
|
||||
}
|
||||
|
||||
/* ── 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 ── */
|
||||
.app-footer {
|
||||
margin-top: 32px;
|
||||
padding-top: 18px;
|
||||
padding: 18px 0;
|
||||
border-top: 1px solid rgba(15, 13, 10, 0.07);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -94,6 +103,7 @@
|
||||
color: #A8A29E;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-dot {
|
||||
|
||||
@ -25,7 +25,7 @@ interface UserProfile {
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
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) -->
|
||||
|
||||
|
||||
@ -644,6 +644,13 @@ interface UserProfile {
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
@ -2,6 +2,14 @@
|
||||
APP PEOPLE — Employee Directory Stylesheet
|
||||
========================================================================== */
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dir-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -10,8 +18,10 @@
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(15, 13, 10, 0.06);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||
min-height: 85vh;
|
||||
height: 100%;
|
||||
font-family: 'Outfit', 'Inter', system-ui, sans-serif;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── TIMI PEOPLE NAVBAR ── */
|
||||
@ -137,11 +147,14 @@
|
||||
|
||||
/* ── MAIN CONTENT ── */
|
||||
.dir-main {
|
||||
padding: 32px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: 16px;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Header block */
|
||||
@ -370,7 +383,11 @@
|
||||
.dir-grid {
|
||||
display: grid;
|
||||
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 */
|
||||
|
||||
@ -262,62 +262,38 @@ import { Router } from '@angular/router';
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── LAYOUT PRINCIPAL ── */
|
||||
.ih-shell {
|
||||
display: flex;
|
||||
background: #f4f3ef; /* Light beige/grey background matching workflow */
|
||||
min-height: 90vh;
|
||||
height: 100%;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(15, 13, 10, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||
font-family: 'Outfit', 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.ih-content {
|
||||
flex-grow: 1;
|
||||
padding: 24px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Topbar */
|
||||
@ -421,7 +397,11 @@ import { Router } from '@angular/router';
|
||||
.ih-grid {
|
||||
display: grid;
|
||||
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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,201 +1,320 @@
|
||||
<div class="wf-shell">
|
||||
<div class="gc-container">
|
||||
|
||||
<!-- ═══════════════════════════════ HEADER (GOOGLE CALENDAR TOPBAR) ═══════════════════════════════ -->
|
||||
<header class="gc-header">
|
||||
<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>
|
||||
|
||||
<!-- ═══════════════════════════════ CONTENIDO PRINCIPAL ═══════════════════════════════ -->
|
||||
<main class="wf-content">
|
||||
<div class="gc-header__logo">
|
||||
<div class="gc-header__logo-icon">
|
||||
<span>16</span>
|
||||
</div>
|
||||
<span class="gc-header__logo-text">Calendar</span>
|
||||
</div>
|
||||
|
||||
<!-- Barra superior oscura -->
|
||||
<header class="wf-topbar">
|
||||
<div class="wf-topbar__header-row">
|
||||
<h1 class="wf-topbar__title">RESERVAS DE ESPACIOS</h1>
|
||||
<button class="gc-header__btn gc-header__btn--rounded" (click)="selectedSlot.set(null)" title="Volver al día de hoy">Hoy</button>
|
||||
|
||||
<!-- Controles a la derecha -->
|
||||
<div class="wf-topbar__controls">
|
||||
<div class="wf-search">
|
||||
<input type="text" placeholder="Reservar Espacios" (input)="onSearch($event)" class="wf-search__field" />
|
||||
<button class="wf-search__btn">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
|
||||
<div class="gc-header__right">
|
||||
<!-- Search Input aligned inside Header -->
|
||||
<div class="gc-header__search-container">
|
||||
<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>
|
||||
|
||||
<div class="wf-filter-dropdown">
|
||||
<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>
|
||||
|
||||
<!-- Categorías de Espacios -->
|
||||
<div class="ih-category-filters">
|
||||
<button (click)="setCategory('Sala Reuniones')"
|
||||
[class.ih-category-btn--active]="activeCategory() === 'Sala Reuniones'"
|
||||
class="ih-category-btn">Sala Reuniones</button>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- Grid de Calendario de Espacios y Mis Reservas -->
|
||||
<div class="ih-grid">
|
||||
<!-- ═══════════════════════════════ MAIN LAYOUT BODY ═══════════════════════════════ -->
|
||||
<div class="gc-body">
|
||||
|
||||
<!-- Columna Izquierda: Calendario de Espacios -->
|
||||
<div class="ih-news-section">
|
||||
<div class="wf-card wf-card--calendar">
|
||||
<!-- ── SIDEBAR IZQUIERDO (256px) ── -->
|
||||
<aside class="gc-left-sidebar">
|
||||
|
||||
<div class="wf-card__header">
|
||||
<h3 class="wf-card__title">Calendario de Espacios</h3>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Vista toggle -->
|
||||
<div class="wf-view-toggle">
|
||||
<button class="wf-view-toggle__btn wf-view-toggle__btn--active">Día</button>
|
||||
<button class="wf-view-toggle__btn">Semana</button>
|
||||
<button class="wf-view-toggle__btn">Mes</button>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Cuadrícula Horaria por Recursos -->
|
||||
<div class="res-calendar-container">
|
||||
<div class="res-grid-header" [style.grid-template-columns]="'60px repeat(' + filteredResources().length + ', 1fr)'">
|
||||
<div class="res-time-column-label"></div>
|
||||
|
||||
<!-- Columnas de recursos dinámicas -->
|
||||
@for (res of filteredResources(); track res.id) {
|
||||
<div class="res-resource-column">
|
||||
<div class="res-resource-header">
|
||||
<span class="res-resource-icon" [ngClass]="{
|
||||
'res-resource-icon--yellow': res.icon === '☀️',
|
||||
'res-resource-icon--office': res.icon === '🏢'
|
||||
}">{{ res.icon }}</span>
|
||||
<div class="res-resource-info">
|
||||
<strong class="res-resource-name">{{ res.name }}</strong>
|
||||
<span class="res-resource-desc">{{ res.desc }}</span>
|
||||
<div class="gc-mini-calendar__days-header">
|
||||
<span>D</span><span>L</span><span>M</span><span>M</span><span>J</span><span>V</span><span>S</span>
|
||||
</div>
|
||||
|
||||
<div class="gc-mini-calendar__days-grid">
|
||||
@for (d of miniCalendarDays; track d) {
|
||||
<span class="gc-mini-calendar__day"
|
||||
[ngClass]="{
|
||||
'gc-mini-calendar__day--inactive': !d.currentMonth,
|
||||
'gc-mini-calendar__day--active': d.active
|
||||
}">
|
||||
{{ d.day }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Box inside Sidebar -->
|
||||
<div class="gc-sidebar-search">
|
||||
<div class="gc-search-box">
|
||||
<span class="material-icons gc-search-box__icon">people_outline</span>
|
||||
<input type="text" placeholder="Buscar a gente" class="gc-search-box__input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accordion: Mis Calendarios / Categorías de Reservas -->
|
||||
<div class="gc-accordion">
|
||||
<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>
|
||||
|
||||
<label class="gc-checkbox-item">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
<label class="gc-checkbox-item">
|
||||
<input type="checkbox" [checked]="isCategorySelected('Zona Relax')" (change)="toggleCategory('Zona Relax')" class="gc-checkbox-item__input gc-checkbox-item__input--yellow" />
|
||||
<span class="gc-checkbox-item__label">Zona Relax</span>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Accordion: Otros Calendarios / Mis Reservas Activas -->
|
||||
<div class="gc-accordion gc-accordion--reservas">
|
||||
<div class="gc-accordion__header">
|
||||
<span class="gc-accordion__title">Actividad de Reservas</span>
|
||||
<span class="material-icons gc-accordion__chevron">keyboard_arrow_up</span>
|
||||
</div>
|
||||
<div class="gc-accordion__content gc-accordion__content--scroll">
|
||||
@for (act of recentActivities(); track act.title) {
|
||||
<div class="gc-activity-item">
|
||||
<img [src]="act.avatar" alt="user" class="gc-activity-item__avatar" />
|
||||
<div class="gc-activity-item__body">
|
||||
<span class="gc-activity-item__title"><strong>{{ act.title }}</strong></span>
|
||||
<span class="gc-activity-item__desc">{{ act.detail }}</span>
|
||||
<span class="gc-activity-item__time">{{ act.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cuerpo de la cuadrícula -->
|
||||
<div class="res-grid-body">
|
||||
</aside>
|
||||
|
||||
<!-- ── CENTRAL CALENDAR VIEW (GRID WEEK VIEW STYLE) ── -->
|
||||
<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">
|
||||
|
||||
<!-- Column of hours scale (left side) -->
|
||||
<div class="gc-hours-column">
|
||||
@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>
|
||||
<div class="gc-hour-cell">
|
||||
<span class="gc-hour-label">{{ time }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Celdas para cada recurso -->
|
||||
@for (res of filteredResources(); track res.id) {
|
||||
<div class="res-slot-cell">
|
||||
<!-- 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)) {
|
||||
<!-- 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>
|
||||
<!-- 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 {
|
||||
<!-- Caso especial Oficina Flex A1 a las 12:00 y 13:30 (mostrando avatares ocupados en un slot flexible) -->
|
||||
|
||||
<!-- Flex space placeholder mock showing avatars on Oficina Flex -->
|
||||
@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 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="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 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 {
|
||||
<!-- Celda Disponible -->
|
||||
<div class="res-booking-block cursor-pointer hover:bg-slate-200/80" (click)="onSlotClick(res, time)"></div>
|
||||
}
|
||||
<!-- Empty Available Cell click to add -->
|
||||
<div class="gc-slot-click-zone" (click)="onSlotClick(res, time)" title="Haga clic para reservar"></div>
|
||||
}
|
||||
|
||||
<!-- Popup flotante interactivo de reserva dinámico -->
|
||||
}
|
||||
|
||||
<!-- Google Calendar style interactive Event creation popup -->
|
||||
@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 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>
|
||||
|
||||
<span class="res-popup-status">
|
||||
<span class="res-popup-status-dot"></span>
|
||||
Disponibilidad, disponible
|
||||
</span>
|
||||
<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>
|
||||
|
||||
@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 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>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha: Mis Reservas y Notificaciones (Dinámicas) -->
|
||||
<div class="ih-activity-section">
|
||||
<div class="ih-card-activity">
|
||||
<h3 class="ih-card-activity__title">Mis Reservas y Notificaciones</h3>
|
||||
|
||||
<div class="ih-activity-list">
|
||||
@for (act of recentActivities(); track act.title) {
|
||||
<div class="ih-activity-item animate-fadeIn">
|
||||
@if (act.type === 'reserva') {
|
||||
<img [src]="act.avatar" alt="res" class="ih-activity-avatar" />
|
||||
<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>
|
||||
|
||||
</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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -37,7 +37,7 @@ export class CalendarBookingComponent implements OnInit {
|
||||
]);
|
||||
|
||||
// Signals para gestionar estado interactivo
|
||||
activeCategory = signal<string>('Sala Reuniones');
|
||||
selectedCategories = signal<string[]>(['Sala Reuniones', 'Oficina Flexible', 'Zona Relax', 'Evento']);
|
||||
searchQuery = signal<string>('');
|
||||
|
||||
// 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' }
|
||||
];
|
||||
|
||||
// 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
|
||||
filteredResources = computed(() => {
|
||||
const cat = this.activeCategory();
|
||||
const selectedCats = this.selectedCategories();
|
||||
const query = this.searchQuery().toLowerCase().trim();
|
||||
return this.resources().filter(r =>
|
||||
r.type === cat &&
|
||||
selectedCats.includes(r.type) &&
|
||||
(!query || r.name.toLowerCase().includes(query) || r.desc.toLowerCase().includes(query))
|
||||
);
|
||||
});
|
||||
@ -95,9 +141,19 @@ export class CalendarBookingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
setCategory(cat: string): void {
|
||||
this.activeCategory.set(cat);
|
||||
this.selectedSlot.set(null); // Reset popup al cambiar pestaña
|
||||
toggleCategory(cat: string): void {
|
||||
this.selectedCategories.update(prev => {
|
||||
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 {
|
||||
|
||||
@ -237,63 +237,39 @@ import { Router } from '@angular/router';
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── LAYOUT PRINCIPAL ── */
|
||||
.wf-shell {
|
||||
display: flex;
|
||||
background: #f4f3ef; /* Light beige/grey background */
|
||||
min-height: 90vh;
|
||||
height: 100%;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(15, 13, 10, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||
font-family: 'Outfit', 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.wf-content {
|
||||
flex-grow: 1;
|
||||
padding: 24px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Topbar */
|
||||
@ -396,7 +372,11 @@ import { Router } from '@angular/router';
|
||||
.wf-grid {
|
||||
display: grid;
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user