feat: add info-hub, calendar-booking, and workflows components to frontend features

This commit is contained in:
Jessica Orozco 2026-06-16 15:26:46 -04:00
parent 1965494cc3
commit 705f263de5
5 changed files with 799 additions and 834 deletions

View File

@ -11,13 +11,6 @@ import { Router } from '@angular/router';
template: `
<div class="ih-shell">
<!-- ═══════════════════════════════ SIDEBAR IZQUIERDO ═══════════════════════════════ -->
<aside class="ih-sidebar">
<div class="ih-sidebar__brand">APP</div>
<div class="ih-sidebar__logo-container">
<div class="ih-sidebar__logo-btn" (click)="router.navigate(['/dashboard'])">A</div>
</div>
</aside>
<!-- ═══════════════════════════════ CONTENIDO PRINCIPAL ═══════════════════════════════ -->
<main class="ih-content">

View File

@ -1,234 +1,201 @@
<div class="calendar-layout">
<!-- Sidebar de Filtros e Info -->
<aside class="calendar-sidebar">
<button (click)="selectDay(currentDate())" class="btn-create">
<span class="plus-icon">+</span>
<span>Crear Reserva</span>
<div class="wf-shell">
<!-- ═══════════════════════════════ CONTENIDO PRINCIPAL ═══════════════════════════════ -->
<main class="wf-content">
<!-- Barra superior oscura -->
<header class="wf-topbar">
<div class="wf-topbar__header-row">
<h1 class="wf-topbar__title">RESERVAS DE ESPACIOS</h1>
<!-- 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">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<!-- Mini Calendario -->
<div class="mini-calendar">
<div class="mini-calendar-header">
<span class="mini-calendar-title">{{ monthYearLabel() }}</span>
<div class="mini-calendar-nav">
<button (click)="prev()" class="nav-arrow" style="width:24px; height:24px; font-size:12px;">&lt;</button>
<button (click)="next()" class="nav-arrow" style="width:24px; height:24px; font-size:12px;">&gt;</button>
</div>
</div>
<div class="mini-calendar-grid">
<!-- Nombres días -->
<span class="mini-day-name">D</span>
<span class="mini-day-name">L</span>
<span class="mini-day-name">M</span>
<span class="mini-day-name">M</span>
<span class="mini-day-name">J</span>
<span class="mini-day-name">V</span>
<span class="mini-day-name">S</span>
<!-- Celdas -->
@for (day of calendarDays().slice(0, 35); track day.getTime()) {
<span (click)="selectDay(day)" [class.today]="isToday(day)" [class.other-month]="!isCurrentMonth(day)"
class="mini-day-cell">
{{ day.getDate() }}
</span>
}
<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>
<!-- Filtros de Recursos -->
<div class="filter-section">
<h4 class="filter-title">Categorías</h4>
<div class="filter-list">
<label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'all'" (change)="setFilter('all')"
class="filter-checkbox">
<span>Ver Todo</span>
</label>
<label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'room'" (change)="setFilter('room')"
class="filter-checkbox">
<span>Salas de Juntas</span>
</label>
<label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'desk'" (change)="setFilter('desk')"
class="filter-checkbox">
<span>Escritorios</span>
</label>
<label class="filter-item">
<input type="radio" name="resType" [checked]="selectedFilter() === 'asset'" (change)="setFilter('asset')"
class="filter-checkbox">
<span>Activos</span>
</label>
</div>
</div>
</aside>
<!-- 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>
<!-- Panel Principal -->
<main class="calendar-main">
<header class="main-header">
<div class="main-header-left">
<div class="header-title-group">
<div class="header-app-logo">App <span>Intranet</span></div>
</div>
<button (click)="today()" class="btn-today">Hoy</button>
<button (click)="prev()" class="nav-arrow">&lt;</button>
<button (click)="next()" class="nav-arrow">&gt;</button>
<span class="current-date-label">{{ monthYearLabel() }}</span>
</div>
<div class="main-header-right">
<select [value]="currentView()" (change)="setView($any($event.target).value)" class="view-select">
<option value="month">Mes</option>
<option value="week">Semana</option>
<option value="day">Día</option>
</select>
<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>
</header>
<!-- Calendario Contenido -->
<div class="calendar-view-container">
<!-- Grid de Calendario de Espacios y Mis Reservas -->
<div class="ih-grid">
<!-- 1. VISTA SEMANAL -->
@if (currentView() === 'week') {
<div class="week-view animate-fadeIn">
<!-- Encabezado de la semana -->
<div class="week-header">
<div class="timezone-label">GMT-04</div>
@for (day of currentWeekDays(); track day.getTime()) {
<div (click)="selectDay(day)" class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors">
<span class="day-name">{{ weekDays[day.getDay()] }}</span>
<span [class.today]="isToday(day)" class="day-number">
{{ day.getDate() }}
<!-- Columna Izquierda: Calendario de Espacios -->
<div class="ih-news-section">
<div class="wf-card wf-card--calendar">
<div class="wf-card__header">
<h3 class="wf-card__title">Calendario de Espacios</h3>
<!-- 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>
</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>
</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>
<!-- Cuerpo con cuadrícula de horas -->
<div class="week-grid-body">
<div class="hour-labels">
@for (hour of hours; track hour) {
<div class="hour-label-cell">
{{ formatHour(hour) }}
</div>
}
</div>
<!-- Columnas por día -->
@for (day of currentWeekDays(); track day.getTime()) {
<div class="day-column">
@if (isToday(day)) {
<div [style.top.px]="getCurrentTimeTop()" class="current-time-indicator"></div>
}
@for (hour of hours; track hour) {
<div (click)="selectTimeSlot(day, hour)" class="hour-slot-cell"></div>
}
<!-- Eventos -->
@for (event of getEventsForDay(day); track event.id) {
<div (click)="$event.stopPropagation(); selectDay(day)" [style]="getEventStyle(event)"
[class]="event.colorClass" class="calendar-event cursor-pointer">
<span class="event-time">{{ event.start | date:'shortTime' }} - {{ event.end | date:'shortTime' }}</span>
<strong>{{ event.title }}</strong>
<span>{{ event.resourceName }}</span>
</div>
}
</div>
}
</div>
</div>
}
<!-- 2. VISTA MENSUAL -->
@if (currentView() === 'month') {
<div class="animate-fadeIn" style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<!-- Fila de nombres de días -->
<div class="day-names-row">
@for (dayName of weekDays; track $index) {
<div class="day-name-cell">
{{ dayName }}
</div>
}
</div>
<!-- Cuadrícula mensual -->
<div class="grid-month">
@for (day of calendarDays(); track day.getTime()) {
<div (click)="selectDay(day)" [class.other-month]="!isCurrentMonth(day)" class="month-day-cell">
<div style="display: flex; justify-content: flex-start; align-items: center; margin-bottom: 4px;">
<span [class.today]="isToday(day)" class="month-day-number">
{{ day.getDate() }}
</span>
</div>
<div style="flex: 1; display: flex; flex-direction: column; gap: 4px; overflow-y: auto;">
@for (event of getEventsForDay(day); track event.id) {
<div (click)="$event.stopPropagation(); selectDay(day)" [class]="event.colorClass" class="calendar-event"
style="position: static; width: auto; margin: 0; box-shadow: none; border-left-width: 3px;"
[title]="event.title">
<div style="font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{
event.title }}</div>
<div
style="font-size: 9px; opacity: 0.75; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ event.resourceName }}</div>
</div>
}
</div>
</div>
}
</div>
</div>
}
<!-- 3. VISTA DIARIA -->
@if (currentView() === 'day') {
<div class="week-view animate-fadeIn">
<!-- Encabezado de la semana -->
<div class="week-header" style="grid-template-cols: 64px 1fr;">
<div class="timezone-label">GMT-04</div>
<div (click)="selectDay(currentDate())"
class="week-day-header cursor-pointer hover:bg-slate-50 transition-colors">
<span class="day-name">{{ weekDays[currentDate().getDay()] }}</span>
<span [class.today]="isToday(currentDate())" class="day-number">
{{ currentDate().getDate() }}
</span>
</div>
</div>
<!-- Cuerpo con cuadrícula de horas -->
<div class="week-grid-body" style="grid-template-cols: 64px 1fr;">
<div class="hour-labels">
@for (hour of hours; track hour) {
<div class="hour-label-cell">
{{ formatHour(hour) }}
@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>
<!-- Columna única por el día actual -->
<div class="day-column">
@if (isToday(currentDate())) {
<div [style.top.px]="getCurrentTimeTop()" class="current-time-indicator"></div>
}
@for (hour of hours; track hour) {
<div (click)="selectTimeSlot(currentDate(), hour)" class="hour-slot-cell"></div>
}
</div>
</div>
<!-- Eventos -->
@for (event of getEventsForDay(currentDate()); track event.id) {
<div (click)="$event.stopPropagation(); selectDay(currentDate())" [style]="getEventStyle(event)"
[class]="event.colorClass" class="calendar-event cursor-pointer">
<span class="event-time">{{ event.start | date:'shortTime' }} - {{ event.end | date:'shortTime' }}</span>
<strong>{{ event.title }}</strong>
<span>{{ event.resourceName }}</span>
<!-- 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>
</div>

View File

@ -2,14 +2,13 @@ import { Component, OnInit, signal, computed, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterModule } from '@angular/router';
export interface CalendarEvent {
export interface Resource {
id: string;
title: string;
start: Date;
end: Date;
type: 'room' | 'desk' | 'asset';
resourceName: string;
colorClass: string;
name: string;
type: 'Sala Reuniones' | 'Oficina Flexible' | 'Zona Relax' | 'Evento';
icon: string;
desc: string;
capacity: number;
}
@Component({
@ -22,201 +21,137 @@ export interface CalendarEvent {
export class CalendarBookingComponent implements OnInit {
private router = inject(Router);
// Array de horas (0 a 23)
hours: number[] = Array.from({ length: 24 }, (_, i) => i);
// Horarios de la captura
timeSlots = ['08:00', '09:30', '10:00', '11:30', '12:00', '13:30', '14:00', '15:00'];
// 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'
}
// Base de datos de recursos por categoría
resources = signal<Resource[]>([
{ id: '1', name: 'SALA LUNA', type: 'Sala Reuniones', icon: '🌙', desc: 'Sala Reuniones resutenias or Sala Luna', capacity: 8 },
{ id: '2', name: 'SALA SOL', type: 'Sala Reuniones', icon: '☀️', desc: 'Estables:nación de nuevos luxes', capacity: 12 },
{ id: '3', name: 'OFICINA FLEX', type: 'Oficina Flexible', icon: '🏢', desc: 'Resultalioes sala tia r Oficina flexible', capacity: 1 },
{ id: '4', name: 'OFICINA FLEX A1', type: 'Oficina Flexible', icon: '🏢', desc: 'Oficina de espacios resutentu-u planetia', capacity: 1 },
{ id: '5', name: 'ZONA COFFEE', type: 'Zona Relax', icon: '☕', desc: 'Espacio de café y socialización', capacity: 20 },
{ id: '6', name: 'SALA DE JUEGOS', type: 'Zona Relax', icon: '🎮', desc: 'Mesa de ping pong y consolas', capacity: 10 },
{ id: '7', name: 'AUDITORIO A', type: 'Evento', icon: '🎤', desc: 'Auditorio principal para presentaciones', capacity: 100 },
{ id: '8', name: 'TERRAZA', type: 'Evento', icon: '🌿', desc: 'Espacio abierto para eventos corporativos', capacity: 50 }
]);
// Lista de días de la semana
weekDays = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'];
// Signals para gestionar estado interactivo
activeCategory = signal<string>('Sala Reuniones');
searchQuery = signal<string>('');
// Nombre legible del mes y año actual
monthYearLabel = computed(() => {
const date = this.currentDate();
return date.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' });
// Slot seleccionado para el popup dinámico
selectedSlot = signal<{ resource: Resource, time: string } | null>(null);
// Ocupación en tiempo real (inicializada con la captura)
reservations = signal<any[]>([
{ resourceName: 'SALA LUNA', time: '08:00', author: 'Sistema', date: 'Hace 10h' },
{ resourceName: 'SALA LUNA', time: '09:30', author: 'Sistema', date: 'Hace 10h' },
{ resourceName: 'SALA LUNA', time: '13:30', author: 'Sistema', date: 'Hace 10h' },
{ resourceName: 'SALA LUNA', time: '14:00', author: 'Sistema', date: 'Hace 10h' },
{ resourceName: 'SALA SOL', time: '08:00', author: 'Sistema', date: 'Hace 10h' },
{ resourceName: 'SALA SOL', time: '09:30', author: 'Sistema', date: 'Hace 10h' }
]);
// Notificaciones estáticas de David Chen
staticNotifications = [
{ 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' }
];
// Computed signals
filteredResources = computed(() => {
const cat = this.activeCategory();
const query = this.searchQuery().toLowerCase().trim();
return this.resources().filter(r =>
r.type === cat &&
(!query || r.name.toLowerCase().includes(query) || r.desc.toLowerCase().includes(query))
);
});
// 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));
recentActivities = computed(() => {
// Convertir reservas a actividades
const resList = this.reservations().map(res => {
let avatar = 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=120&q=80';
if (res.resourceName.includes('LUNA')) {
avatar = 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=120&q=80';
}
// 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();
}
// Días de la semana actual para la vista semanal
currentWeekDays = computed(() => {
const today = this.currentDate();
const startOfWeek = new Date(today);
// Ajustar para que comience en el domingo de la semana
startOfWeek.setDate(today.getDate() - today.getDay());
const days: Date[] = [];
for (let i = 0; i < 7; i++) {
const d = new Date(startOfWeek);
d.setDate(startOfWeek.getDate() + i);
days.push(d);
}
return days;
});
formatHour(hour: number): string {
if (hour === 0) return '12 AM';
if (hour === 12) return '12 PM';
return hour > 12 ? `${hour - 12} PM` : `${hour} AM`;
}
getEventStyle(event: CalendarEvent): any {
const startHour = event.start.getHours() + event.start.getMinutes() / 60;
const endHour = event.end.getHours() + event.end.getMinutes() / 60;
const duration = Math.max(endHour - startHour, 0.5); // mínimo 30 min
const top = startHour * 60; // 60px por hora
const height = duration * 60;
return {
top: `${top}px`,
height: `${height}px`
title: res.resourceName,
detail: `Reservado ${res.time}`,
time: res.date,
type: 'reserva',
avatar: avatar
};
});
return [...resList, ...this.staticNotifications];
});
ngOnInit(): void {
// Inicializar el popup en Sala Luna de las 10:00 para emular la captura al cargar
const luna = this.resources().find(r => r.name === 'SALA LUNA');
if (luna) {
this.selectedSlot.set({ resource: luna, time: '11:30' });
}
}
getCurrentTimeTop(): number {
const now = new Date();
const currentHour = now.getHours() + now.getMinutes() / 60;
return currentHour * 60; // 60px por hora
setCategory(cat: string): void {
this.activeCategory.set(cat);
this.selectedSlot.set(null); // Reset popup al cambiar pestaña
}
isSameDay(date1: Date, date2: Date): boolean {
return date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear();
onSearch(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.searchQuery.set(value);
}
selectTimeSlot(day: Date, hour: number): void {
const dateStr = day.toISOString().split('T')[0];
const hourStr = String(hour).padStart(2, '0') + ':00';
isSlotOccupied(resourceName: string, time: string): boolean {
return this.reservations().some(res => res.resourceName === resourceName && res.time === time);
}
onSlotClick(resource: Resource, time: string): void {
if (this.isSlotOccupied(resource.name, time)) {
return;
}
this.selectedSlot.set({ resource, time });
}
getEndTime(startTime: string): string {
const [h, m] = startTime.split(':').map(Number);
let fh = h + 1;
let fm = m + 30;
if (fm >= 60) {
fh += 1;
fm -= 60;
}
return `${String(fh).padStart(2, '0')}:${String(fm).padStart(2, '0')}`;
}
confirmReserva(): void {
const slot = this.selectedSlot();
if (slot) {
const newRes = {
resourceName: slot.resource.name,
time: slot.time,
author: 'Jess Miller',
date: 'Hace un momento'
};
this.reservations.update(prev => [newRes, ...prev]);
this.selectedSlot.set(null);
}
}
closePopup(): void {
this.selectedSlot.set(null);
}
navigateToNuevo(): void {
const slot = this.selectedSlot();
this.router.navigate(['/reservas/nuevo'], {
queryParams: {
fecha: dateStr,
hora: hourStr,
tipo: this.selectedFilter() !== 'all' ? this.selectedFilter() : 'room'
fecha: new Date().toISOString().split('T')[0],
tipo: slot ? (slot.resource.type === 'Sala Reuniones' ? 'room' : 'desk') : 'room',
hora: slot ? slot.time : '10:00'
}
});
}

View File

@ -11,13 +11,6 @@ import { Router } from '@angular/router';
template: `
<div class="wf-shell">
<!-- ═══════════════════════════════ SIDEBAR IZQUIERDO ═══════════════════════════════ -->
<aside class="wf-sidebar">
<div class="wf-sidebar__brand">APP</div>
<div class="wf-sidebar__logo-container">
<div class="wf-sidebar__logo-btn">A</div>
</div>
</aside>
<!-- ═══════════════════════════════ CONTENIDO PRINCIPAL ═══════════════════════════════ -->
<main class="wf-content">