WIP: development_backend #4
							
								
								
									
										0
									
								
								actix_mvc_app/src/db/calendar.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								actix_mvc_app/src/db/calendar.rs
									
									
									
									
									
										Normal file
									
								
							@@ -26,11 +26,16 @@ impl std::fmt::Display for TemplateError {
 | 
			
		||||
 | 
			
		||||
impl std::error::Error for TemplateError {}
 | 
			
		||||
 | 
			
		||||
/// Registers custom Tera functions
 | 
			
		||||
/// Registers custom Tera functions and filters
 | 
			
		||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
 | 
			
		||||
    tera.register_function("now", NowFunction);
 | 
			
		||||
    tera.register_function("format_date", FormatDateFunction);
 | 
			
		||||
    tera.register_function("local_time", LocalTimeFunction);
 | 
			
		||||
 | 
			
		||||
    // Register custom filters
 | 
			
		||||
    tera.register_filter("format_hour", format_hour_filter);
 | 
			
		||||
    tera.register_filter("extract_hour", extract_hour_filter);
 | 
			
		||||
    tera.register_filter("format_time", format_time_filter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera function to get the current date/time
 | 
			
		||||
@@ -140,6 +145,69 @@ impl Function for LocalTimeFunction {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera filter to format hour with zero padding
 | 
			
		||||
pub fn format_hour_filter(
 | 
			
		||||
    value: &Value,
 | 
			
		||||
    _args: &std::collections::HashMap<String, Value>,
 | 
			
		||||
) -> tera::Result<Value> {
 | 
			
		||||
    match value.as_i64() {
 | 
			
		||||
        Some(hour) => Ok(Value::String(format!("{:02}", hour))),
 | 
			
		||||
        None => Err(tera::Error::msg("Value must be a number")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera filter to extract hour from datetime string
 | 
			
		||||
pub fn extract_hour_filter(
 | 
			
		||||
    value: &Value,
 | 
			
		||||
    _args: &std::collections::HashMap<String, Value>,
 | 
			
		||||
) -> tera::Result<Value> {
 | 
			
		||||
    match value.as_str() {
 | 
			
		||||
        Some(datetime_str) => {
 | 
			
		||||
            // Try to parse as RFC3339 first
 | 
			
		||||
            if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
 | 
			
		||||
                Ok(Value::String(dt.format("%H").to_string()))
 | 
			
		||||
            } else {
 | 
			
		||||
                // Try to parse as our standard format
 | 
			
		||||
                match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
 | 
			
		||||
                    Ok(dt) => Ok(Value::String(dt.format("%H").to_string())),
 | 
			
		||||
                    Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        None => Err(tera::Error::msg("Value must be a string")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Tera filter to format time from datetime string
 | 
			
		||||
pub fn format_time_filter(
 | 
			
		||||
    value: &Value,
 | 
			
		||||
    args: &std::collections::HashMap<String, Value>,
 | 
			
		||||
) -> tera::Result<Value> {
 | 
			
		||||
    let format = match args.get("format") {
 | 
			
		||||
        Some(val) => match val.as_str() {
 | 
			
		||||
            Some(s) => s,
 | 
			
		||||
            None => "%H:%M",
 | 
			
		||||
        },
 | 
			
		||||
        None => "%H:%M",
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    match value.as_str() {
 | 
			
		||||
        Some(datetime_str) => {
 | 
			
		||||
            // Try to parse as RFC3339 first
 | 
			
		||||
            if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
 | 
			
		||||
                Ok(Value::String(dt.format(format).to_string()))
 | 
			
		||||
            } else {
 | 
			
		||||
                // Try to parse as our standard format
 | 
			
		||||
                match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
 | 
			
		||||
                    Ok(dt) => Ok(Value::String(dt.format(format).to_string())),
 | 
			
		||||
                    Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        None => Err(tera::Error::msg("Value must be a string")),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Formats a date for display
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,129 +4,633 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <h1>Calendar</h1>
 | 
			
		||||
    
 | 
			
		||||
    <p>View Mode: {{ view_mode }}</p>
 | 
			
		||||
    <p>Current Date: {{ current_date }}</p>
 | 
			
		||||
    
 | 
			
		||||
    <div class="d-flex justify-content-between align-items-center mb-3">
 | 
			
		||||
        <div class="btn-group">
 | 
			
		||||
            <a href="/calendar?view=day" class="btn btn-outline-primary">Day</a>
 | 
			
		||||
            <a href="/calendar?view=month" class="btn btn-outline-primary">Month</a>
 | 
			
		||||
            <a href="/calendar?view=year" class="btn btn-outline-primary">Year</a>
 | 
			
		||||
    <!-- Calendar Page Header -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <h1 class="h3 mb-1">Calendar</h1>
 | 
			
		||||
                    <p class="text-muted mb-0">Manage your events and schedule</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <button type="button" class="btn btn-primary" data-bs-toggle="modal"
 | 
			
		||||
                        data-bs-target="#newEventModal">
 | 
			
		||||
                        <i class="bi bi-plus-circle"></i> Create Event
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a href="/calendar/new" class="btn btn-success">
 | 
			
		||||
            <i class="bi bi-plus-circle"></i> Create New Event
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    <!-- Calendar Navigation -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <div class="btn-group" role="group" aria-label="Calendar view modes">
 | 
			
		||||
                    <button id="todayBtn" onclick="goToToday()" class="btn btn-outline-success">
 | 
			
		||||
                        <i class="bi bi-calendar-check"></i> Today
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <a href="/calendar?view=day"
 | 
			
		||||
                        class="btn {% if view_mode == 'day' %}btn-primary{% else %}btn-outline-primary{% endif %}">
 | 
			
		||||
                        <i class="bi bi-calendar-day"></i> Day
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a href="/calendar?view=month"
 | 
			
		||||
                        class="btn {% if view_mode == 'month' %}btn-primary{% else %}btn-outline-primary{% endif %}">
 | 
			
		||||
                        <i class="bi bi-calendar3"></i> Month
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a href="/calendar?view=year"
 | 
			
		||||
                        class="btn {% if view_mode == 'year' %}btn-primary{% else %}btn-outline-primary{% endif %}">
 | 
			
		||||
                        <i class="bi bi-calendar4-range"></i> Year
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="text-muted">
 | 
			
		||||
                    <i class="bi bi-calendar-event"></i> {{ current_date }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if view_mode == "month" %}
 | 
			
		||||
            <div class="text-center month-nav-hint">
 | 
			
		||||
                <small class="text-muted">
 | 
			
		||||
                    <i class="bi bi-arrow-left"></i> <i class="bi bi-arrow-right"></i> Use arrow keys to navigate months
 | 
			
		||||
                    | Click on any day to create an event
 | 
			
		||||
                </small>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if view_mode == "month" %}
 | 
			
		||||
        <h2>Month View: {{ month_name }} {{ current_year }}</h2>
 | 
			
		||||
        
 | 
			
		||||
        <table class="table table-bordered">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th>Sun</th>
 | 
			
		||||
                    <th>Mon</th>
 | 
			
		||||
                    <th>Tue</th>
 | 
			
		||||
                    <th>Wed</th>
 | 
			
		||||
                    <th>Thu</th>
 | 
			
		||||
                    <th>Fri</th>
 | 
			
		||||
                    <th>Sat</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
                {% for week in range(start=0, end=6) %}
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        {% for day_idx in range(start=0, end=7) %}
 | 
			
		||||
                            <td>
 | 
			
		||||
                                {% set idx = week * 7 + day_idx %}
 | 
			
		||||
                                {% if idx < calendar_days|length %}
 | 
			
		||||
                                    {% set day = calendar_days[idx] %}
 | 
			
		||||
                                    {% if day.day > 0 %}
 | 
			
		||||
                                        {{ day.day }}
 | 
			
		||||
    <!-- Month View -->
 | 
			
		||||
    <div class="card">
 | 
			
		||||
        <div class="card-header bg-primary text-white">
 | 
			
		||||
            <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
                <button class="btn btn-outline-light btn-sm" onclick="navigateMonth(-1)">
 | 
			
		||||
                    <i class="bi bi-chevron-left"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
                <h4 class="mb-0">
 | 
			
		||||
                    <i class="bi bi-calendar3"></i> {{ month_name }} {{ current_year }}
 | 
			
		||||
                </h4>
 | 
			
		||||
                <button class="btn btn-outline-light btn-sm" onclick="navigateMonth(1)">
 | 
			
		||||
                    <i class="bi bi-chevron-right"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body p-0">
 | 
			
		||||
            <div class="table-responsive">
 | 
			
		||||
                <table class="table table-bordered mb-0 calendar-table">
 | 
			
		||||
                    <thead class="table-light">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th class="text-center py-3">Sunday</th>
 | 
			
		||||
                            <th class="text-center py-3">Monday</th>
 | 
			
		||||
                            <th class="text-center py-3">Tuesday</th>
 | 
			
		||||
                            <th class="text-center py-3">Wednesday</th>
 | 
			
		||||
                            <th class="text-center py-3">Thursday</th>
 | 
			
		||||
                            <th class="text-center py-3">Friday</th>
 | 
			
		||||
                            <th class="text-center py-3">Saturday</th>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                        {% for week in range(start=0, end=6) %}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            {% for day_idx in range(start=0, end=7) %}
 | 
			
		||||
                            {% set idx = week * 7 + day_idx %}
 | 
			
		||||
                            <td class="calendar-day" onclick="openEventModalForDate(this)" {% if idx <
 | 
			
		||||
                                calendar_days|length %} data-day="{{ calendar_days[idx].day }}"
 | 
			
		||||
                                data-is-current-month="{{ calendar_days[idx].is_current_month }}"
 | 
			
		||||
                                data-date="{{ current_year }}-{{ current_month|format_hour }}-{{ calendar_days[idx].day|format_hour }}"
 | 
			
		||||
                                {% else %} data-day="0" data-is-current-month="false" data-date="" {% endif %}>
 | 
			
		||||
                                {% if idx < calendar_days|length %} {% set day=calendar_days[idx] %} {% if day.day> 0 %}
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        class="calendar-day-header d-flex justify-content-between align-items-center mb-2">
 | 
			
		||||
                                        <span
 | 
			
		||||
                                            class="calendar-day-number {% if day.is_current_month %}text-dark{% else %}text-muted{% endif %}">
 | 
			
		||||
                                            {{ day.day }}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                        {% if day.events|length > 0 %}
 | 
			
		||||
                                        <small class="badge bg-primary">{{ day.events|length }}</small>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    {% if day.events|length > 0 %}
 | 
			
		||||
                                    <div class="calendar-events">
 | 
			
		||||
                                        {% for event in day.events %}
 | 
			
		||||
                                        {% if loop.index <= 2 %} <div class="event-preview text-truncate mb-1"
 | 
			
		||||
                                            style="background-color: {{ event.color }}; color: white;">
 | 
			
		||||
                                            {{ event.title }}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </td>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                                    {% endfor %}
 | 
			
		||||
                                    {% if day.events|length > 2 %}
 | 
			
		||||
                                    <div class="small text-muted text-center">+{{ day.events|length - 2 }} more</div>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
            </tr>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    {% elif view_mode == "year" %}
 | 
			
		||||
        <h2>Year View: {{ current_year }}</h2>
 | 
			
		||||
        
 | 
			
		||||
            </table>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% elif view_mode == "year" %}
 | 
			
		||||
<!-- Year View -->
 | 
			
		||||
<div class="card">
 | 
			
		||||
    <div class="card-header bg-primary text-white">
 | 
			
		||||
        <h4 class="mb-0">
 | 
			
		||||
            <i class="bi bi-calendar4-range"></i> Year {{ current_year }}
 | 
			
		||||
        </h4>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            {% for month in months %}
 | 
			
		||||
                <div class="col-md-4 mb-3">
 | 
			
		||||
                    <div class="card">
 | 
			
		||||
                        <div class="card-header">{{ month.name }}</div>
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <p>Events: {{ month.events|length }}</p>
 | 
			
		||||
            <div class="col-lg-3 col-md-4 col-sm-6 mb-4">
 | 
			
		||||
                <div class="card h-100 shadow-sm">
 | 
			
		||||
                    <div class="card-header bg-light">
 | 
			
		||||
                        <h6 class="mb-0 text-center">{{ month.name }}</h6>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="card-body text-center">
 | 
			
		||||
                        {% if month.events|length > 0 %}
 | 
			
		||||
                        <div class="mb-2">
 | 
			
		||||
                            <span class="badge bg-primary fs-6">{{ month.events|length }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <p class="text-muted small mb-0">
 | 
			
		||||
                            {% if month.events|length == 1 %}
 | 
			
		||||
                            1 event
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            {{ month.events|length }} events
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <div class="mt-2">
 | 
			
		||||
                            <a href="/calendar?view=month" class="btn btn-sm btn-outline-primary">
 | 
			
		||||
                                View Month
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <div class="text-muted">
 | 
			
		||||
                            <i class="bi bi-calendar-x fs-4 mb-2 d-block"></i>
 | 
			
		||||
                            <p class="small mb-0">No events</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </div>
 | 
			
		||||
    {% elif view_mode == "day" %}
 | 
			
		||||
        <h2>Day View: {{ current_date }}</h2>
 | 
			
		||||
        
 | 
			
		||||
        <div class="card mb-4">
 | 
			
		||||
            <div class="card-header bg-primary text-white">
 | 
			
		||||
                All Day Events
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% elif view_mode == "day" %}
 | 
			
		||||
<h2>Day View: {{ current_date }}</h2>
 | 
			
		||||
 | 
			
		||||
<div class="card mb-4">
 | 
			
		||||
    <div class="card-header bg-primary text-white">
 | 
			
		||||
        All Day Events
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
        {% if events is defined and events|length > 0 %}
 | 
			
		||||
        {% for event in events %}
 | 
			
		||||
        {% if event.all_day %}
 | 
			
		||||
        <div class="alert" style="background-color: {{ event.color }}; color: white;">
 | 
			
		||||
            <h5>{{ event.title }}</h5>
 | 
			
		||||
            <p>{{ event.description }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <p class="text-muted">No all-day events</p>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="list-group">
 | 
			
		||||
    {% for hour in range(start=0, end=24) %}
 | 
			
		||||
    <div class="list-group-item">
 | 
			
		||||
        <div class="d-flex">
 | 
			
		||||
            <div class="pe-3" style="width: 60px; text-align: right;">
 | 
			
		||||
                <strong>{{ hour|format_hour }}:00</strong>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
            <div class="flex-grow-1">
 | 
			
		||||
                {% if events is defined and events|length > 0 %}
 | 
			
		||||
                    {% for event in events %}
 | 
			
		||||
                        {% if event.all_day %}
 | 
			
		||||
                            <div class="alert" style="background-color: {{ event.color }}; color: white;">
 | 
			
		||||
                                <h5>{{ event.title }}</h5>
 | 
			
		||||
                                <p>{{ event.description }}</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    <p class="text-muted">No all-day events</p>
 | 
			
		||||
                {% for event in events %}
 | 
			
		||||
                {% if not event.all_day %}
 | 
			
		||||
                {% set start_hour = event.start_time|extract_hour %}
 | 
			
		||||
                {% if start_hour == hour|string %}
 | 
			
		||||
                <div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
 | 
			
		||||
                    <h5>{{ event.title }}</h5>
 | 
			
		||||
                    <p>{{ event.start_time|format_time }} - {{ event.end_time|format_time }}</p>
 | 
			
		||||
                    <p>{{ event.description }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="list-group">
 | 
			
		||||
            {% for hour in range(start=0, end=24) %}
 | 
			
		||||
                <div class="list-group-item">
 | 
			
		||||
                    <div class="d-flex">
 | 
			
		||||
                        <div class="pe-3" style="width: 60px; text-align: right;">
 | 
			
		||||
                            <strong>{{ "%02d"|format(value=hour) }}:00</strong>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<!-- New Event Modal -->
 | 
			
		||||
<div class="modal fade" id="newEventModal" tabindex="-1" aria-labelledby="newEventModalLabel" aria-hidden="true">
 | 
			
		||||
    <div class="modal-dialog modal-lg">
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
            <div class="modal-header bg-primary text-white">
 | 
			
		||||
                <h5 class="modal-title" id="newEventModalLabel">
 | 
			
		||||
                    <i class="bi bi-plus-circle"></i> Create New Event
 | 
			
		||||
                </h5>
 | 
			
		||||
                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
 | 
			
		||||
                    aria-label="Close"></button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <form id="newEventForm" action="/calendar/events" method="post">
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                    <div class="row">
 | 
			
		||||
                        <div class="col-md-8">
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="eventTitle" class="form-label">Event Title *</label>
 | 
			
		||||
                                <input type="text" class="form-control" id="eventTitle" name="title" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="flex-grow-1">
 | 
			
		||||
                            {% if events is defined and events|length > 0 %}
 | 
			
		||||
                                {% for event in events %}
 | 
			
		||||
                                    {% if not event.all_day %}
 | 
			
		||||
                                        {% set start_hour = event.start_time|date(format="%H") %}
 | 
			
		||||
                                        {% if start_hour == hour|string %}
 | 
			
		||||
                                            <div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
 | 
			
		||||
                                                <h5>{{ event.title }}</h5>
 | 
			
		||||
                                                <p>{{ event.start_time|date(format="%H:%M") }} - {{ event.end_time|date(format="%H:%M") }}</p>
 | 
			
		||||
                                                <p>{{ event.description }}</p>
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        {% endif %}
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        <div class="col-md-4">
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="eventColor" class="form-label">Color</label>
 | 
			
		||||
                                <select class="form-select" id="eventColor" name="color">
 | 
			
		||||
                                    <option value="#4285F4" selected>Blue</option>
 | 
			
		||||
                                    <option value="#EA4335">Red</option>
 | 
			
		||||
                                    <option value="#34A853">Green</option>
 | 
			
		||||
                                    <option value="#FBBC05">Yellow</option>
 | 
			
		||||
                                    <option value="#A142F4">Purple</option>
 | 
			
		||||
                                    <option value="#24C1E0">Cyan</option>
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <label for="eventDescription" class="form-label">Description</label>
 | 
			
		||||
                        <textarea class="form-control" id="eventDescription" name="description" rows="3"></textarea>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="mb-3">
 | 
			
		||||
                        <div class="form-check">
 | 
			
		||||
                            <input type="checkbox" class="form-check-input" id="allDayEvent" name="all_day">
 | 
			
		||||
                            <label class="form-check-label" for="allDayEvent">All Day Event</label>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="row" id="timeInputs">
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="startTime" class="form-label">Start Time *</label>
 | 
			
		||||
                                <input type="datetime-local" class="form-control" id="startTime" name="start_time"
 | 
			
		||||
                                    required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="col-md-6">
 | 
			
		||||
                            <div class="mb-3">
 | 
			
		||||
                                <label for="endTime" class="form-label">End Time *</label>
 | 
			
		||||
                                <input type="datetime-local" class="form-control" id="endTime" name="end_time" required>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
 | 
			
		||||
                    <button type="submit" class="btn btn-primary">
 | 
			
		||||
                        <i class="bi bi-check-circle"></i> Create Event
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    
 | 
			
		||||
    <!-- Floating Action Button (FAB) for creating new events -->
 | 
			
		||||
    <a href="/calendar/new" class="position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow" style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
 | 
			
		||||
        <i class="bi bi-plus"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<!-- Floating Action Button (FAB) for mobile -->
 | 
			
		||||
<button type="button" class="d-md-none position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow"
 | 
			
		||||
    data-bs-toggle="modal" data-bs-target="#newEventModal"
 | 
			
		||||
    style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
 | 
			
		||||
    <i class="bi bi-plus"></i>
 | 
			
		||||
</button>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% block extra_css %}
 | 
			
		||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
 | 
			
		||||
<style>
 | 
			
		||||
    .calendar-table {
 | 
			
		||||
        border: 2px solid #dee2e6;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-table td {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        transition: background-color 0.2s ease;
 | 
			
		||||
        border: 1px solid #dee2e6 !important;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        vertical-align: top;
 | 
			
		||||
        position: relative;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-table td:hover {
 | 
			
		||||
        background-color: #e3f2fd;
 | 
			
		||||
        border-color: #2196f3 !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day {
 | 
			
		||||
        min-height: 120px;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        padding: 30px !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day-header {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        margin-bottom: 8px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day-number {
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        font-size: 1.1rem;
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day[data-is-current-month="true"]:hover::after {
 | 
			
		||||
        content: "Click to add event";
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        bottom: 5px;
 | 
			
		||||
        left: 50%;
 | 
			
		||||
        transform: translateX(-50%);
 | 
			
		||||
        background: rgba(33, 150, 243, 0.9);
 | 
			
		||||
        color: white;
 | 
			
		||||
        padding: 2px 6px;
 | 
			
		||||
        border-radius: 3px;
 | 
			
		||||
        font-size: 0.7rem;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        z-index: 10;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day[data-is-current-month="false"] {
 | 
			
		||||
        background-color: #f8f9fa;
 | 
			
		||||
        color: #6c757d;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day.today {
 | 
			
		||||
        background-color: #e3f2fd;
 | 
			
		||||
        border: 2px solid #2196f3 !important;
 | 
			
		||||
        box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-day.today .calendar-day-number {
 | 
			
		||||
        color: #1976d2;
 | 
			
		||||
        font-weight: 900;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .calendar-events {
 | 
			
		||||
        text-align: left;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .event-preview {
 | 
			
		||||
        font-size: 0.7rem;
 | 
			
		||||
        padding: 2px 4px;
 | 
			
		||||
        margin: 1px 0;
 | 
			
		||||
        border-radius: 2px;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        transition: opacity 0.2s;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .event-preview:hover {
 | 
			
		||||
        opacity: 0.8;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Month navigation buttons */
 | 
			
		||||
    .btn-outline-light:hover {
 | 
			
		||||
        background-color: rgba(255, 255, 255, 0.2);
 | 
			
		||||
        border-color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Keyboard navigation hint */
 | 
			
		||||
    .month-nav-hint {
 | 
			
		||||
        font-size: 0.8rem;
 | 
			
		||||
        opacity: 0.7;
 | 
			
		||||
        margin-top: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extra_js %}
 | 
			
		||||
<script>
 | 
			
		||||
    // Handle all-day event checkbox
 | 
			
		||||
    document.getElementById('allDayEvent').addEventListener('change', function () {
 | 
			
		||||
        const timeInputs = document.getElementById('timeInputs');
 | 
			
		||||
        const startTime = document.getElementById('startTime');
 | 
			
		||||
        const endTime = document.getElementById('endTime');
 | 
			
		||||
 | 
			
		||||
        if (this.checked) {
 | 
			
		||||
            timeInputs.style.display = 'none';
 | 
			
		||||
            startTime.removeAttribute('required');
 | 
			
		||||
            endTime.removeAttribute('required');
 | 
			
		||||
        } else {
 | 
			
		||||
            timeInputs.style.display = 'block';
 | 
			
		||||
            startTime.setAttribute('required', '');
 | 
			
		||||
            endTime.setAttribute('required', '');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Handle form submission
 | 
			
		||||
    document.getElementById('newEventForm').addEventListener('submit', function (e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const formData = new FormData(this);
 | 
			
		||||
        const allDay = document.getElementById('allDayEvent').checked;
 | 
			
		||||
 | 
			
		||||
        if (!allDay) {
 | 
			
		||||
            // Convert datetime-local to RFC3339 format
 | 
			
		||||
            const startTime = document.getElementById('startTime').value;
 | 
			
		||||
            const endTime = document.getElementById('endTime').value;
 | 
			
		||||
 | 
			
		||||
            if (startTime) {
 | 
			
		||||
                formData.set('start_time', new Date(startTime).toISOString());
 | 
			
		||||
            }
 | 
			
		||||
            if (endTime) {
 | 
			
		||||
                formData.set('end_time', new Date(endTime).toISOString());
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // For all-day events, set times to start and end of day
 | 
			
		||||
            const today = new Date();
 | 
			
		||||
            const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
 | 
			
		||||
            const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59);
 | 
			
		||||
 | 
			
		||||
            formData.set('start_time', startOfDay.toISOString());
 | 
			
		||||
            formData.set('end_time', endOfDay.toISOString());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Submit the form
 | 
			
		||||
        fetch(this.action, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: formData
 | 
			
		||||
        }).then(response => {
 | 
			
		||||
            if (response.ok) {
 | 
			
		||||
                window.location.reload();
 | 
			
		||||
            } else {
 | 
			
		||||
                alert('Error creating event. Please try again.');
 | 
			
		||||
            }
 | 
			
		||||
        }).catch(error => {
 | 
			
		||||
            console.error('Error:', error);
 | 
			
		||||
            alert('Error creating event. Please try again.');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Delete event function
 | 
			
		||||
    function deleteEvent(eventId) {
 | 
			
		||||
        if (confirm('Are you sure you want to delete this event?')) {
 | 
			
		||||
            fetch(`/calendar/events/${eventId}/delete`, {
 | 
			
		||||
                method: 'POST'
 | 
			
		||||
            }).then(response => {
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    window.location.reload();
 | 
			
		||||
                } else {
 | 
			
		||||
                    alert('Error deleting event. Please try again.');
 | 
			
		||||
                }
 | 
			
		||||
            }).catch(error => {
 | 
			
		||||
                console.error('Error:', error);
 | 
			
		||||
                alert('Error deleting event. Please try again.');
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Navigate to today
 | 
			
		||||
    function goToToday() {
 | 
			
		||||
        const today = new Date().toISOString().split('T')[0];
 | 
			
		||||
        window.location.href = `/calendar?view=month&date=${today}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Navigate between months
 | 
			
		||||
    function navigateMonth(direction) {
 | 
			
		||||
        const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
        const currentDate = urlParams.get('date') || new Date().toISOString().split('T')[0];
 | 
			
		||||
        const date = new Date(currentDate);
 | 
			
		||||
 | 
			
		||||
        // Add or subtract months
 | 
			
		||||
        date.setMonth(date.getMonth() + direction);
 | 
			
		||||
 | 
			
		||||
        // Format the new date
 | 
			
		||||
        const newDate = date.toISOString().split('T')[0];
 | 
			
		||||
 | 
			
		||||
        // Navigate to the new month
 | 
			
		||||
        window.location.href = `/calendar?view=month&date=${newDate}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Open event modal for specific date
 | 
			
		||||
    function openEventModalForDate(dayCell) {
 | 
			
		||||
        const day = dayCell.getAttribute('data-day');
 | 
			
		||||
        const isCurrentMonth = dayCell.getAttribute('data-is-current-month') === 'true';
 | 
			
		||||
 | 
			
		||||
        // Only proceed if it's a valid day in the current month
 | 
			
		||||
        if (day > 0 && isCurrentMonth) {
 | 
			
		||||
            // Get current month and year from URL or current date
 | 
			
		||||
            const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
            const currentDate = urlParams.get('date') || new Date().toISOString().split('T')[0];
 | 
			
		||||
            const date = new Date(currentDate);
 | 
			
		||||
 | 
			
		||||
            // Set the date for the selected day
 | 
			
		||||
            const selectedDate = new Date(date.getFullYear(), date.getMonth(), parseInt(day));
 | 
			
		||||
 | 
			
		||||
            // Set default times for the selected date
 | 
			
		||||
            const startTime = new Date(selectedDate);
 | 
			
		||||
            startTime.setHours(9, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
            const endTime = new Date(selectedDate);
 | 
			
		||||
            endTime.setHours(10, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
            // Update the modal form with the selected date
 | 
			
		||||
            document.getElementById('startTime').value = startTime.toISOString().slice(0, 16);
 | 
			
		||||
            document.getElementById('endTime').value = endTime.toISOString().slice(0, 16);
 | 
			
		||||
 | 
			
		||||
            // Update modal title to show the selected date
 | 
			
		||||
            const modalTitle = document.getElementById('newEventModalLabel');
 | 
			
		||||
            const dateStr = selectedDate.toLocaleDateString('en-US', {
 | 
			
		||||
                weekday: 'long',
 | 
			
		||||
                year: 'numeric',
 | 
			
		||||
                month: 'long',
 | 
			
		||||
                day: 'numeric'
 | 
			
		||||
            });
 | 
			
		||||
            modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create Event for ${dateStr}`;
 | 
			
		||||
 | 
			
		||||
            // Show the modal
 | 
			
		||||
            const modal = new bootstrap.Modal(document.getElementById('newEventModal'));
 | 
			
		||||
            modal.show();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Initialize calendar features
 | 
			
		||||
    function initializeCalendar() {
 | 
			
		||||
        // Highlight today's date
 | 
			
		||||
        const today = new Date();
 | 
			
		||||
        const todayStr = today.toISOString().split('T')[0];
 | 
			
		||||
 | 
			
		||||
        // Find and highlight today's cell
 | 
			
		||||
        const calendarCells = document.querySelectorAll('.calendar-day[data-date]');
 | 
			
		||||
        calendarCells.forEach(cell => {
 | 
			
		||||
            const cellDate = cell.getAttribute('data-date');
 | 
			
		||||
            if (cellDate === todayStr && cell.getAttribute('data-is-current-month') === 'true') {
 | 
			
		||||
                cell.classList.add('today');
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Manage Today button state
 | 
			
		||||
        const todayBtn = document.getElementById('todayBtn');
 | 
			
		||||
        if (todayBtn) {
 | 
			
		||||
            const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
            const currentDate = urlParams.get('date') || today.toISOString().split('T')[0];
 | 
			
		||||
            const currentMonth = new Date(currentDate).getMonth();
 | 
			
		||||
            const currentYear = new Date(currentDate).getFullYear();
 | 
			
		||||
            const todayMonth = today.getMonth();
 | 
			
		||||
            const todayYear = today.getFullYear();
 | 
			
		||||
 | 
			
		||||
            // Disable Today button if we're already viewing the current month
 | 
			
		||||
            if (currentMonth === todayMonth && currentYear === todayYear) {
 | 
			
		||||
                todayBtn.disabled = true;
 | 
			
		||||
                todayBtn.classList.remove('btn-outline-success');
 | 
			
		||||
                todayBtn.classList.add('btn-secondary');
 | 
			
		||||
                todayBtn.title = 'Already viewing current month';
 | 
			
		||||
            } else {
 | 
			
		||||
                todayBtn.disabled = false;
 | 
			
		||||
                todayBtn.classList.remove('btn-secondary');
 | 
			
		||||
                todayBtn.classList.add('btn-outline-success');
 | 
			
		||||
                todayBtn.title = 'Go to current month';
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set default date/time for new events
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        const tomorrow = new Date(now);
 | 
			
		||||
        tomorrow.setDate(tomorrow.getDate() + 1);
 | 
			
		||||
        tomorrow.setHours(9, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
        const tomorrowEnd = new Date(tomorrow);
 | 
			
		||||
        tomorrowEnd.setHours(10, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
        document.getElementById('startTime').value = tomorrow.toISOString().slice(0, 16);
 | 
			
		||||
        document.getElementById('endTime').value = tomorrowEnd.toISOString().slice(0, 16);
 | 
			
		||||
 | 
			
		||||
        // Initialize calendar features
 | 
			
		||||
        initializeCalendar();
 | 
			
		||||
 | 
			
		||||
        // Add keyboard navigation for month view
 | 
			
		||||
        document.addEventListener('keydown', function (e) {
 | 
			
		||||
            if (window.location.search.includes('view=month') || (!window.location.search.includes('view=') && window.location.pathname === '/calendar')) {
 | 
			
		||||
                if (e.key === 'ArrowLeft') {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    navigateMonth(-1);
 | 
			
		||||
                } else if (e.key === 'ArrowRight') {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    navigateMonth(1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user