Fix cottage calendar requests

This commit is contained in:
Chris Jean-Marie 2025-06-06 14:33:20 +00:00
parent 8032019807
commit e9edd3e82a
7 changed files with 301 additions and 49 deletions

View File

@ -0,0 +1,29 @@
-- Add down migration script here
ALTER TABLE if exists calendar_event_types
drop column if exists state;
DO $$
BEGIN
-- Check if the constraint 'unique_calendar_et_name' already exists
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'unique_calendar_et_name'
AND conrelid = 'public.calendar_event_types'::regclass -- Specify table to narrow down
) THEN
-- If it exists, drop the constraint
ALTER TABLE IF EXISTS public.calendar_event_types
DROP CONSTRAINT unique_calendar_et_name;
RAISE NOTICE 'Constraint unique_calendar_et_name dropped from table calendar_event_types.';
ELSE
RAISE NOTICE 'Constraint unique_calendar_et_name doesn''t exist on table calendar_event_types.';
END IF;
END
$$;
-- Add the constraint
ALTER TABLE IF EXISTS public.calendar_event_types
ADD CONSTRAINT unique_calendar_et_name UNIQUE (name);
RAISE NOTICE 'Constraint unique_calendar_et_name added to table calendar_event_types.';

View File

@ -0,0 +1,23 @@
-- Add up migration script here
ALTER TABLE if exists calendar_event_types
ADD COLUMN IF NOT EXISTS state character varying(25);
DO $$
BEGIN
-- Check if the constraint 'unique_calendar_et_name' already exists
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'unique_calendar_et_name'
AND conrelid = 'public.calendar_event_types'::regclass -- Specify table to narrow down
) THEN
-- If it exists, drop the constraint
ALTER TABLE IF EXISTS public.calendar_event_types
DROP CONSTRAINT unique_calendar_et_name;
END IF;
END
$$;
-- Add the constraint
ALTER TABLE IF EXISTS public.calendar_event_types
ADD CONSTRAINT unique_calendar_et_name UNIQUE (name, state);

View File

@ -0,0 +1 @@
-- Add down migration script here

View File

@ -0,0 +1,36 @@
-- Truncate tables
TRUNCATE TABLE calendar_event_types, calendar;
-- Add default calendar and event types
insert into calendar (created_by, updated_by, name, colour)
select id, id, 'Cottage', 'blue' from users where email = 'admin@jean-marie.ca';
insert into calendar (created_by, updated_by, name, colour)
select id, id, 'Family tree', 'brown' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Reservation', 'Requested', 'purple' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Reservation', 'Approved', 'green' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Reservation', 'Confirmed', 'blue' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Reservation', 'Tentative', 'light-purple' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Reservation', 'Rejected', 'red' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Reservation', 'Cancelled', 'light-red' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Life event', 'Birthday', 'green' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Life event', 'Anniversary', 'orange' from users where email = 'admin@jean-marie.ca';
insert into calendar_event_types (created_by, updated_by, name, state, colour)
select id, id, 'Life event', 'Other', 'light-orange' from users where email = 'admin@jean-marie.ca';

View File

