diff --git a/.vscode/settings.json b/.vscode/settings.json index c0565cc..005d96d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "now.instance.host.url": "", "now.instance.OAuth.client.id": "", "now.instance.user.password": "", - "now.instance.OAuth.client.secret": "" + "now.instance.OAuth.client.secret": "", + "postman.settings.dotenv-detection-notification-visibility": false } \ No newline at end of file diff --git a/backend/migrations/20250525235328_add_state_to_calendar_event.down.sql b/backend/migrations/20250525235328_add_state_to_calendar_event.down.sql new file mode 100644 index 0000000..7c2c1e1 --- /dev/null +++ b/backend/migrations/20250525235328_add_state_to_calendar_event.down.sql @@ -0,0 +1,3 @@ +-- Remove column from calendar_events +ALTER TABLE if exists calendar_events + drop column if exists state; diff --git a/backend/migrations/20250525235328_add_state_to_calendar_event.up.sql b/backend/migrations/20250525235328_add_state_to_calendar_event.up.sql new file mode 100644 index 0000000..e787d72 --- /dev/null +++ b/backend/migrations/20250525235328_add_state_to_calendar_event.up.sql @@ -0,0 +1,3 @@ +-- Add column to calendar_events +ALTER TABLE if exists calendar_events + ADD COLUMN IF NOT EXISTS state character varying(25); \ No newline at end of file diff --git a/backend/migrations/20250527031601_default_calendar_and_event_types.down.sql b/backend/migrations/20250527031601_default_calendar_and_event_types.down.sql new file mode 100644 index 0000000..1ca89a4 --- /dev/null +++ b/backend/migrations/20250527031601_default_calendar_and_event_types.down.sql @@ -0,0 +1,10 @@ +-- Remove default calendar and event types +DELETE FROM calendar WHERE name = 'Cottage'; +DELETE FROM calendar WHERE name = 'Family tree'; + +DELETE FROM calendar_event_types WHERE name = 'Reservation'; +DELETE FROM calendar_event_types WHERE name = 'Life event'; + +-- Remove constraints +ALTER TABLE public.calendar DROP CONSTRAINT unique_name; +ALTER TABLE public.calendar_event_types DROP CONSTRAINT unique_name; diff --git a/backend/migrations/20250527031601_default_calendar_and_event_types.up.sql b/backend/migrations/20250527031601_default_calendar_and_event_types.up.sql new file mode 100644 index 0000000..19632a5 --- /dev/null +++ b/backend/migrations/20250527031601_default_calendar_and_event_types.up.sql @@ -0,0 +1,56 @@ +-- Add required constraints +DO $$ +BEGIN + -- Check if the constraint 'unique_name' already exists + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'unique_calendar_name' + AND conrelid = 'public.calendar'::regclass -- Specify table to narrow down + ) THEN + -- If it doesn't exist, add the constraint + ALTER TABLE IF EXISTS public.calendar + ADD CONSTRAINT unique_calendar_name UNIQUE (name); + RAISE NOTICE 'Constraint unique_calendar_name added to table calendar.'; + ELSE + RAISE NOTICE 'Constraint unique_calendar_name already exists on table calendar.'; + END IF; +END +$$; + +DO $$ +BEGIN + -- Check if the constraint 'unique_name' already exists + IF NOT 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 doesn't exist, 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.'; + ELSE + RAISE NOTICE 'Constraint unique_calendar_et_name already exists on table calendar_event_types.'; + END IF; +END +$$; + +-- Add default calendar and event types +insert into calendar (created_by, updated_by, name) +select id, id, 'Cottage' from users where email = 'admin@jean-marie.ca' +on conflict (name) do nothing; + +insert into calendar (created_by, updated_by, name) +select id, id, 'Family tree' from users where email = 'admin@jean-marie.ca' +on conflict (name) do nothing; + +insert into calendar_event_types (created_by, updated_by, name) +select id, id, 'Reservation' from users where email = 'admin@jean-marie.ca' +on conflict (name) do nothing; + +insert into calendar_event_types (created_by, updated_by, name) +select id, id, 'Life event' from users where email = 'admin@jean-marie.ca' +on conflict (name) do nothing; + diff --git a/backend/migrations/20250527225828_add_colour_to_calendar.down.sql b/backend/migrations/20250527225828_add_colour_to_calendar.down.sql new file mode 100644 index 0000000..20d487f --- /dev/null +++ b/backend/migrations/20250527225828_add_colour_to_calendar.down.sql @@ -0,0 +1,7 @@ +-- Add down migration script here +ALTER TABLE if exists calendar + drop column if exists colour; + +ALTER TABLE if exists calendar_event_types + drop column if exists colour; + \ No newline at end of file diff --git a/backend/migrations/20250527225828_add_colour_to_calendar.up.sql b/backend/migrations/20250527225828_add_colour_to_calendar.up.sql new file mode 100644 index 0000000..37846d5 --- /dev/null +++ b/backend/migrations/20250527225828_add_colour_to_calendar.up.sql @@ -0,0 +1,6 @@ +-- Add up migration script here +ALTER TABLE if exists calendar + ADD COLUMN IF NOT EXISTS colour character varying(50); + +ALTER TABLE if exists calendar_event_types + ADD COLUMN IF NOT EXISTS colour character varying(50); \ No newline at end of file diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index 0c8adba..8e629d4 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -1,10 +1,14 @@ +use core::fmt; + use askama::Template; use axum::{ - extract::{Path, Query, State}, response::{Html, IntoResponse, Redirect, Response}, Extension + body::{self, Body}, extract::{Path, Query, State}, response::{Html, IntoResponse, Redirect, Response}, Extension, Form }; +use chrono::{FixedOffset, Utc}; use http::StatusCode; use serde::{Deserialize, Serialize}; -use sqlx::{PgPool, Row}; +use sqlx::{FromRow, PgPool, Row}; +use uuid::Uuid; use crate::{ middlewares::is_authorized, @@ -29,7 +33,9 @@ where } } -struct Calendar { +#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)] +pub struct Calendar { + id: Uuid, name: String, colour: String, } @@ -59,16 +65,7 @@ pub async fn calendar( // Get user roles let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; - let calendars: Vec = vec![ - Calendar { - name: "Cottage".to_string(), - colour: "green".to_string(), - }, - Calendar { - name: "Personal".to_string(), - colour: "blue".to_string(), - }, - ]; + let calendars: Vec = get_calendars(db_pool.clone()).await; let template = CalendarTemplate { logged_in, @@ -90,13 +87,23 @@ pub struct EventParams { start: String, end: String, } + +pub async fn get_calendars(db_pool: PgPool) -> Vec { + let calendars: Vec = sqlx::query_as(r#"select id, name, colour from calendar"#) + .fetch_all(&db_pool) + .await + .unwrap(); + + calendars +} + pub async fn get_events( Path(calendar): Path, State(db_pool): State, Query(params): Query, Extension(user_data): Extension>, ) -> String { - println!("Calendar: {}", calendar); + //println!("Calendar: {}", calendar); //println!("Paramters: {:?}", params); // Is the user logged in? let logged_in = user_data.is_some(); @@ -111,7 +118,8 @@ pub async fn get_events( if is_authorized("/calendar", user_data, db_pool.clone()).await { // Get requested calendar events from database - let events = sqlx::query(r#"select to_json(json_agg(json_build_object( + let events = sqlx::query( + r#"select to_json(json_agg(json_build_object( 'title', ce.title, 'start', ce.start_time, 'end', ce.end_time, @@ -122,21 +130,131 @@ pub async fn get_events( where ce.celebrate = true and c.name = $1 and start_time > $2 - and start_time < $3"#) - .bind(calendar) - .bind(chrono::DateTime::parse_from_rfc3339(¶ms.start).unwrap()) - .bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap()) - .fetch_one(&db_pool) - .await; + and start_time < $3"#, + ) + .bind(calendar) + .bind(chrono::DateTime::parse_from_rfc3339(¶ms.start).unwrap()) + .bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap()) + .fetch_one(&db_pool) + .await; - if let Ok(json_string) = events { - if let Ok(stringevents) = json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or("")) { - println!("PgValue: {:?}", stringevents); - eventstring = stringevents.to_string(); - } + if let Ok(json_string) = events { + if let Ok(stringevents) = + json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or("")) + { + //println!("PgValue: {:?}", stringevents); + eventstring = stringevents.to_string(); } + } } } eventstring } + +#[derive(Deserialize, Serialize, Debug)] +pub struct Event { + pub id: uuid::Uuid, + pub created_at: chrono::NaiveDateTime, + pub created_by: uuid::Uuid, + pub updated_at: chrono::NaiveDateTime, + pub updated_by: uuid::Uuid, + pub calendar_id: uuid::Uuid, + pub event_type_id: uuid::Uuid, + pub title: String, + pub description: String, + pub state: String, + pub start_time: chrono::NaiveDateTime, + pub end_time: chrono::NaiveDateTime, + pub celebrate: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct EventCreate { + pub title: String, + pub description: String, + pub state: String, + pub calendar_id: uuid::Uuid, + pub event_type_id: uuid::Uuid, + pub start_time: String, + pub end_time: String, +} + +pub async fn create_event( + State(db_pool): State, + Extension(user_data): Extension>, + Form(event): Form, +) -> impl IntoResponse { + 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 start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap(); + let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap(); + + let start_dt = start_datetime.and_utc(); + let end_dt = end_datetime.and_utc(); + + let _ = sqlx::query( + r#"INSERT INTO calendar_events (created_by, updated_by, calendar_id, event_type_id, title, description, state, start_time, end_time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"#) + .bind(user_data.as_ref().unwrap().id)// Created by current user + .bind(user_data.as_ref().unwrap().id) // Updated by current user + .bind(event.calendar_id) + .bind(event.event_type_id) + .bind(event.title) + .bind(event.description) + .bind(event.state) + .bind(start_dt) + .bind(end_dt) + .execute(&db_pool) + .await + .map_err(|e| { + (StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating event: {}", e)) + }); + } + + let redirect_url = format!("/calendar"); + Redirect::to(&redirect_url).into_response() +} + +#[derive(Template)] +#[template(path = "newevent.html")] +struct EventTemplate { + logged_in: bool, + user: UserData, + user_roles: Vec, + calendars: Vec, +} + +pub async fn new_event( + Extension(user_data): Extension>, + State(db_pool): State, +) -> impl IntoResponse { + // Is the user logged in? + let logged_in = user_data.is_some(); + + 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(); + + if is_authorized("/calendar", user_data, db_pool.clone()).await { + // Get user roles + let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; + let calendars: Vec = get_calendars(db_pool.clone()).await; + + let template = EventTemplate { + logged_in, + user, + user_roles, + calendars, + }; + HtmlTemplate(template).into_response() + } else { + Redirect::to("/").into_response() + } + } else { + Redirect::to("/").into_response() + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 53dacef..1f63dde 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,6 +1,6 @@ use axum::{ middleware, - routing::{get, get_service}, + routing::{get, get_service, post, put}, Extension, Router, }; use dotenvy::var; @@ -20,7 +20,7 @@ mod secret_gift_exchange; mod user; mod wishlist; -use calendar::{calendar, get_events}; +use calendar::{calendar, get_events, create_event, new_event}; use error_handling::AppError; use google_oauth::{google_auth_return, login, logout}; use middlewares::inject_user_data; @@ -87,6 +87,8 @@ async fn main() { // Calendar .route("/calendar", get(calendar)) .route("/getevents/{calendar}", get(get_events)) + .route("/createevent", post(create_event)) + .route("/newevent", get(new_event)) // Wishlist .route("/wishlists", get(wishlists)) .route("/userwishlist/{user_id}", get(user_wishlist)) diff --git a/backend/src/routes.rs b/backend/src/routes.rs index 93dbb06..758f51d 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -170,8 +170,8 @@ pub async fn user_profile( 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 user = user_data.as_ref().unwrap().clone(); + //let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); // Extract the user data. let profile: UserData = sqlx::query_as( "SELECT * FROM people WHERE id = $1") diff --git a/backend/templates/authorized.html b/backend/templates/authorized.html index 6cc1fb4..402c336 100644 --- a/backend/templates/authorized.html +++ b/backend/templates/authorized.html @@ -14,6 +14,7 @@ {% endif %} {% endfor %} diff --git a/backend/templates/base.html b/backend/templates/base.html index bc14410..1fc2a31 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -21,6 +21,8 @@ body { font-family: "Montserrat", sans-serif; + height: 100vh; + margin: 0; } {% block links %}{% endblock links %} @@ -30,7 +32,8 @@ -
+
+
@@ -64,7 +67,7 @@
-
+
{% block content %}{% endblock content %}
@@ -76,6 +79,7 @@
+
diff --git a/backend/templates/newevent.html b/backend/templates/newevent.html new file mode 100644 index 0000000..bf4ff0e --- /dev/null +++ b/backend/templates/newevent.html @@ -0,0 +1,44 @@ +{% extends "authorized.html" %} +{% block center %} +

Create Event

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+{% endblock center %} \ No newline at end of file diff --git a/backend/toprod.sh b/backend/toprod.sh index e29e5c1..af96081 100755 --- a/backend/toprod.sh +++ b/backend/toprod.sh @@ -1,5 +1,5 @@ cargo build --release -ssh www@192.168.59.11 'pkill jean-marie' +#ssh www@192.168.59.11 'pkill jean-marie' scp target/release/jean-marie www@192.168.59.11:/opt/jean-marie scp runsite.sh www@192.168.59.11:/opt/jean-marie scp prod.env www@192.168.59.11:/opt/jean-marie