diff --git a/backend/migrations/20241103140734_secret-gift-exchange.down.sql b/backend/migrations/20241103140734_secret-gift-exchange.down.sql new file mode 100644 index 0000000..506c33d --- /dev/null +++ b/backend/migrations/20241103140734_secret-gift-exchange.down.sql @@ -0,0 +1,5 @@ +-- Add down migration script here +drop table gift_exchange; +drop table gift_exchange_participants; + +delete from role_permissions where item = '/giftexchange'; diff --git a/backend/migrations/20241103140734_secret-gift-exchange.up.sql b/backend/migrations/20241103140734_secret-gift-exchange.up.sql new file mode 100644 index 0000000..0d5946a --- /dev/null +++ b/backend/migrations/20241103140734_secret-gift-exchange.up.sql @@ -0,0 +1,28 @@ +-- Add up migration script here +CREATE TABLE + `gift_exchange` ( + `id` integer not null primary key autoincrement, + `created_at` INTEGER not null default CURRENT_TIMESTAMP, + `created_by` integer not null default 0, + `updated_at` INTEGER not null default CURRENT_TIMESTAMP, + `updated_by` integer not null default 0, + `name` varchar(255) not null, + `exchange_date` INTEGER not null, + `status` INTEGER not null default 0, + unique (`id`) + ); + + CREATE TABLE + `gift_exchange_participants` ( + `id` integer not null primary key autoincrement, + `created_at` INTEGER not null default CURRENT_TIMESTAMP, + `created_by` integer not null default 0, + `updated_at` INTEGER not null default CURRENT_TIMESTAMP, + `updated_by` integer not null default 0, + `exchange_id` INTEGER not null, + `participant_id` INTEGER not null, + `gifter_id` INTEGER not null, + unique (`id`) + ); + +insert into `role_permissions` (`created_at`, `created_by`, `id`, `item`, `role_id`, `updated_at`, `updated_by`) values ('0', '0', '10', '/giftexchange', '2', '0', '0') \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index b45f866..06d45eb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,6 +2,7 @@ use std::net::SocketAddr; use axum::{ middleware, routing::{get, get_service}, Extension, Router }; +use secret_gift_exchange::{giftexchange, giftexchange_save, giftexchanges}; use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; use sqlx::migrate::Migrator; use tower_http::services::ServeDir; @@ -13,6 +14,7 @@ mod routes; mod user; mod wishlist; mod email; +mod secret_gift_exchange; use error_handling::AppError; use middlewares::inject_user_data; @@ -65,6 +67,8 @@ async fn main() { .route("/userwishlist/received/:user_id", get(user_wishlist_received_item)) .route("/userwishlist/delete/:item_id", get(user_wishlist_delete_item)) .route("/userwishlist/returned/:item_id", get(user_wishlist_returned_item)) + .route("/giftexchanges", get(giftexchanges)) + .route("/giftexchange/:giftexchange_id", get(giftexchange).post(giftexchange_save)) .nest_service("/assets", ServeDir::new("templates/assets") .fallback(get_service(ServeDir::new("templates/assets")))) .route("/", get(index)) diff --git a/backend/src/secret_gift_exchange.rs b/backend/src/secret_gift_exchange.rs new file mode 100644 index 0000000..bbc6ee0 --- /dev/null +++ b/backend/src/secret_gift_exchange.rs @@ -0,0 +1,261 @@ +use std::collections::HashMap; + +use askama::Template; +use askama_axum::{IntoResponse, Response}; +use axum::{ + body::{self, Body}, + extract::{FromRequest, Path, Request, State}, + response::Redirect, + Extension, Form, Json, RequestExt, +}; +use axum_extra::response::Html; +use chrono::Utc; +use http::{header::CONTENT_TYPE, StatusCode}; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; + +use crate::{ + middlewares::is_authorized, + user::{get_user_roles_display, UserData}, +}; + +/// Select participants from user list +/// create group id for exchange +/// allow user to only see their recipient but whole list of participants +/// link to recipient wish list +/// button to create selections +/// +/// Database schema +/// Table - gift_exchange +/// Columns - id -> number +/// - created_by -> number +/// - created_at -> number +/// - updated_by -> number +/// - updated_at -> number +/// - name -> text +/// - exchange_date -> number +/// +/// Table - gift_exchange_participants +/// Columns - id -> number +/// - created_by -> number +/// - created_at -> number +/// - updated_by -> number +/// - updated_at -> number +/// - exchange_id -> number (reference gift_exchange table) +/// - participant_id -> number (reference user table) +/// - gifter_id -> number (reference user table) +/// +/// Pages - sge_list +/// - list of gift exchanges user is part of +/// - sge_exchange +/// - exchange details +/// - list of participants +/// - sge_edit +/// - create new exchange +/// - edit existing exchange +/// - sge_participant_edit +/// - add or remove participant to exchange +/// +/// API - select gifters + +struct HtmlTemplate(T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> Response { + match self.0.render() { + Ok(html) => Html(html).into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to render template. Error: {}", err), + ) + .into_response(), + } + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)] +struct GiftExchange { + id: i64, + created_at: i64, + created_by: i64, + updated_at: i64, + updated_by: i64, + name: String, + exchange_date: i64, + status: i64, +} + +#[derive(Template)] +#[template(path = "giftexchanges.html")] +struct GiftExchangesTemplate { + logged_in: bool, + name: String, + user_roles: Vec, + giftexchanges: Vec, +} + +pub async fn giftexchanges( + Extension(user_data): Extension>, + State(db_pool): State, +) -> impl IntoResponse { + let user_name = user_data.as_ref().map(|s| s.name.clone()); + let logged_in = user_name.is_some(); + let name = user_name.unwrap_or_default(); + + let giftexchanges = sqlx::query_as::<_, GiftExchange>("SELECT * FROM gift_exchange") + .fetch_all(&db_pool) + .await + .unwrap(); + + let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); + + if is_authorized("/giftexchange", user_data, db_pool.clone()).await { + // Get user roles + let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; + + let template = GiftExchangesTemplate { + logged_in, + name, + user_roles, + giftexchanges, + }; + HtmlTemplate(template).into_response() + } else { + Redirect::to("/").into_response() + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)] +struct GiftExchangeParticipant { + id: i64, + created_at: i64, + created_by: i64, + updated_at: i64, + updated_by: i64, + exchange_id: i64, + participant_id: i64, + gifter_id: i64, +} + +#[derive(Template)] +#[template(path = "giftexchange.html")] +struct GiftExchangeTemplate { + logged_in: bool, + name: String, + user_roles: Vec, + giftexchange: GiftExchange, + participants: Vec, + non_participants: Vec, +} + +pub async fn giftexchange( + Path(exchange_id): Path, + Extension(user_data): Extension>, + State(db_pool): State, +) -> impl IntoResponse { + let user_name = user_data.as_ref().map(|s| s.name.clone()); + let logged_in = user_name.is_some(); + let name = user_name.unwrap_or_default(); + let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); + + if is_authorized("/giftexchange", user_data, db_pool.clone()).await { + // Get user roles + let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; + + // Get gift exchange + let giftexchange = match sqlx::query_as!( + GiftExchange, + "SELECT * FROM gift_exchange WHERE id = ?", + exchange_id + ) + .fetch_one(&db_pool) + .await + { + Ok(giftexchange) => giftexchange, + Err(_) => GiftExchange::default(), + }; + + // Get participants + let participants = sqlx::query_as::<_, UserData>( + "select * from users where users.id in (select participant_id from gift_exchange_participants where exchange_id = ?)", + ) + .bind(exchange_id) + .fetch_all(&db_pool) + .await + .unwrap(); + + // Get non participants + let non_participants = sqlx::query_as::<_, UserData>( + "select * from users where users.id not in (select participant_id from gift_exchange_participants where exchange_id = ?)", + ) + .bind(exchange_id) + .fetch_all(&db_pool) + .await + .unwrap(); + + let template = GiftExchangeTemplate { + logged_in, + name, + user_roles, + giftexchange, + participants, + non_participants, + }; + HtmlTemplate(template).into_response() + } else { + Redirect::to("/").into_response() + } +} + +#[derive(Deserialize, Debug)] +pub struct ExchangeForm { + name: String, + exchange_date: String, + non_participants: Vec, +} + +pub async fn giftexchange_save( + State(db_pool): State, + request: Request, +) -> impl IntoResponse { + let content_type_header = request.headers().get(CONTENT_TYPE); + let content_type = content_type_header.and_then(|value| value.to_str().ok()); + + /* if let Some(content_type) = content_type { + if content_type.starts_with("application/json") { + let payload = request + .extract() + .await + .map_err(IntoResponse::into_response); + } + + if content_type.starts_with("application/x-www-form-urlencoded") { + let payload = request + .extract() + .await + .map_err(IntoResponse::into_response); + } + } */ + let (req_parts, map_request_body) = request.into_parts(); + let bytes = match body::to_bytes(map_request_body,usize::MAX).await { + Ok(bytes) => bytes, + Err(err) => { + return Err(( + StatusCode::BAD_REQUEST, + format!("failed to read request body: {}", err), + )); + } + }; + + println!("Saving gift exchange: {:?}", req_parts); + println!("Saving gift exchange: {:?} ", bytes); + Ok(Redirect::to("/").into_response()) +} + +#[derive(Debug, Serialize, Deserialize)] +struct Payload { + foo: String, +} diff --git a/backend/src/wishlist.rs b/backend/src/wishlist.rs index 25062b9..cc02c2a 100644 --- a/backend/src/wishlist.rs +++ b/backend/src/wishlist.rs @@ -1,8 +1,12 @@ use askama_axum::{IntoResponse, Response, Template}; use axum::{ + extract::{Path, State}, + response::Redirect, + Extension, Form, +, }; use axum_extra::response::Html; use chrono::Utc; @@ -11,11 +15,14 @@ use serde::Deserialize; use sqlx::{Row, SqlitePool}; use crate::{ + middlewares::is_authorized, + user::{ get_user_roles_display, get_user_wishlist_item_by_id, get_user_wishlist_items, UserData, UserWishlistItem, }, +, }; struct HtmlTemplate(T); @@ -111,9 +118,9 @@ pub async fn user_wishlist( .await .unwrap(); - if is_authorized("/wishlist", user_data, db_pool.clone()).await { - // Get user roles - let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; + if is_authorized("/wishlist", user_data, db_pool.clone()).await { + // Get user roles + let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; // Get user wishlist let person_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await; @@ -203,6 +210,7 @@ pub async fn user_wishlist_add_item( State(db_pool): State, Extension(user_data): Extension>, Form(item_form): Form, + Form(item_form): Form, ) -> impl IntoResponse { if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { // Insert new item to database diff --git a/backend/templates/authorized.html b/backend/templates/authorized.html index 1c8500a..f3429f7 100644 --- a/backend/templates/authorized.html +++ b/backend/templates/authorized.html @@ -9,6 +9,7 @@
  • Web links
  • Cottage Calendar
  • Wish lists
  • +
  • Gift Exchanges
  • {% for user_role in user_roles %} {% if user_role.role_name == "admin" %} diff --git a/backend/templates/base.html b/backend/templates/base.html index 64b151c..e4cf8ca 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -8,10 +8,11 @@ - + @@ -24,9 +25,12 @@ {% block links %}{% endblock links %} + + + -
    +
    @@ -76,10 +80,16 @@
    + {% block scripts %}{% endblock scripts %} + + + {% block script %}{% endblock script %} \ No newline at end of file diff --git a/backend/templates/giftexchange.html b/backend/templates/giftexchange.html new file mode 100644 index 0000000..772760d --- /dev/null +++ b/backend/templates/giftexchange.html @@ -0,0 +1,158 @@ +{% extends "authorized.html" %} +{% block title %}Gift Exchange{% endblock %} +{% block center %} +
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + + {% for participant in non_participants %} + + + + + + {% endfor %} + +
    AvailableID
    {{ participant.name }}{{ participant.id }}
    +
    +
    +
    + + +
    +
    +
    + + + + + + + + + + {% for participant in participants %} + + + + + + {% endfor %} + +
    ParticipatingID
    {{ participant.name }}{{ participant.id }}
    +
    +
    +
    +{% endblock center %} + +{% block script %} + +{% endblock script %} \ No newline at end of file diff --git a/backend/templates/giftexchanges.html b/backend/templates/giftexchanges.html new file mode 100644 index 0000000..bffbc3f --- /dev/null +++ b/backend/templates/giftexchanges.html @@ -0,0 +1,29 @@ +{% extends "authorized.html" %} +{% block title %}Gift Exchanges{% endblock %} +{% block center %} +
    + +
    +
    + + + + + + + + + + {% for giftexchange in giftexchanges %} + + + + + + {% endfor %} + +
    NameDateStatus
    {{ giftexchange.name }}{{ giftexchange.exchange_date }}{{ giftexchange.status }}
    +
    +{% endblock center %} \ No newline at end of file diff --git a/backend/templates/useradmin.html b/backend/templates/useradmin.html index 0418cd4..b215f79 100644 --- a/backend/templates/useradmin.html +++ b/backend/templates/useradmin.html @@ -2,11 +2,11 @@ {% block title %}User Administration{% endblock %} {% block center %}

    Users

    - +
    - - + + diff --git a/backend/templates/userwishlist.html b/backend/templates/userwishlist.html index 6ecbe1c..44ef8dd 100644 --- a/backend/templates/userwishlist.html +++ b/backend/templates/userwishlist.html @@ -12,12 +12,12 @@ Add {% endif %}
    -
    NameemailNameemail
    +
    - + - + diff --git a/backend/templates/userwishlists.html b/backend/templates/userwishlists.html index be9e49a..b857ea7 100644 --- a/backend/templates/userwishlists.html +++ b/backend/templates/userwishlists.html @@ -2,11 +2,11 @@ {% block title %}Wish Lists{% endblock %} {% block center %}

    Wishlists

    -
    ItemItem LinkStateState Action
    +
    - - + +
    NameemailNameemail