Skip to main content

Overview

The Calendar component provides a weekly calendar view that displays appointments in a time-grid format. Users can navigate between weeks, view appointments positioned by their scheduled time, and create new appointments by clicking on available time slots.

Component Definition

selector
string
default:"app-calendar"
The CSS selector used to include this component in templates
standalone
boolean
default:true
Standalone component with explicit imports
imports
array
Dependencies: CommonModule, NewAppointment

Source Code Location

File: src/app/calendar/calendar.ts:13
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AppointmentService, AppointmentData } from '../services/appointment.service';
import { NewAppointment } from '../new-appointment/new-appointment';

interface CalendarDay {
  date: Date;
  isCurrentMonth: boolean;
  isToday: boolean;
  appointments: AppointmentData[];
}

@Component({
  selector: 'app-calendar',
  standalone: true,
  imports: [CommonModule, NewAppointment],
  templateUrl: './calendar.html',
  styleUrl: './calendar.css',
})
export class Calendar implements OnInit {
  currentDate: Date = new Date();
  days: CalendarDay[] = [];
  weekDays: string[] = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
  showNewAppointmentModal: boolean = false;
  selectedDateForNewAppointment: string = '';
  hours: string[] = [
    '08:00', '09:00', '10:00', '11:00', '12:00', '13:00',
    '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00'
  ];

  constructor(private appointmentService: AppointmentService) { }

  ngOnInit(): void {
    this.generateCalendar();
  }

  generateCalendar(): void {
    const year = this.currentDate.getFullYear();
    const month = this.currentDate.getMonth();
    const day = this.currentDate.getDate();

    const dateCopy = new Date(year, month, day);
    let dayOfWeek = dateCopy.getDay();
    // Adjust to Monday start (0=Sun, 1=Mon...6=Sat)
    let diff = dateCopy.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
    const startOfWeek = new Date(dateCopy.setDate(diff));

    this.days = [];
    const allAppointments = this.appointmentService.getAppointments();

    for (let i = 0; i < 7; i++) {
      const date = new Date(
        startOfWeek.getFullYear(), 
        startOfWeek.getMonth(), 
        startOfWeek.getDate() + i
      );
      const isToday = this.isSameDay(date, new Date());
      const dayAppointments = allAppointments.filter(app => 
        this.isAppointmentOnDay(app, date)
      );

      this.days.push({
        date,
        isCurrentMonth: true,
        isToday,
        appointments: dayAppointments
      });
    }
  }

  isAppointmentOnDay(app: AppointmentData, date: Date): boolean {
    if (!app.fecha) return false;

    // Handle YYYY-MM-DD format
    if (app.fecha.includes('-')) {
      const [y, m, d] = app.fecha.split('-').map(Number);
      return d === date.getDate() && 
             (m - 1) === date.getMonth() && 
             y === date.getFullYear();
    }

    // Handle D/M/YYYY format
    if (app.fecha.includes('/')) {
      const [d, m, y] = app.fecha.split('/').map(Number);
      return d === date.getDate() && 
             (m - 1) === date.getMonth() && 
             y === date.getFullYear();
    }

    return false;
  }

  isSameDay(date1: Date, date2: Date): boolean {
    return date1.getDate() === date2.getDate() &&
      date1.getMonth() === date2.getMonth() &&
      date1.getFullYear() === date2.getFullYear();
  }

  prevWeek(): void {
    this.currentDate.setDate(this.currentDate.getDate() - 7);
    this.generateCalendar();
  }

  nextWeek(): void {
    this.currentDate.setDate(this.currentDate.getDate() + 7);
    this.generateCalendar();
  }

  getWeekRange(): string {
    const start = this.days[0]?.date;
    const end = this.days[6]?.date;
    if (!start || !end) return '';

    const formatMonth = new Intl.DateTimeFormat('es-ES', { month: 'short' });
    const formatYear = new Intl.DateTimeFormat('es-ES', { year: 'numeric' });

    if (start.getMonth() === end.getMonth()) {
      return `${start.getDate()} - ${end.getDate()} ${formatMonth.format(start)} ${start.getFullYear()}`;
    }
    return `${start.getDate()} ${formatMonth.format(start)} - ${end.getDate()} ${formatMonth.format(end)} ${start.getFullYear()}`;
  }

  getMonthName(): string {
    return this.getWeekRange();
  }

  openNewAppointment(date?: Date): void {
    if (date) {
      const y = date.getFullYear();
      const m = String(date.getMonth() + 1).padStart(2, '0');
      const d = String(date.getDate()).padStart(2, '0');
      this.selectedDateForNewAppointment = `${y}-${m}-${d}`;
    } else {
      this.selectedDateForNewAppointment = '';
    }
    this.showNewAppointmentModal = true;
  }

  closeNewAppointment(): void {
    this.showNewAppointmentModal = false;
  }

  onAppointmentCreated(): void {
    this.generateCalendar();
    this.closeNewAppointment();
  }

