Allow celendar event editing

This commit is contained in:
Chris Jean-Marie 2025-06-20 14:48:37 +00:00
parent fe8056179b
commit d156cbfa98
4 changed files with 202 additions and 14 deletions

View File

@ -4,15 +4,18 @@ use axum::{
response::{Html, IntoResponse, Redirect, Response}, response::{Html, IntoResponse, Redirect, Response},
Extension, Form, Extension, Form,
}; };
use chrono::Days; use chrono::{Days, TimeDelta};
use http::StatusCode; use http::StatusCode;
use rbac::RbacService;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row}; use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row};
use uuid::Uuid; use uuid::Uuid;
use rbac::RbacService;
use crate::{ use crate::{
middlewares::is_authorized, rbac, user::{get_user_roles_display, AccountData} middlewares::is_authorized,
rbac,
user::{get_user_roles_display, AccountData},
}; };
struct HtmlTemplate<T>(T); struct HtmlTemplate<T>(T);
@ -116,7 +119,7 @@ async fn get_event(event_id: Uuid, db_pool: &PgPool) -> String {
// Set default events // Set default events
let mut eventstring: String = "[]".to_string(); let mut eventstring: String = "[]".to_string();
let event: Result<PgRow, Error> = Ok(sqlx::query( let event: Result<PgRow, Error> = Ok(sqlx::query(
r#"select to_json(json_build_object( r#"select to_json(json_build_object(
'title', ce.title, 'title', ce.title,
'start', ce.start_time, 'start', ce.start_time,
@ -162,6 +165,10 @@ pub async fn get_events(
// Extract the user data. // Extract the user data.
let _user = user_data.as_ref().unwrap().clone(); let _user = user_data.as_ref().unwrap().clone();
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
let personid = user_data
.as_ref()
.map(|s| s.person_id.clone())
.unwrap_or_default();
// Set empty query string // Set empty query string
let mut query = r#""#; let mut query = r#""#;
@ -172,6 +179,7 @@ pub async fn get_events(
} else { } else {
query = r#"select to_json(json_agg(jbo.val)) query = r#"select to_json(json_agg(jbo.val))
from (select json_build_object( from (select json_build_object(
'id', ce.id,
'title', ce.title, 'title', ce.title,
'start', ce.start_time, 'start', ce.start_time,
'end', ce.end_time, 'end', ce.end_time,
@ -187,6 +195,7 @@ pub async fn get_events(
and ce.created_by = $3 and ce.created_by = $3
union all union all
select json_build_object( select json_build_object(
'id', ce.id,
'title', 'In use', 'title', 'In use',
'start', ce.start_time, 'start', ce.start_time,
'end', ce.end_time, 'end', ce.end_time,
@ -203,6 +212,7 @@ pub async fn get_events(
} }
} else { } else {
query = r#"select to_json(json_agg(json_build_object( query = r#"select to_json(json_agg(json_build_object(
'id', ce.id,
'title', ce.title, 'title', ce.title,
'start', ce.start_time, 'start', ce.start_time,
'end', ce.end_time, 'end', ce.end_time,
@ -218,6 +228,7 @@ pub async fn get_events(
} }
} else { } else {
query = r#"select to_json(json_agg(json_build_object( query = r#"select to_json(json_agg(json_build_object(
'id', ce.id,
'title', ce.title, 'title', ce.title,
'start', ce.start_time, 'start', ce.start_time,
'end', ce.end_time, 'end', ce.end_time,
@ -237,12 +248,12 @@ pub async fn get_events(
//println!("User is authorized"); //println!("User is authorized");
// Get requested calendar events from database // Get requested calendar events from database
let events = sqlx::query(query,) let events = sqlx::query(query)
.bind(chrono::DateTime::parse_from_rfc3339(&params.start).unwrap()) .bind(chrono::DateTime::parse_from_rfc3339(&params.start).unwrap())
.bind(chrono::DateTime::parse_from_rfc3339(&params.end).unwrap()) .bind(chrono::DateTime::parse_from_rfc3339(&params.end).unwrap())
.bind(userid) .bind(personid)
.fetch_one(&db_pool) .fetch_one(&db_pool)
.await; .await;
//println!("Events: {:?}", events); //println!("Events: {:?}", events);
@ -297,7 +308,10 @@ pub async fn create_event(
if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await { if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await {
let fmt = "%Y-%m-%d"; let fmt = "%Y-%m-%d";
let start_date = chrono::NaiveDate::parse_from_str(&event.start_time, fmt).unwrap(); let start_date = chrono::NaiveDate::parse_from_str(&event.start_time, fmt).unwrap();
let end_date = chrono::NaiveDate::parse_from_str(&event.end_time, fmt).unwrap().checked_sub_days(Days::new(1)).unwrap(); let end_date = chrono::NaiveDate::parse_from_str(&event.end_time, fmt)
.unwrap()
.checked_sub_days(Days::new(1))
.unwrap();
let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap(); let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap(); let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
@ -378,7 +392,10 @@ pub async fn new_request(
let fmt = "%Y-%m-%d"; let fmt = "%Y-%m-%d";
let start_date = chrono::NaiveDate::parse_from_str(&params.start, fmt).unwrap(); let start_date = chrono::NaiveDate::parse_from_str(&params.start, fmt).unwrap();
let end_date = chrono::NaiveDate::parse_from_str(&params.end, fmt).unwrap().checked_sub_days(Days::new(1)).unwrap(); let end_date = chrono::NaiveDate::parse_from_str(&params.end, fmt)
.unwrap()
.checked_sub_days(Days::new(1))
.unwrap();
let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap(); let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap(); let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
@ -423,6 +440,91 @@ pub async fn new_request(
eventstring eventstring
} }
pub async fn update_event(
State(db_pool): State<PgPool>,
Extension(user_data): Extension<Option<AccountData>>,
Extension(rbac): Extension<RbacService>,
request: axum::http::Request<axum::body::Body>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
// Set default events
let mut eventstring: String = "[]".to_string();
if logged_in {
// Extract the user data.
let _user = user_data.as_ref().unwrap().clone();
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
let personid = user_data
.as_ref()
.map(|s| s.person_id.clone())
.unwrap_or_default();
if rbac.has_permission(userid, "calendar:*:*").await {
let (_parts, body) = request.into_parts();
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
let body_str = String::from_utf8(bytes.to_vec()).unwrap();
// println!("Body: {}", body_str);
let v: Value = serde_json::from_str(&body_str).unwrap();
let fmt = "%Y-%m-%d";
let start_date = chrono::NaiveDate::parse_from_str(v["start"].as_str().unwrap(), fmt).unwrap();
let end_date = chrono::NaiveDate::parse_from_str(v["end"].as_str().unwrap(), fmt).unwrap();
let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
// Convert calendar id to UUID
let calendar_event_id = Uuid::parse_str(v["id"].as_str().unwrap()).unwrap();
// Display values to be updated
// println!("person : {}", personid);
// println!("title : {}", v["title"]);
// println!("start : {:#?}", start_datetime);
// println!("end : {:#?}", end_datetime);
let event = sqlx::query_scalar::<_, uuid::Uuid>(
r#"with cet as (select id from calendar_event_types where name = 'Reservation' and state = 'Requested')
update calendar_events
set updated_by = $1,
updated_at = now(),
event_type_id = cet.id,
title = $2,
start_time = $3,
end_time = $4
from cet
where calendar_events.id = $5
returning calendar_events.id"#
)
.bind(personid)
.bind(v["title"].as_str().unwrap())
.bind(start_datetime)
.bind(end_datetime)
.bind(calendar_event_id)
.fetch_one(&db_pool)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error creating event: {}", e),
)
});
let event_id = event.clone();
// println!("Event: {:#?}", event);
eventstring = get_event(event_id.unwrap(), &db_pool).await;
// println!("{:#?}", eventstring);
}
}
eventstring
}
#[derive(Template)] #[derive(Template)]
#[template(path = "newevent.html")] #[template(path = "newevent.html")]
struct EventTemplate { struct EventTemplate {

View File

@ -34,6 +34,8 @@ use wishlist::{
user_wishlist_returned_item, user_wishlist_save_item, wishlists, user_wishlist_returned_item, user_wishlist_save_item, wishlists,
}; };
use crate::calendar::update_event;
//use email::send_emails; //use email::send_emails;
#[derive(Clone)] #[derive(Clone)]
@ -95,6 +97,7 @@ async fn main() {
.route("/calendar/createevent", post(create_event)) .route("/calendar/createevent", post(create_event))
.route("/calendar/newevent", get(new_event)) .route("/calendar/newevent", get(new_event))
.route("/calendar/newrequest", post(new_request)) .route("/calendar/newrequest", post(new_request))
.route("/calendar/updaterequest", post(update_event))
// Wishlist // Wishlist
.route("/wishlists", get(wishlists)) .route("/wishlists", get(wishlists))
.route("/userwishlist/{user_id}", get(user_wishlist)) .route("/userwishlist/{user_id}", get(user_wishlist))

View File

@ -43,5 +43,5 @@ fn permission_matches(pattern: &str, resource: &str) -> bool {
pattern_segments.iter() pattern_segments.iter()
.zip(resource_segments.iter()) .zip(resource_segments.iter())
.all(|(p, r)| p == r || *p == "*") .all(|(p, r)| p == r || *p == "*" || *r == "*")
} }

View File

@ -8,7 +8,7 @@
<div id="calendar" class="fc"></div> <div id="calendar" class="fc"></div>
</div> </div>
<!-- Modal --> <!-- Create Event Modal -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1" role="dialog" aria-labelledby="eventDetailsModalTitle" <div class="modal fade" id="eventDetailsModal" tabindex="-1" role="dialog" aria-labelledby="eventDetailsModalTitle"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
@ -37,6 +37,30 @@
</div> </div>
</div> </div>
<!-- Edit Event Modal -->
<div class="modal fade" id="eventEditModal" tabindex="-1" role="dialog" aria-labelledby="eventEditModalTitle"
aria-hidden="true">
<div class="modal-dialog">
<form id="eventForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Event</h5>
</div>
<div class="modal-body">
<input type="text" id="eventEditTitle" class="form-control" placeholder="Event Title">
<input type="date" id="eventEditStart" class="form-control" placeholder="Start">
<input type="date" id="eventEditEnd" class="form-control" placeholder="End">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="closeEventEdit"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="saveEvent">Save</button>
</div>
</div>
</form>
</div>
</div>
{% endblock center %} {% endblock center %}
{% block scripts %} {% block scripts %}
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.17/index.global.min.js'></script> <script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.17/index.global.min.js'></script>
@ -63,6 +87,16 @@
$('#eventStart').val(info.startStr); $('#eventStart').val(info.startStr);
$('#eventEnd').val(info.endStr); $('#eventEnd').val(info.endStr);
$('#eventDetailsModal').modal('show'); $('#eventDetailsModal').modal('show');
},
eventClick: function (info) {
if (info.event.title != "In use") {
$('#eventEditTitle').val(info.event.title);
$('#eventEditStart').val(moment(info.event.start).format('YYYY-MM-DD'));
$('#eventEditEnd').val(moment(info.event.end).format('YYYY-MM-DD'));
$('#eventEditModal').modal('show');
// Store the event for later update
window.calEvent = info.event;
}
} }
}); });
@ -110,6 +144,55 @@
document.getElementById('eventDetailsModalClose').addEventListener('click', function () { document.getElementById('eventDetailsModalClose').addEventListener('click', function () {
$('#eventDetailsModal').modal('hide'); $('#eventDetailsModal').modal('hide');
}); });
$('#saveEvent').on('click', function () {
var updatedEvent = {
id: window.calEvent.id,
title: $('#eventEditTitle').val(),
start: $('#eventEditStart').val(),
end: $('#eventEditEnd').val()
};
// Save the updates to the record
fetch('/calendar/updaterequest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedEvent)
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
// Optionally, use the response to add the event to the calendar
$('#eventDetailsModal').modal('hide');
// Update the original event object
if (window.calEvent.title != data.title) {
window.calEvent.setProp('title', data.title);
}
if (window.calEvent.start != data.start) {
window.calEvent.setStart(data.start);
}
if (window.calEvent.end != data.end) {
window.calEvent.setEnd(data.end);
}
window.calEvent.setProp('allDay', data.allDay);
window.calEvent.setProp('backgroundColor', data.backgroundColor);
e.target.reset();
})
.catch(error => {
console.error('Error creating event:', error);
//alert('An error occurred while creating the event.');
});
// Hide the popup
$('#eventEditModal').modal('hide');
});
$('#closeEventEdit').on('click', function () {
$('#eventEditModal').modal('hide');
});
}); });
</script> </script>
{% endblock scripts %} {% endblock scripts %}