Merge pull request 'single-user-edit-page' (#968) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m27s
CI/CD Pipeline / deploy-staging (push) Successful in 6m34s
CI/CD Pipeline / deploy-main (push) Has been skipped

Reviewed-on: #968
This commit is contained in:
philipp 2025-05-02 18:16:53 +02:00
commit 49e657ab54
10 changed files with 250 additions and 85 deletions

View File

@ -24,6 +24,7 @@ document.addEventListener("DOMContentLoaded", function () {
reloadPage();
setCurrentdate(<HTMLInputElement>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

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -1,6 +1,7 @@
use super::ScheckbuchUser;
use crate::model::{
logbook::{Logbook, LogbookWithBoatAndRowers},
role::Role,
user::User,
};
use serde::{Deserialize, Serialize};

View File

@ -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 {

View File

@ -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, &regular).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());
// }

View File

@ -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/<id>/scheckbook-to-regular", data = "<data>")]
async fn scheckbook_to_regular(
db: &State<SqlitePool>,
data: Form<ScheckToRegularForm<'_>>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
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<Route> {
routes![
index,
@ -840,5 +906,7 @@ pub fn routes() -> Vec<Route> {
add_membership_pdf,
add_role,
remove_role,
//
scheckbook_to_regular,
]
}

View File

@ -16,44 +16,23 @@
{% endif %}
</div>
<div class="py-3">
<ul>
<ul class="grid gap-3">
<li>
Mail: {{ user.mail }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-mail" method="post">
{{ macros::input(label='Neue Mailadresse', name='mail', type="text", value=user.mail) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
<form action="/admin/user/{{ user.id }}/change-mail" method="post">
{{ macros::inputgroup(label='Mailadresse', name='mail', type="text", value=user.mail, readonly=not allowed_to_edit) }}
</form>
</li>
<li>
<form action="/admin/user/{{ user.id }}/change-phone" method="post">
{{ macros::inputgroup(label='Telefonnummer', name='phone', type="text", value=user.phone, readonly=not allowed_to_edit) }}
</form>
</li>
<li>
<form action="/admin/user/{{ user.id }}/change-nickname" method="post">
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
</form>
</li>
<li>Notizen: to be replaced with activity :-)</li>
<li>
Telefon: {{ user.phone }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-phone" method="post">
{{ macros::input(label='Neue Telefonnummer', name='phone', type="text", value=user.phone) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Spitzname: {{ user.nickname }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-nickname" method="post">
{{ macros::input(label='Neuer Spitzname', name='nickname', type="text", value=user.nickname) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
</ul>
</div>
<div class="py-3">
@ -110,13 +89,7 @@
<h2 class="h2">💸</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{% if "Schnupperant" in member %}
{% if "paid" in user.roles %}
✅ Schnupperant hat schon bezahlt
{% else %}
❌ Schnupperant hat noch <b>nicht</b> bezahlt
{% endif %}
{% else %}
{% if fee %}
<div>
<strong>{{ fee.name }}</strong>
<span class="block">{{ fee.sum_in_cents / 100 }}€</span>
@ -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 %}
</div>
</div>
@ -143,44 +126,23 @@
<h2 class="h2">Vereinsmitglied</h2>
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<ul class="list-disc ms-4">
<ul class="grid gap-3">
<li>
Mitglied seit: {{ user.member_since_date }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-member-since" method="post">
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
<form action="/admin/user/{{ user.id }}/change-member-since" method="post">
{{ macros::inputgroup(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date, readonly=not allowed_to_edit) }}
</form>
</li>
<li>
Geburtsdatum: {{ user.birthdate }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-birthdate" method="post">
{{ macros::input(label='Geburtstag', name='birthdate', type="date", value=user.birthdate) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
<form action="/admin/user/{{ user.id }}/change-birthdate" method="post">
{{ macros::inputgroup(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, readonly=not allowed_to_edit) }}
</form>
</li>
<li>
Adresse: {{ user.address }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-address" method="post">
{{ macros::input(label='Neue Adresse', name='address', type="text", value=user.address) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
<form action="/admin/user/{{ user.id }}/change-address" method="post">
{{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
</form>
</li>
<li>
<li>
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 ⤵️
<form action="/admin/user/{{ user.id }}/add-membership-pdf" method="post">
<form action="/admin/user/{{ user.id }}/add-membership-pdf"
method="post"
enctype="multipart/form-data">
<fieldset>
{{ macros::input(label='Neue Beitrittserklärung hochladen', name='membership_pdf', type="file", accept='application/pdf') }}
<input value="Hochladen" type="submit" class="btn btn-primary ml-1" />
@ -248,6 +212,21 @@
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
{% endfor %}
</details>
<details>
<summary>Zu reguläres Vereinsmitglied umwandeln</summary>
<form action="/admin/user/{{ user.id }}/scheckbook-to-regular"
method="post"
enctype="multipart/form-data">
{{ 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') }}
<input value="Als neues, reguläres Mitglied anlegen"
type="submit"
class="btn btn-primary ml-1" />
</form>
</details>
{% elif "Regular" in member %}
ist ein reguläres Vereinsmitglied.
{% elif "Foerdernd" in member %}

View File

@ -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 %}/>
</div>
{% endmacro input %}
{% macro inputgroup(label, name, type, required=false, class='', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='') %}
<div class="{{ wrapper_class }}">
<label for="{{ name }}"
class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">
{{ label }}
</label>
<div class="input-group">
<input {% if type=='datetime-local' %}onclick='if (!this.value) setCurrentdate(this)'{% endif %}
{% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %}
name="{{ name }}"
type="{{ type }}"
{% if required %}required{% endif %}
value="{{ value }}"
class="input {% if readonly %}rounded-md{% else %}rounded-l-md{% endif %} {{ class }}"
placeholder="{% if hide_label %}{{ label }}{% endif %}"
{% if min is defined %}min="{{ min }}"{% endif %}
{% if autofocus %}autofocus{% endif %}
{% if accept %}accept="{{ accept }}"{% endif %}
{% if pattern %}pattern="{{ pattern }}"{% endif %}
readonly/>
{% if allowed_to_edit %}
<button type="button" class="btn btn-primary rounded-l-none-important edit-js">Ändern</button>
<input value="x" type="reset" class="edit-js btn btn-alert btn-hidden rounded-none-important"/>
<input value="💾" type="submit" class="btn btn-primary btn-hidden rounded-l-none-important" />
{% endif %}
</div>
</div>
{% endmacro inputgroup %}
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false, readonly=false) %}
<label for="{{ name }}{{ id }}"
class="flex items-center cursor-pointer text-black dark:text-white hover:text-gray-900 dark:hover:text-gray-100 {{ class }}">