  getAppointmentTop(time: string): string {
    const [hours, minutes] = time.split(':').map(Number);
    const startHour = 8;
    const hourHeight = 60; // Should match row height in CSS
    const top = (hours - startHour) * hourHeight + (minutes / 60) * hourHeight;
    return `${top}px`;
  }
}

Interfaces

CalendarDay

CalendarDay
interface
Represents a single day in the calendar view
interface CalendarDay {
  date: Date;
  isCurrentMonth: boolean;
  isToday: boolean;
  appointments: AppointmentData[];
}
Properties:
date
Date
The date object for this calendar day
isCurrentMonth
boolean
Whether this day belongs to the current month (always true for weekly view)
isToday
boolean
Whether this day is today’s date
appointments
AppointmentData[]
Array of appointments scheduled for this day

Properties

currentDate

currentDate
Date
default:"new Date()"
The reference date for the current week being displayed
Implementation: src/app/calendar/calendar.ts:21

days

days
CalendarDay[]
default:"[]"
Array of 7 days representing the current week
Implementation: src/app/calendar/calendar.ts:22

weekDays

weekDays
string[]
Array of abbreviated weekday names in Spanish
weekDays: string[] = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
Implementation: src/app/calendar/calendar.ts:23

showNewAppointmentModal

showNewAppointmentModal
boolean
default:false
Controls the visibility of the new appointment modal
Implementation: src/app/calendar/calendar.ts:24

selectedDateForNewAppointment

selectedDateForNewAppointment
string
default:"''"
The date selected when creating a new appointment (YYYY-MM-DD format)
Implementation: src/app/calendar/calendar.ts:25

hours

hours
string[]
Array of hour labels for the time axis (8:00 AM to 8:00 PM)
hours: string[] = [
  '08:00', '09:00', '10:00', '11:00', '12:00', '13:00',
  '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00'
];
Implementation: src/app/calendar/calendar.ts:26-29

Methods

ngOnInit()

ngOnInit
() => void
Lifecycle hook that generates the calendar on component initialization
Implementation: src/app/calendar/calendar.ts:33-35

generateCalendar()

generateCalendar
() => void
Generates the calendar grid for the current week with appointments
This method:
  1. Calculates the Monday of the current week
  2. Creates 7 CalendarDay objects (Monday through Sunday)
  3. Filters appointments for each day
  4. Marks today’s date
Implementation: src/app/calendar/calendar.ts:37-70
const dateCopy = new Date(year, month, day);
let dayOfWeek = dateCopy.getDay();
// Adjust to Monday start (0=Sun, 1=Mon...6=Sat)
let diff = dateCopy.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
const startOfWeek = new Date(dateCopy.setDate(diff));

isAppointmentOnDay()

isAppointmentOnDay
(app: AppointmentData, date: Date) => boolean
Checks if an appointment is scheduled for a specific date
Handles two date formats:
  • YYYY-MM-DD: ISO format
  • D/M/YYYY: European format
Implementation: src/app/calendar/calendar.ts:72-93

isSameDay()

isSameDay
(date1: Date, date2: Date) => boolean
Compares two dates to check if they are the same day
Implementation: src/app/calendar/calendar.ts:95-99

prevWeek()

prevWeek
() => void
Navigates to the previous week
prevWeek(): void {
  this.currentDate.setDate(this.currentDate.getDate() - 7);
  this.generateCalendar();
}
Implementation: src/app/calendar/calendar.ts:101-104

nextWeek()

nextWeek
() => void
Navigates to the next week
nextWeek(): void {
  this.currentDate.setDate(this.currentDate.getDate() + 7);
  this.generateCalendar();
}
Implementation: src/app/calendar/calendar.ts:106-109

getWeekRange()

getWeekRange
() => string
Returns a formatted string representing the date range of the current week
Examples:
  • “1 - 7 mar 2026” (same month)
  • “29 feb - 6 mar 2026” (spanning months)
Implementation: src/app/calendar/calendar.ts:111-123

getMonthName()

getMonthName
() => string
Alias for getWeekRange() used in the template
Implementation: src/app/calendar/calendar.ts:125-127

openNewAppointment()

openNewAppointment
(date?: Date) => void
Opens the new appointment modal, optionally pre-filling a date
If a date is provided, it’s formatted as YYYY-MM-DD for the date input. Implementation: src/app/calendar/calendar.ts:129-140

closeNewAppointment()

closeNewAppointment
() => void
Closes the new appointment modal
Implementation: src/app/calendar/calendar.ts:142-144

onAppointmentCreated()

onAppointmentCreated
() => void
Event handler called when a new appointment is created
Regenerates the calendar and closes the modal. Implementation: src/app/calendar/calendar.ts:146-149

getAppointmentTop()

getAppointmentTop
(time: string) => string
Calculates the top position (in pixels) for an appointment based on its time
Converts appointment time to a pixel offset from the start of the day (8:00 AM). Implementation: src/app/calendar/calendar.ts:151-157
getAppointmentTop(time: string): string {
  const [hours, minutes] = time.split(':').map(Number);
  const startHour = 8;
  const hourHeight = 60; // Should match row height in CSS
  const top = (hours - startHour) * hourHeight + (minutes / 60) * hourHeight;
  return `${top}px`;
}

