diff --git a/frontend/main.ts b/frontend/main.ts index e8d528c..82c38e9 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -24,6 +24,7 @@ document.addEventListener("DOMContentLoaded", function () { reloadPage(); setCurrentdate(document.querySelector("#departure")); initDropdown(); + editReadOnlyField(); }); function changeTheme() { @@ -40,6 +41,25 @@ function changeTheme() { } } + +function editReadOnlyField() { + const editBtns = document.querySelectorAll( + '.edit-js' + ); + if (editBtns) { + Array.prototype.forEach.call(editBtns, (btn: HTMLButtonElement) => { + btn.addEventListener("click", function () { + let wrapper = btn.parentElement; + let input = wrapper?.querySelector('input'); + + wrapper?.classList.toggle('editable') + input?.toggleAttribute('readonly'); + if(!input?.hasAttribute('readonly')) input?.focus(); + }); + }); + } +} + /*** * init javascript * 1) detect native color scheme or use set theme in local storage diff --git a/frontend/scss/components/_btns.scss b/frontend/scss/components/_btns.scss index 5691039..873617f 100644 --- a/frontend/scss/components/_btns.scss +++ b/frontend/scss/components/_btns.scss @@ -28,4 +28,8 @@ &[aria-pressed='true'] { @apply outline outline-2 outline-offset-2 outline-primary-600 bg-primary-100 text-primary-950; } + + &-hidden { + @apply hidden; + } } diff --git a/frontend/scss/components/_important.scss b/frontend/scss/components/_important.scss index 7d2e869..54a7790 100644 --- a/frontend/scss/components/_important.scss +++ b/frontend/scss/components/_important.scss @@ -2,3 +2,12 @@ border-top-left-radius: 0px !important; border-top-right-radius: 0px !important; } + +.rounded-l-none-important { + border-bottom-left-radius: 0px !important; + border-top-left-radius: 0px !important; +} + +.rounded-none-important { + border-radius: 0px !important; +} diff --git a/frontend/scss/components/_input.scss b/frontend/scss/components/_input.scss index 8665c7c..54e75c8 100644 --- a/frontend/scss/components/_input.scss +++ b/frontend/scss/components/_input.scss @@ -2,6 +2,25 @@ @apply relative block w-full bg-white dark:bg-black border-0 py-1.5 px-2 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-black placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6; } +.input-group { + @apply flex; + + input[readonly] { + opacity: .7; + } + + &.editable { + input[type="reset"], + input[type="submit"] { + @apply block; + } + + button[type="button"] { + @apply hidden; + } + } +} + select { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); background-repeat: no-repeat; diff --git a/src/model/user/member.rs b/src/model/user/member.rs index 9161ac6..faebd87 100644 --- a/src/model/user/member.rs +++ b/src/model/user/member.rs @@ -1,6 +1,7 @@ use super::ScheckbuchUser; use crate::model::{ logbook::{Logbook, LogbookWithBoatAndRowers}, + role::Role, user::User, }; use serde::{Deserialize, Serialize}; diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 8ca32b8..ba89924 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -35,7 +35,7 @@ use scheckbuch::ScheckbuchUser; mod basic; mod fee; pub(crate) mod member; -mod scheckbuch; +pub(crate) mod scheckbuch; #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] pub struct User { diff --git a/src/model/user/scheckbuch.rs b/src/model/user/scheckbuch.rs index 2521eef..9a66521 100644 --- a/src/model/user/scheckbuch.rs +++ b/src/model/user/scheckbuch.rs @@ -1,10 +1,15 @@ -use super::User; +use super::member::Member; +use super::{ManageUserUser, User}; +use crate::model::role::Role; use crate::model::user::LoginError; +use crate::tera::admin::user::ScheckToRegularForm; use crate::{ model::{mail::Mail, notification::Notification}, special_user, SCHECKBUCH, }; +use chrono::NaiveDate; use rocket::async_trait; +use rocket::fs::TempFile; use rocket::http::Status; use rocket::request; use rocket::request::FromRequest; @@ -16,10 +21,39 @@ use std::ops::Deref; special_user!(ScheckbuchUser, +"scheckbuch"); impl ScheckbuchUser { + pub(crate) async fn convert_to_regular_user( + self, + db: &SqlitePool, + changed_by: &ManageUserUser, + member_since: &NaiveDate, + birthdate: &NaiveDate, + phone: &str, + address: &str, + membership_pdf: &TempFile<'_>, + ) -> Result<(), String> { + // Set data + self.user.update_birthdate(db, changed_by, birthdate).await; + self.user + .update_member_since(db, changed_by, member_since) + .await; + self.user.update_phone(db, changed_by, phone).await?; + self.user.update_address(db, changed_by, address).await?; + self.user.update_address(db, changed_by, address).await?; + self.user + .add_membership_pdf(db, changed_by, membership_pdf) + .await?; + + // Change roles + let regular = Role::find_by_name(db, "Donau Linz").await.unwrap(); + let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap(); + self.user.remove_role(db, changed_by, &scheckbook).await?; + self.user.add_role(db, changed_by, ®ular).await?; + + // Notify + todo!() // Continue here + } + //async fn from(user: User, db: &SqlitePool, mail: &str, smtp_pw: &str) -> Result<(), String> { - // // TODO: see when/how to invoke this function (explicit `Neue Person hinzufügen` button? - // // Button to transition existing users to scheckbuch? Automatically called when - // // `scheckbuch` is newly selected as role? // if user.has_role(db, "scheckbuch").await { // return Err("User is already a scheckbuch".into()); // } diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 42b40f0..a9d2e4d 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -7,8 +7,9 @@ use crate::{ logbook::Logbook, role::Role, user::{ - member::Member, AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, - UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, + member::Member, scheckbuch::ScheckbuchUser, AdminUser, AllowedToEditPaymentStatusUser, + ManageUserUser, User, UserWithDetails, UserWithMembershipPdf, + UserWithRolesAndMembershipPdf, VorstandUser, }, }, tera::Config, @@ -553,7 +554,7 @@ async fn update_member_since( return Flash::error( Redirect::to("/admin/user/{id}"), format!( - "Datum {} ist nicht im YYYY-MM-DD Format", + "Beitrittsdatum {} ist nicht im YYYY-MM-DD Format", &data.member_since ), ); @@ -589,7 +590,10 @@ async fn update_birthdate( let Ok(new_birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else { return Flash::error( Redirect::to("/admin/user/{id}"), - format!("Datum {} ist nicht im YYYY-MM-DD Format", &data.birthdate), + format!( + "Geburtsdatum {} ist nicht im YYYY-MM-DD Format", + &data.birthdate + ), ); }; @@ -813,6 +817,68 @@ struct UserAddScheckbuchForm<'r> { // Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap())) //} +#[derive(FromForm, Debug)] +pub struct ScheckToRegularForm<'a> { + member_since: String, + birthdate: String, + phone: String, + address: String, + membership_pdf: TempFile<'a>, +} + +#[post("/user//scheckbook-to-regular", data = "")] +async fn scheckbook_to_regular( + db: &State, + data: Form>, + admin: ManageUserUser, + id: i32, +) -> Flash { + let Some(user) = User::find_by_id(db, id).await else { + return Flash::error( + Redirect::to("/admin/user"), + format!("User with ID {} does not exist!", id), + ); + }; + let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!("Datum {} ist nicht im YYYY-MM-DD Format", &data.birthdate), + ); + }; + let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + format!("Datum {} ist nicht im YYYY-MM-DD Format", &data.birthdate), + ); + }; + + let Some(user) = ScheckbuchUser::new(db, &user).await else { + return Flash::error( + Redirect::to(format!("/admin/user/{id}")), + "User ist kein Scheckbuchuser", + ); + }; + + match user + .convert_to_regular_user( + db, + &admin, + &member_since, + &birthdate, + &data.phone, + &data.address, + &data.membership_pdf, + ) + .await + { + Ok(_) => Flash::success( + Redirect::to(format!("/admin/user/{}", id)), + "Mitgliedstyp umgewandelt und Infos versendet", + ), + Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e), + } +} + pub fn routes() -> Vec { routes![ index, @@ -840,5 +906,7 @@ pub fn routes() -> Vec { add_membership_pdf, add_role, remove_role, + // + scheckbook_to_regular, ] } diff --git a/templates/admin/user/view.html.tera b/templates/admin/user/view.html.tera index a7db460..f870461 100644 --- a/templates/admin/user/view.html.tera +++ b/templates/admin/user/view.html.tera @@ -16,44 +16,23 @@ {% endif %}
-
    +
    • - Mail: {{ user.mail }} - {% if allowed_to_edit %} -
      - ✏️ -
      - {{ macros::input(label='Neue Mailadresse', name='mail', type="text", value=user.mail) }} - -
      -
      - {% endif %} +
      + {{ macros::inputgroup(label='Mailadresse', name='mail', type="text", value=user.mail, readonly=not allowed_to_edit) }} +
      +
    • +
    • +
      + {{ macros::inputgroup(label='Telefonnummer', name='phone', type="text", value=user.phone, readonly=not allowed_to_edit) }} +
      +
    • +
    • +
      + {{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }} +
    • Notizen: to be replaced with activity :-)
    • -
    • - Telefon: {{ user.phone }} - {% if allowed_to_edit %} -
      - ✏️ -
      - {{ macros::input(label='Neue Telefonnummer', name='phone', type="text", value=user.phone) }} - -
      -
      - {% endif %} -
    • -
    • - Spitzname: {{ user.nickname }} - {% if allowed_to_edit %} -
      - ✏️ -
      - {{ macros::input(label='Neuer Spitzname', name='nickname', type="text", value=user.nickname) }} - -
      -
      - {% endif %} -
@@ -110,13 +89,7 @@

💸

- {% if "Schnupperant" in member %} - {% if "paid" in user.roles %} - ✅ Schnupperant hat schon bezahlt - {% else %} - ❌ Schnupperant hat noch nicht bezahlt - {% endif %} - {% else %} + {% if fee %}
{{ fee.name }} {{ fee.sum_in_cents / 100 }}€ @@ -132,6 +105,16 @@ {% else %} ❌ Zahlung ausständig {% endif %} + {% else %} + {% if "paid" in user.roles %} + ✅ {{ member | keys }} hat schon bezahlt + {% else %} + ❌ + {% for key, value in member %} + {% if loop.first %}{{ key }}{% endif %} + {% endfor %} + hat noch nicht bezahlt + {% endif %} {% endif %}
@@ -143,44 +126,23 @@

Vereinsmitglied

-
    +
    • - Mitglied seit: {{ user.member_since_date }} - {% if allowed_to_edit %} -
      - ✏️ -
      - {{ macros::input(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date) }} - -
      -
      - {% endif %} +
      + {{ macros::inputgroup(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date, readonly=not allowed_to_edit) }} +
    • - Geburtsdatum: {{ user.birthdate }} - {% if allowed_to_edit %} -
      - ✏️ -
      - {{ macros::input(label='Geburtstag', name='birthdate', type="date", value=user.birthdate) }} - -
      -
      - {% endif %} +
      + {{ macros::inputgroup(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, readonly=not allowed_to_edit) }} +
    • - Adresse: {{ user.address }} - {% if allowed_to_edit %} -
      - ✏️ -
      - {{ macros::input(label='Neue Adresse', name='address', type="text", value=user.address) }} - -
      -
      - {% endif %} +
      + {{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }} +
    • -
    • +
    • Familie: {% for family in families %} {% if user.family_id == family.id %}{{ family.names }}{% endif %} @@ -205,7 +167,9 @@ ⚠️ Aktuell gibt's keine Beitrittserklärung 😢 {% if allowed_to_edit %} Das kannst du hier ändern ⤵️ -
      +
      {{ macros::input(label='Neue Beitrittserklärung hochladen', name='membership_pdf', type="file", accept='application/pdf') }} @@ -248,6 +212,21 @@ {{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }} {% endfor %} +
      + Zu reguläres Vereinsmitglied umwandeln + + {{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date()) }} + {{ macros::input(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate) }} + {{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone) }} + {{ macros::input(label='Adresse', name='address', type="text", value=user.address) }} + {{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf') }} + + +
      {% elif "Regular" in member %} ist ein reguläres Vereinsmitglied. {% elif "Foerdernd" in member %} diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 109bb4f..1dcb298 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -174,9 +174,40 @@ function setChoiceByLabel(choicesInstance, label) { {% if autofocus %}autofocus{% endif %} {% if accept %}accept="{{ accept }}"{% endif %} {% if pattern %}pattern="{{ pattern }}"{% endif %} - {% if readonly %}readonly{% endif %}> + {% if readonly %}readonly{% endif %}/>
{% endmacro input %} + +{% macro inputgroup(label, name, type, required=false, class='', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='') %} +
+ +
+ + {% if allowed_to_edit %} + + + + {% endif %} +
+
+{% endmacro inputgroup %} + + {% macro checkbox(label, name, id='', checked=false, class='', disabled=false, readonly=false) %}