@ -1,10 +1,14 @@
use askama::Template;
use axum::{
extract::{Path, Query, State}, response::{Html, IntoResponse, Redirect, Response}, Extension, Form
extract::{Query, State},
response::{Html, IntoResponse, Redirect, Response},
Extension, Form,
};
use chrono::Days;
use http::StatusCode;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool, Row};
use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row};
use tracing::event;
use uuid::Uuid;
use crate::{
@ -94,13 +98,41 @@ pub async fn get_calendars(db_pool: PgPool) -> Vec<Calendar> {
calendars
}
async fn get_event(event_id: Uuid, db_pool: &PgPool) -> String {
// Set default events
let mut eventstring: String = "[]".to_string();
let event: Result<PgRow, Error> = Ok(sqlx::query(
r#"select to_json(json_build_object(
'title', ce.title,
'start', ce.start_time,
'end', ce.end_time,
'allDay', false,
'backgroundColor', cet.colour))
from calendar_events ce
join calendar_event_types cet on cet.id = ce.event_type_id
where ce.id = $1"#,
)
.bind(event_id)
.fetch_one(db_pool)
.await
.unwrap());
if let Ok(json_string) = event {
if let Ok(stringevents) = json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or("")) {
//println!("Event: {:?}", stringevents);
eventstring = stringevents.to_string();
}
}
eventstring
}
pub async fn get_events(
Path(calendar): Path<String>,
State(db_pool): State<PgPool>,
Query(params): Query<EventParams>,
Extension(user_data): Extension<Option<AccountData>>,
) -> String {
//println!("Calendar: {}", calendar);
//println!("Paramters: {:?}", params);
// Is the user logged in?
let logged_in = user_data.is_some();
@ -109,32 +141,40 @@ pub async fn get_events(
let mut eventstring: String = "[]".to_string();
if logged_in {
// User is logged in
//println!("User is 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();
if is_authorized("/calendar", user_data, db_pool.clone()).await {
// User is authorized
//println!("User is authorized");
// Get requested calendar events from database
let events = sqlx::query(
r#"select to_json(json_agg(json_build_object(
'title', ce.title,
'start', ce.start_time,
'end', ce.end_time,
'allDay', false)))
'allDay', false,
'backgroundColor', cet.colour)))
from calendar_events ce
join calendar c on c.id = ce.calendar_id
join calendar_event_types cet on cet.id = ce.event_type_id
where ce.celebrate = true
and c.name = $1
and start_time > $2
and start_time < $3"#,
and c.name = 'Cottage'
and start_time > $1
and start_time < $2"#,
)
.bind(calendar)
.bind(chrono::DateTime::parse_from_rfc3339(&params.start).unwrap())
.bind(chrono::DateTime::parse_from_rfc3339(&params.end).unwrap())
.fetch_one(&db_pool)
.await;
//println!("Events: {:?}", events);
if let Ok(json_string) = events {
if let Ok(stringevents) =
json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or(""))
@ -185,7 +225,7 @@ pub async fn create_event(
if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await {
let fmt = "%Y-%m-%d";
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();
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 end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
@ -215,6 +255,89 @@ pub async fn create_event(
Redirect::to(&redirect_url).into_response()
}
#[derive(Deserialize, Serialize, Debug)]
pub struct NewRequest {
pub start: String,
pub end: String,
}
pub async fn new_request(
State(db_pool): State<PgPool>,
Extension(user_data): Extension<Option<AccountData>>,
request: axum::http::Request<axum::body::Body>,
) -> impl IntoResponse {
let logged_in = user_data.is_some();
// Set default events
let mut eventstring: String = "[]".to_string();
if logged_in {
// User is logged in
//println!("User is 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 is_authorized("/calendar", user_data, db_pool.clone()).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 params: NewRequest = serde_json::from_str(&body_str).unwrap();
let fmt = "%Y-%m-%d";
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 start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
let event = sqlx::query_scalar::<_, uuid::Uuid>(
r#"insert into calendar_events (created_by, updated_by, calendar_id, event_type_id, title, start_time, end_time)
select p.id as created_by,
p.id as updated_by,
c.id as calendar_id,
cet.id as event_type_id,
p.given_name as title,
$1 as start_time,
$2 as end_time
from calendar c,
calendar_event_types cet,
people p
where c.name = 'Cottage'
and cet.name = 'Reservation'
and cet.state = 'Requested'
and p.id = $3
returning id"#
)
.bind(start_datetime)
.bind(end_datetime)
.bind(personid)
.fetch_one(&db_pool)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error creating event: {}", e),
)
});
//println!("Event: {:#?}", &event.unwrap());
let event_id = event.clone();
eventstring = get_event(event_id.unwrap(), &db_pool).await;
}
}
eventstring
}
#[derive(Template)]
#[template(path = "newevent.html")]
struct EventTemplate {

View File

@ -1,6 +1,6 @@
use axum::{
middleware,
routing::{get, get_service, post, put},
routing::{get, get_service, post},
Extension, Router,
};
use dotenvy::var;
@ -31,6 +31,8 @@ use wishlist::{
user_wishlist_delete_item, user_wishlist_edit_item, user_wishlist_received_item,
user_wishlist_returned_item, user_wishlist_save_item, wishlists,
};
use crate::calendar::new_request;
//use email::send_emails;
#[derive(Clone)]
@ -86,9 +88,10 @@ async fn main() {
)
// Calendar
.route("/calendar", get(calendar))
.route("/getevents/{calendar}", get(get_events))
.route("/createevent", post(create_event))
.route("/newevent", get(new_event))
.route("/calendar/getevents", get(get_events))
.route("/calendar/createevent", post(create_event))
.route("/calendar/newevent", get(new_event))
.route("/calendar/newrequest", post(new_request))
// Wishlist
.route("/wishlists", get(wishlists))
.route("/userwishlist/{user_id}", get(user_wishlist))

View File

@ -4,7 +4,6 @@
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@4.3.0/main.min.css'>
{% endblock links %}
{% block center %}
<h1>Calendar</h1>
<div class="mh-100">
<div id="calendar" class="fc"></div>
</div>
@ -13,25 +12,34 @@
<div class="modal fade" id="eventDetailsModal" tabindex="-1" role="dialog" aria-labelledby="eventDetailsModalTitle"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<form id="eventDetailsModalForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="eventDetailsModalTitle">Event details</h5>
<h5 class="modal-title" id="eventDetailsModalTitle">Request dates</h5>
</div>
<div class="modal-body">
<p id="eventDetailsModalDateRange"></p>
<p id="eventDetailsModalBody">Cottage request</p>
<div class="form-group">
<label for="eventStart">Starting</label>
<input type="date" class="form-control" id="eventStart" required>
</div>
<div class="form-group">
<label for="eventEnd">Leaving</label>
<input type="date" class="form-control" id="eventEnd" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
<button type="button" id="eventDetailsModalClose" class="btn btn-secondary"
data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Send Request</button>
</div>
</div>
</form>
</div>
</div>
{% endblock center %}
{% block scripts %}
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js'></script>
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.17/index.global.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js'></script>
<script src='https://cdn.jsdelivr.net/npm/uuid@8.3.2/dist/umd/uuidv4.min.js'></script>
<script>
@ -50,29 +58,58 @@
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay,multiMonthYear'
},
eventSources: [
{% for calendar in calendars %}
{
url: '/getevents/{{calendar.name}}',
color: '{{calendar.colour}}',
},
{% endfor %}
],
events: '/calendar/getevents',
select: function (info) {
$('#eventStart').val(info.startStr);
$('#eventEnd').val(info.endStr);
$('#eventDetailsModal').modal('show');
$('#eventDetailsModalDateRange').text(info.startStr + ' to ' + info.endStr);
info.view.calendar.addEvent({
id: uuidv4(),
title: 'Cottage request',
start: info.startStr + 'T14:00:00',
end: info.endStr + 'T10:00:00',
allDay: false
});
}
});
calendar.render();
document.getElementById('eventDetailsModalForm').addEventListener('submit', function (e) {
e.preventDefault();
var start = document.getElementById('eventStart').value;
var end = document.getElementById('eventEnd').value;
// Prepare the event data
var eventData = {
start: start,
end: end
};
// Send data to the API
fetch('/calendar/newrequest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData)
})
.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');
calendar.addEvent({
title: data.title,
start: data.start,
end: data.end,
allDay: data.allDay,
backgroundColor: data.backgroundColor
});
e.target.reset();
})
.catch(error => {
console.error('Error creating event:', error);
alert('An error occurred while creating the event.');
});
});
document.getElementById('eventDetailsModalClose').addEventListener('click', function () {
$('#eventDetailsModal').modal('hide');
});
});
</script>
{% endblock scripts %}