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 component with explicit imports
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
Represents a single day in the calendar view
interface CalendarDay {
date : Date ;
isCurrentMonth : boolean ;
isToday : boolean ;
appointments : AppointmentData [];
}
Properties :
The date object for this calendar day
Whether this day belongs to the current month (always true for weekly view)
Whether this day is today’s date
Array of appointments scheduled for this day
Properties
currentDate
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
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
Controls the visibility of the new appointment modal
Implementation : src/app/calendar/calendar.ts:24
selectedDateForNewAppointment
selectedDateForNewAppointment
The date selected when creating a new appointment (YYYY-MM-DD format)
Implementation : src/app/calendar/calendar.ts:25
hours
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()
Lifecycle hook that generates the calendar on component initialization
Implementation : src/app/calendar/calendar.ts:33-35
generateCalendar()
Generates the calendar grid for the current week with appointments
This method:
Calculates the Monday of the current week
Creates 7 CalendarDay objects (Monday through Sunday)
Filters appointments for each day
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()
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()
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()
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()
Alias for getWeekRange() used in the template
Implementation : src/app/calendar/calendar.ts:125-127
openNewAppointment()
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()
Closes the new appointment modal
Implementation : src/app/calendar/calendar.ts:142-144
onAppointmentCreated()
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()
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:
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
The main calendar grid has two columns:
Time Axis
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
Seven columns, one for each day of the week
Column Header
Column Body
Appointment Block
< 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. < div class = "column-body" (click) = "openNewAppointment(day.date)" >
<!-- Grid Lines -->
< div *ngFor = "let hour of hours" class = "grid-line" ></ div >
<!-- Appointments -->
< div *ngFor = "let app of day.appointments"
class = "timetable-appointment"
[class.status-confirmada] = "app.estado === 'confirmada'"
[class.status-pendiente] = "app.estado === 'pendiente'"
[style.top] = "getAppointmentTop(app.hora)" >
<!-- appointment content -->
</ div >
</ div >
Contains grid lines and absolutely positioned appointment blocks. < div class = "app-content" >
< div class = "app-header" >
< span class = "app-time" > {{ app.hora }} </ span >
< span class = "app-patient" > {{ app.paciente }} </ span >
</ div >
< div class = "app-body" >
< span class = "app-treatment" > {{ app.tratamiento }} </ span >
</ div >
</ div >
Displays appointment time, patient name, and treatment.
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
The component handles multiple date formats:
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:
Spanish abbreviated weekday names
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)" >
For large numbers of appointments, consider:
Virtual Scrolling : Only render visible time slots
Debounce Navigation : Prevent rapid week changes
Memoization : Cache filtered appointments per day
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