Template Structure

The calendar template consists of several key sections:
calendar-header
header
  • Title: “Calendario”
  • Week range: Formatted date range (e.g., “1 - 7 mar 2026”)
  • Navigation buttons: Previous/Next week buttons
  • Nueva Cita button: Opens new appointment modal

Timetable Container

The main calendar grid has two columns:

Time Axis

time-axis
div
Vertical list of hour labels from 08:00 to 20:00
<div class="time-axis">
  <div class="axis-header"></div>
  <div *ngFor="let hour of hours" class="hour-label">
    {{ hour }}
  </div>
</div>

Days Columns

days-columns
div
Seven columns, one for each day of the week
<div class="column-header" [class.today]="day.isToday">
  <span class="day-name">{{ weekDays[i] }}</span>
  <span class="day-num">{{ day.date.getDate() }}</span>
</div>
Displays weekday name and date number. Highlighted if it’s today.

Appointment Positioning

Appointments are positioned using absolute positioning with dynamic top values:
<div *ngFor="let app of day.appointments" 
     class="timetable-appointment"
     [style.top]="getAppointmentTop(app.hora)">
The getAppointmentTop() method calculates the pixel offset based on:
  • Start hour: 8:00 AM (hour 8)
  • Hour height: 60px per hour
  • Minutes offset: Proportional within the hour
// 09:00 → (9 - 8) * 60 + 0 = 60px
// 09:30 → (9 - 8) * 60 + (30/60) * 60 = 90px
// 14:15 → (14 - 8) * 60 + (15/60) * 60 = 375px

Status Styling

Appointments have dynamic CSS classes based on their status:
[class.status-confirmada]="app.estado === 'confirmada'"
[class.status-pendiente]="app.estado === 'pendiente'"
Status Classes:
  • status-confirmada: Green background for confirmed appointments
  • status-pendiente: Yellow background for pending appointments

Date Format Handling

The component handles multiple date formats:
Format: YYYY-MM-DD (e.g., “2026-03-15”)
if (app.fecha.includes('-')) {
  const [y, m, d] = app.fecha.split('-').map(Number);
  return d === date.getDate() && 
         (m - 1) === date.getMonth() && 
         y === date.getFullYear();
}

Usage Example

The Calendar component is used as a routed component:
import { Routes } from '@angular/router';
import { Calendar } from './calendar/calendar';

export const routes: Routes = [
  {
    path: 'calendar',
    component: Calendar,
    data: { title: 'Calendario' }
  }
];

Internationalization

The component uses Spanish localization:
weekDays
string[]
Spanish abbreviated weekday names
Intl.DateTimeFormat
API
Browser API for date formatting with ‘es-ES’ locale
const formatMonth = new Intl.DateTimeFormat('es-ES', { month: 'short' });
const formatYear = new Intl.DateTimeFormat('es-ES', { year: 'numeric' });

Customization

Change Business Hours

Modify the hours array to display different time ranges:
hours: string[] = [
  '07:00', '08:00', '09:00', '10:00', '11:00', '12:00',
  '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00'
];
Don’t forget to update the startHour in getAppointmentTop():
const startHour = 7; // Changed from 8

Switch to Month View

Modify generateCalendar() to display a full month instead of a week:
generateCalendar(): void {
  const year = this.currentDate.getFullYear();
  const month = this.currentDate.getMonth();
  const firstDay = new Date(year, month, 1);
  const lastDay = new Date(year, month + 1, 0);
  
  // Generate days for entire month
  this.days = [];
  for (let d = 1; d <= lastDay.getDate(); d++) {
    const date = new Date(year, month, d);
    // ... populate calendar day
  }
}

Add Appointment Duration Display

Show appointment blocks with height based on duration:
getAppointmentHeight(duration: string): string {
  const minutes = parseInt(duration); // Assumes duration like "30 min"
  const hourHeight = 60;
  return `${(minutes / 60) * hourHeight}px`;
}
In template:
<div class="timetable-appointment"
     [style.top]="getAppointmentTop(app.hora)"
     [style.height]="getAppointmentHeight(app.duracion)">

Add Click Handler for Appointments

Make appointments clickable to view details:
onAppointmentClick(appointment: AppointmentData, event: Event): void {
  event.stopPropagation(); // Prevent day click
  this.router.navigate(['/appointment', appointment.id]);
}
<div *ngFor="let app of day.appointments" 
     class="timetable-appointment"
     (click)="onAppointmentClick(app, $event)">

Performance Considerations

For large numbers of appointments, consider:
  1. Virtual Scrolling: Only render visible time slots
  2. Debounce Navigation: Prevent rapid week changes
  3. Memoization: Cache filtered appointments per day
  4. Lazy Loading: Load appointment details on demand

Appointment View

List view of appointments

Appointment Service

Service for appointment data

Calendar Feature

Calendar feature documentation

Appointment Scheduling

Appointment scheduling feature

Build docs developers (and LLMs) love