feat: add info-hub, calendar-booking, and workflows components to frontend features
This commit is contained in:
parent
1965494cc3
commit
705f263de5
@ -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">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;"><</button>
|
||||
<button (click)="next()" class="nav-arrow" style="width:24px; height:24px; font-size:12px;">></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"><</button>
|
||||
<button (click)="next()" class="nav-arrow">></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>
|
||||
@ -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'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user