Added user/visits 'deleted' status, hide it in UI
Build and Publish / Build and Publish Docker Image (push) Successful in 1m22s
Build and Publish / Build and Publish Docker Image (push) Successful in 1m22s
This commit is contained in:
Generated
+1
-1
@@ -3467,7 +3467,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.13"
|
version = "0.1.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "web-petting"
|
name = "web-petting"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+95
-13
@@ -366,7 +366,7 @@ struct ScheduleEditTemplate<'a> {
|
|||||||
lang: Lang,
|
lang: Lang,
|
||||||
admin_name: &'a str,
|
admin_name: &'a str,
|
||||||
visit: Visit,
|
visit: Visit,
|
||||||
clients: Vec<Client>,
|
client: Client,
|
||||||
users: Vec<User>,
|
users: Vec<User>,
|
||||||
media: Vec<Media>,
|
media: Vec<Media>,
|
||||||
}
|
}
|
||||||
@@ -943,8 +943,10 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R
|
|||||||
let tz = crate::tz::load_tz(&db).await;
|
let tz = crate::tz::load_tz(&db).await;
|
||||||
let today = crate::tz::today_in_tz(tz);
|
let today = crate::tz::today_in_tz(tz);
|
||||||
|
|
||||||
let all_visits = Visit::objects().all(&db).await?;
|
let mut all_visits = Visit::objects().all(&db).await?;
|
||||||
let clients = Client::objects().all(&db).await?;
|
all_visits.retain(|v| v.status != "deleted");
|
||||||
|
let mut clients = Client::objects().all(&db).await?;
|
||||||
|
clients.retain(|c| c.status != "deleted");
|
||||||
|
|
||||||
let mut today_visits: Vec<TodayVisit> = all_visits
|
let mut today_visits: Vec<TodayVisit> = all_visits
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1046,11 +1048,12 @@ async fn clients_page(request: Request, session: Session, db: Database) -> cot::
|
|||||||
Err(resp) => return Ok(resp),
|
Err(resp) => return Ok(resp),
|
||||||
};
|
};
|
||||||
let show_all = has_query_flag(&request, "all");
|
let show_all = has_query_flag(&request, "all");
|
||||||
let clients = if show_all {
|
let mut clients = if show_all {
|
||||||
Client::objects().all(&db).await?
|
Client::objects().all(&db).await?
|
||||||
} else {
|
} else {
|
||||||
query!(Client, $status == "active").all(&db).await?
|
query!(Client, $status == "active").all(&db).await?
|
||||||
};
|
};
|
||||||
|
clients.retain(|c| c.status != "deleted");
|
||||||
let body = ClientsTemplate {
|
let body = ClientsTemplate {
|
||||||
t: lang.t(),
|
t: lang.t(),
|
||||||
lang,
|
lang,
|
||||||
@@ -1464,6 +1467,24 @@ async fn client_activate(
|
|||||||
Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response()
|
Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn client_delete(
|
||||||
|
request: Request,
|
||||||
|
session: Session,
|
||||||
|
db: Database,
|
||||||
|
Path(client_id): Path<i64>,
|
||||||
|
) -> cot::Result<Response> {
|
||||||
|
let lang = detect_lang(&request);
|
||||||
|
if let Err(resp) = require_auth(&session, lang).await {
|
||||||
|
return Ok(resp);
|
||||||
|
}
|
||||||
|
if let Some(mut client) = query!(Client, $id == client_id).get(&db).await? {
|
||||||
|
client.status = "deleted".to_string();
|
||||||
|
client.updated_at = now_utc();
|
||||||
|
client.save(&db).await?;
|
||||||
|
}
|
||||||
|
Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
async fn user_archive(
|
async fn user_archive(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: Session,
|
session: Session,
|
||||||
@@ -1681,12 +1702,18 @@ async fn schedule_events(
|
|||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
for v in &visits {
|
for v in &visits {
|
||||||
|
if v.status == "deleted" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if v.visit_date < start_date || v.visit_date > end_date {
|
if v.visit_date < start_date || v.visit_date > end_date {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let client_id_val: i64 = v.client_id.primary_key().unwrap();
|
let client_id_val: i64 = v.client_id.primary_key().unwrap();
|
||||||
let user_id_val: i64 = v.user_id.primary_key().unwrap();
|
let user_id_val: i64 = v.user_id.primary_key().unwrap();
|
||||||
let client = clients.iter().find(|c| c.id.unwrap() == client_id_val);
|
let client = clients.iter().find(|c| c.id.unwrap() == client_id_val);
|
||||||
|
if client.map(|c| c.status.as_str()) == Some("deleted") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let user = users.iter().find(|u| u.id.unwrap() == user_id_val);
|
let user = users.iter().find(|u| u.id.unwrap() == user_id_val);
|
||||||
let client_name = client.map(|c| c.name.as_str()).unwrap_or("?");
|
let client_name = client.map(|c| c.name.as_str()).unwrap_or("?");
|
||||||
let client_phone = client.and_then(|c| c.phone.as_deref()).unwrap_or("");
|
let client_phone = client.and_then(|c| c.phone.as_deref()).unwrap_or("");
|
||||||
@@ -1810,7 +1837,16 @@ async fn schedule_edit_page(
|
|||||||
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let clients = query!(Client, $status == "active").all(&db).await?;
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
|
let client_id: i64 = visit.client_id.primary_key().unwrap();
|
||||||
|
let client = match query!(Client, $id == client_id).get(&db).await? {
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
let users = query!(User, $status == "active").all(&db).await?;
|
let users = query!(User, $status == "active").all(&db).await?;
|
||||||
let mut visit_media = Media::objects().all(&db).await?;
|
let mut visit_media = Media::objects().all(&db).await?;
|
||||||
visit_media.retain(|m| {
|
visit_media.retain(|m| {
|
||||||
@@ -1826,7 +1862,7 @@ async fn schedule_edit_page(
|
|||||||
lang,
|
lang,
|
||||||
admin_name: &admin_name,
|
admin_name: &admin_name,
|
||||||
visit,
|
visit,
|
||||||
clients,
|
client,
|
||||||
users,
|
users,
|
||||||
media: visit_media,
|
media: visit_media,
|
||||||
}
|
}
|
||||||
@@ -1836,7 +1872,6 @@ async fn schedule_edit_page(
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct EditVisitForm {
|
struct EditVisitForm {
|
||||||
client_id: i64,
|
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
visit_date: String,
|
visit_date: String,
|
||||||
time_start: String,
|
time_start: String,
|
||||||
@@ -1857,7 +1892,9 @@ async fn schedule_edit_submit(
|
|||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
visit.client_id = ForeignKey::PrimaryKey(Auto::fixed(form.client_id));
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
visit.user_id = ForeignKey::PrimaryKey(Auto::fixed(form.user_id));
|
visit.user_id = ForeignKey::PrimaryKey(Auto::fixed(form.user_id));
|
||||||
if let Ok(d) = chrono::NaiveDate::parse_from_str(&form.visit_date, "%Y-%m-%d") {
|
if let Ok(d) = chrono::NaiveDate::parse_from_str(&form.visit_date, "%Y-%m-%d") {
|
||||||
visit.visit_date = d;
|
visit.visit_date = d;
|
||||||
@@ -1883,7 +1920,11 @@ async fn visit_delete(
|
|||||||
if let Err(resp) = require_auth(&session, lang).await {
|
if let Err(resp) = require_auth(&session, lang).await {
|
||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
query!(Visit, $id == visit_id).delete(&db).await?;
|
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
|
visit.status = "deleted".to_string();
|
||||||
|
visit.updated_at = now_utc();
|
||||||
|
visit.save(&db).await?;
|
||||||
|
}
|
||||||
Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response()
|
Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1898,6 +1939,9 @@ async fn visit_set_done(
|
|||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
visit.status = "completed".to_string();
|
visit.status = "completed".to_string();
|
||||||
visit.updated_at = now_utc();
|
visit.updated_at = now_utc();
|
||||||
visit.save(&db).await?;
|
visit.save(&db).await?;
|
||||||
@@ -1916,6 +1960,9 @@ async fn visit_set_cancel(
|
|||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
visit.status = "cancelled".to_string();
|
visit.status = "cancelled".to_string();
|
||||||
visit.updated_at = now_utc();
|
visit.updated_at = now_utc();
|
||||||
visit.save(&db).await?;
|
visit.save(&db).await?;
|
||||||
@@ -1945,16 +1992,40 @@ async fn media_page(request: Request, session: Session, db: Database) -> cot::Re
|
|||||||
})
|
})
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let clients_all = Client::objects().all(&db).await?;
|
||||||
|
let visits_all = Visit::objects().all(&db).await?;
|
||||||
let mut media_list = Media::objects().all(&db).await?;
|
let mut media_list = Media::objects().all(&db).await?;
|
||||||
media_list.retain(|m| m.status == "active");
|
media_list.retain(|m| {
|
||||||
|
if m.status != "active" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let cid: i64 = m.client_id.primary_key().unwrap();
|
||||||
|
if clients_all
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.id.unwrap() == cid)
|
||||||
|
.map(|c| c.status.as_str())
|
||||||
|
== Some("deleted")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(fk) = &m.visit_id {
|
||||||
|
let vid: i64 = fk.primary_key().unwrap();
|
||||||
|
if visits_all
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.id.unwrap() == vid)
|
||||||
|
.map(|v| v.status.as_str())
|
||||||
|
== Some("deleted")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
if filter_client_id > 0 {
|
if filter_client_id > 0 {
|
||||||
media_list.retain(|m| m.client_id.primary_key().unwrap() == filter_client_id);
|
media_list.retain(|m| m.client_id.primary_key().unwrap() == filter_client_id);
|
||||||
}
|
}
|
||||||
media_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
media_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||||
|
|
||||||
let clients_all = Client::objects().all(&db).await?;
|
|
||||||
let visits_all = Visit::objects().all(&db).await?;
|
|
||||||
|
|
||||||
let items: Vec<MediaItem> = media_list
|
let items: Vec<MediaItem> = media_list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|m| {
|
.map(|m| {
|
||||||
@@ -2008,6 +2079,9 @@ async fn media_upload_page(
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Redirect::new(format!("/admin/?lang={}", lang.code())).into_response(),
|
None => return Redirect::new(format!("/admin/?lang={}", lang.code())).into_response(),
|
||||||
};
|
};
|
||||||
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
let cid: i64 = visit.client_id.primary_key().unwrap();
|
let cid: i64 = visit.client_id.primary_key().unwrap();
|
||||||
let client = query!(Client, $id == cid).get(&db).await?;
|
let client = query!(Client, $id == cid).get(&db).await?;
|
||||||
let client_name = client.map(|c| c.name).unwrap_or_default();
|
let client_name = client.map(|c| c.name).unwrap_or_default();
|
||||||
@@ -2056,6 +2130,9 @@ async fn media_upload_submit(
|
|||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Redirect::new(format!("/admin/?lang={}", lang.code())).into_response(),
|
None => return Redirect::new(format!("/admin/?lang={}", lang.code())).into_response(),
|
||||||
};
|
};
|
||||||
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
|
||||||
|
}
|
||||||
let client_id: i64 = visit.client_id.primary_key().unwrap();
|
let client_id: i64 = visit.client_id.primary_key().unwrap();
|
||||||
|
|
||||||
let bytes = request.into_body().into_bytes().await?;
|
let bytes = request.into_body().into_bytes().await?;
|
||||||
@@ -2591,6 +2668,11 @@ pub fn admin_router() -> Router {
|
|||||||
client_activate,
|
client_activate,
|
||||||
"admin-client-activate",
|
"admin-client-activate",
|
||||||
),
|
),
|
||||||
|
Route::with_handler_and_name(
|
||||||
|
"/clients/{client_id}/delete",
|
||||||
|
client_delete,
|
||||||
|
"admin-client-delete",
|
||||||
|
),
|
||||||
Route::with_handler_and_name("/schedule", schedule_page, "admin-schedule"),
|
Route::with_handler_and_name("/schedule", schedule_page, "admin-schedule"),
|
||||||
Route::with_handler_and_name("/schedule/new", schedule_new_page, "admin-schedule-new"),
|
Route::with_handler_and_name("/schedule/new", schedule_new_page, "admin-schedule-new"),
|
||||||
Route::with_handler_and_name("/schedule/events", schedule_events, "admin-schedule-events"),
|
Route::with_handler_and_name("/schedule/events", schedule_events, "admin-schedule-events"),
|
||||||
|
|||||||
+14
@@ -103,8 +103,11 @@ pub struct Translations {
|
|||||||
pub clients_media_link: &'static str,
|
pub clients_media_link: &'static str,
|
||||||
pub clients_add_title: &'static str,
|
pub clients_add_title: &'static str,
|
||||||
pub clients_add_button: &'static str,
|
pub clients_add_button: &'static str,
|
||||||
|
pub clients_delete: &'static str,
|
||||||
|
pub clients_delete_confirm: &'static str,
|
||||||
pub client_status_active: &'static str,
|
pub client_status_active: &'static str,
|
||||||
pub client_status_archived: &'static str,
|
pub client_status_archived: &'static str,
|
||||||
|
pub client_status_deleted: &'static str,
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
pub users_title: &'static str,
|
pub users_title: &'static str,
|
||||||
@@ -256,6 +259,7 @@ pub struct Translations {
|
|||||||
pub visit_status_scheduled: &'static str,
|
pub visit_status_scheduled: &'static str,
|
||||||
pub visit_status_completed: &'static str,
|
pub visit_status_completed: &'static str,
|
||||||
pub visit_status_cancelled: &'static str,
|
pub visit_status_cancelled: &'static str,
|
||||||
|
pub visit_status_deleted: &'static str,
|
||||||
pub schedule_mark_done: &'static str,
|
pub schedule_mark_done: &'static str,
|
||||||
pub schedule_cancel: &'static str,
|
pub schedule_cancel: &'static str,
|
||||||
pub schedule_edit_title: &'static str,
|
pub schedule_edit_title: &'static str,
|
||||||
@@ -336,8 +340,11 @@ static RU: Translations = Translations {
|
|||||||
clients_media_link: "Медиа",
|
clients_media_link: "Медиа",
|
||||||
clients_add_title: "Добавить клиента",
|
clients_add_title: "Добавить клиента",
|
||||||
clients_add_button: "Добавить",
|
clients_add_button: "Добавить",
|
||||||
|
clients_delete: "Удалить клиента",
|
||||||
|
clients_delete_confirm: "Точно удалить этого клиента?",
|
||||||
client_status_active: "Активный",
|
client_status_active: "Активный",
|
||||||
client_status_archived: "Архив",
|
client_status_archived: "Архив",
|
||||||
|
client_status_deleted: "Удалён",
|
||||||
|
|
||||||
users_title: "Администраторы",
|
users_title: "Администраторы",
|
||||||
users_login: "Логин",
|
users_login: "Логин",
|
||||||
@@ -453,6 +460,7 @@ static RU: Translations = Translations {
|
|||||||
visit_status_scheduled: "Запланирован",
|
visit_status_scheduled: "Запланирован",
|
||||||
visit_status_completed: "Выполнен",
|
visit_status_completed: "Выполнен",
|
||||||
visit_status_cancelled: "Отменён",
|
visit_status_cancelled: "Отменён",
|
||||||
|
visit_status_deleted: "Удалён",
|
||||||
schedule_mark_done: "Выполнен",
|
schedule_mark_done: "Выполнен",
|
||||||
schedule_cancel: "Отменить",
|
schedule_cancel: "Отменить",
|
||||||
schedule_edit_title: "Редактировать визит",
|
schedule_edit_title: "Редактировать визит",
|
||||||
@@ -557,8 +565,11 @@ static EN: Translations = Translations {
|
|||||||
clients_media_link: "Media",
|
clients_media_link: "Media",
|
||||||
clients_add_title: "Add Client",
|
clients_add_title: "Add Client",
|
||||||
clients_add_button: "Add",
|
clients_add_button: "Add",
|
||||||
|
clients_delete: "Delete client",
|
||||||
|
clients_delete_confirm: "Are you sure you want to delete this client?",
|
||||||
client_status_active: "Active",
|
client_status_active: "Active",
|
||||||
client_status_archived: "Archived",
|
client_status_archived: "Archived",
|
||||||
|
client_status_deleted: "Deleted",
|
||||||
|
|
||||||
users_title: "Administrators",
|
users_title: "Administrators",
|
||||||
users_login: "Login",
|
users_login: "Login",
|
||||||
@@ -674,6 +685,7 @@ static EN: Translations = Translations {
|
|||||||
visit_status_scheduled: "Scheduled",
|
visit_status_scheduled: "Scheduled",
|
||||||
visit_status_completed: "Completed",
|
visit_status_completed: "Completed",
|
||||||
visit_status_cancelled: "Cancelled",
|
visit_status_cancelled: "Cancelled",
|
||||||
|
visit_status_deleted: "Deleted",
|
||||||
schedule_mark_done: "Done",
|
schedule_mark_done: "Done",
|
||||||
schedule_cancel: "Cancel",
|
schedule_cancel: "Cancel",
|
||||||
schedule_edit_title: "Edit Visit",
|
schedule_edit_title: "Edit Visit",
|
||||||
@@ -760,6 +772,7 @@ impl Translations {
|
|||||||
"scheduled" => self.visit_status_scheduled,
|
"scheduled" => self.visit_status_scheduled,
|
||||||
"completed" => self.visit_status_completed,
|
"completed" => self.visit_status_completed,
|
||||||
"cancelled" => self.visit_status_cancelled,
|
"cancelled" => self.visit_status_cancelled,
|
||||||
|
"deleted" => self.visit_status_deleted,
|
||||||
_ => "?",
|
_ => "?",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -768,6 +781,7 @@ impl Translations {
|
|||||||
match status {
|
match status {
|
||||||
"active" => self.client_status_active,
|
"active" => self.client_status_active,
|
||||||
"archived" => self.client_status_archived,
|
"archived" => self.client_status_archived,
|
||||||
|
"deleted" => self.client_status_deleted,
|
||||||
_ => "?",
|
_ => "?",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -51,14 +51,32 @@ impl App for PublicApp {
|
|||||||
|
|
||||||
struct PettingProject;
|
struct PettingProject;
|
||||||
|
|
||||||
|
fn parse_bool_env(name: &str) -> Option<bool> {
|
||||||
|
let value = std::env::var(name).ok()?;
|
||||||
|
match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"1" | "true" | "yes" | "on" => Some(true),
|
||||||
|
"0" | "false" | "no" | "off" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_enabled(config_name: &str) -> bool {
|
||||||
|
parse_bool_env("WEB_PETTING_DEBUG").unwrap_or_else(|| {
|
||||||
|
matches!(
|
||||||
|
config_name,
|
||||||
|
"dev" | "development" | "debug" | "local" | "test"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl Project for PettingProject {
|
impl Project for PettingProject {
|
||||||
fn cli_metadata(&self) -> CliMetadata {
|
fn cli_metadata(&self) -> CliMetadata {
|
||||||
cot::cli::metadata!()
|
cot::cli::metadata!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config(&self, _config_name: &str) -> cot::Result<ProjectConfig> {
|
fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
|
||||||
Ok(ProjectConfig::builder()
|
Ok(ProjectConfig::builder()
|
||||||
.debug(true)
|
.debug(debug_enabled(config_name))
|
||||||
.database(
|
.database(
|
||||||
DatabaseConfig::builder()
|
DatabaseConfig::builder()
|
||||||
.url("sqlite://db.sqlite3?mode=rwc")
|
.url("sqlite://db.sqlite3?mode=rwc")
|
||||||
|
|||||||
+3
-1
@@ -43,6 +43,7 @@ pub enum VisitStatus {
|
|||||||
Scheduled,
|
Scheduled,
|
||||||
Completed,
|
Completed,
|
||||||
Cancelled,
|
Cancelled,
|
||||||
|
Deleted,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VisitStatus {
|
impl VisitStatus {
|
||||||
@@ -51,6 +52,7 @@ impl VisitStatus {
|
|||||||
Self::Scheduled => "scheduled",
|
Self::Scheduled => "scheduled",
|
||||||
Self::Completed => "completed",
|
Self::Completed => "completed",
|
||||||
Self::Cancelled => "cancelled",
|
Self::Cancelled => "cancelled",
|
||||||
|
Self::Deleted => "deleted",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ pub struct Visit {
|
|||||||
pub public_notes: Option<String>,
|
pub public_notes: Option<String>,
|
||||||
/// Feedback text from client via portal.
|
/// Feedback text from client via portal.
|
||||||
pub client_feedback: Option<String>,
|
pub client_feedback: Option<String>,
|
||||||
/// scheduled | completed | cancelled
|
/// scheduled | completed | cancelled | deleted
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub created_at: chrono::NaiveDateTime,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
pub updated_at: chrono::NaiveDateTime,
|
pub updated_at: chrono::NaiveDateTime,
|
||||||
|
|||||||
+26
-4
@@ -240,7 +240,8 @@ async fn client_portal(
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let client = match query!(Client, $media_token == token).get(&db).await? {
|
let client = match query!(Client, $media_token == token).get(&db).await? {
|
||||||
Some(c) => c,
|
Some(c) if c.status != "deleted" => c,
|
||||||
|
Some(_) => return Html::new("404").into_response(),
|
||||||
None => return Html::new("404").into_response(),
|
None => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,7 +250,11 @@ async fn client_portal(
|
|||||||
let today = crate::tz::today_in_tz(tz);
|
let today = crate::tz::today_in_tz(tz);
|
||||||
|
|
||||||
let mut visits = Visit::objects().all(&db).await?;
|
let mut visits = Visit::objects().all(&db).await?;
|
||||||
visits.retain(|v| v.client_id.primary_key().unwrap() == client_id && v.status != "cancelled");
|
visits.retain(|v| {
|
||||||
|
v.client_id.primary_key().unwrap() == client_id
|
||||||
|
&& v.status != "cancelled"
|
||||||
|
&& v.status != "deleted"
|
||||||
|
});
|
||||||
visits.sort_by(|a, b| {
|
visits.sort_by(|a, b| {
|
||||||
a.visit_date
|
a.visit_date
|
||||||
.cmp(&b.visit_date)
|
.cmp(&b.visit_date)
|
||||||
@@ -327,7 +332,8 @@ async fn submit_feedback(
|
|||||||
// Verify token matches visit's client
|
// Verify token matches visit's client
|
||||||
let token_clone = token.clone();
|
let token_clone = token.clone();
|
||||||
let client = match query!(Client, $media_token == token).get(&db).await? {
|
let client = match query!(Client, $media_token == token).get(&db).await? {
|
||||||
Some(c) => c,
|
Some(c) if c.status != "deleted" => c,
|
||||||
|
Some(_) => return Html::new("404").into_response(),
|
||||||
None => return Html::new("404").into_response(),
|
None => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
let client_id = client.id.unwrap();
|
let client_id = client.id.unwrap();
|
||||||
@@ -346,6 +352,14 @@ async fn submit_feedback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
if let Some(mut visit) = query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
|
if visit.status == "deleted" {
|
||||||
|
return Redirect::new(format!(
|
||||||
|
"/client/{}?lang={}",
|
||||||
|
token_clone,
|
||||||
|
lang.code()
|
||||||
|
))
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
if visit.client_id.primary_key().unwrap() == client_id {
|
if visit.client_id.primary_key().unwrap() == client_id {
|
||||||
visit.client_feedback = Some(form.feedback);
|
visit.client_feedback = Some(form.feedback);
|
||||||
visit.updated_at = now_utc();
|
visit.updated_at = now_utc();
|
||||||
@@ -369,7 +383,8 @@ async fn portal_media(
|
|||||||
) -> cot::Result<Response> {
|
) -> cot::Result<Response> {
|
||||||
// Verify token
|
// Verify token
|
||||||
let client = match query!(Client, $media_token == token).get(&db).await? {
|
let client = match query!(Client, $media_token == token).get(&db).await? {
|
||||||
Some(c) => c,
|
Some(c) if c.status != "deleted" => c,
|
||||||
|
Some(_) => return Html::new("404").into_response(),
|
||||||
None => return Html::new("404").into_response(),
|
None => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
let client_id = client.id.unwrap();
|
let client_id = client.id.unwrap();
|
||||||
@@ -378,6 +393,13 @@ async fn portal_media(
|
|||||||
Some(m) if m.client_id.primary_key().unwrap() == client_id && m.status == "active" => m,
|
Some(m) if m.client_id.primary_key().unwrap() == client_id && m.status == "active" => m,
|
||||||
_ => return Html::new("404").into_response(),
|
_ => return Html::new("404").into_response(),
|
||||||
};
|
};
|
||||||
|
if let Some(fk) = &media.visit_id {
|
||||||
|
let visit_id: i64 = fk.primary_key().unwrap();
|
||||||
|
match query!(Visit, $id == visit_id).get(&db).await? {
|
||||||
|
Some(v) if v.status != "deleted" => {}
|
||||||
|
_ => return Html::new("404").into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match tokio::fs::read(&media.file_path).await {
|
match tokio::fs::read(&media.file_path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
|
|||||||
@@ -76,11 +76,16 @@
|
|||||||
<form method="post" action="/admin/clients/{{ client_id }}/archive">
|
<form method="post" action="/admin/clients/{{ client_id }}/archive">
|
||||||
<button type="submit" class="button is-warning is-outlined is-fullwidth">{{ t.action_archive }}</button>
|
<button type="submit" class="button is-warning is-outlined is-fullwidth">{{ t.action_archive }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else if client_status == "archived" %}
|
||||||
<form method="post" action="/admin/clients/{{ client_id }}/activate">
|
<form method="post" action="/admin/clients/{{ client_id }}/activate">
|
||||||
<button type="submit" class="button is-success is-outlined is-fullwidth">{{ t.action_activate }}</button>
|
<button type="submit" class="button is-success is-outlined is-fullwidth">{{ t.action_activate }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if client_status != "deleted" %}
|
||||||
|
<form method="post" action="/admin/clients/{{ client_id }}/delete" onsubmit="return confirm('{{ t.clients_delete_confirm }}');" style="margin-top:0.75rem;">
|
||||||
|
<button type="submit" class="button is-danger is-outlined is-fullwidth">{{ t.clients_delete }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,15 +14,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ t.schedule_client }}</label>
|
<label class="label">{{ t.schedule_client }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select is-fullwidth">
|
<input class="input" type="text" value="{{ client.name }}{% if let Some(p) = client.phone.as_deref() %} ({{ p }}){% endif %}" readonly>
|
||||||
<select name="client_id" required>
|
|
||||||
{% for c in &clients %}
|
|
||||||
<option value="{{ c.id }}" {% if c.id.unwrap() == visit.client_id.primary_key().unwrap() %}selected{% endif %}>
|
|
||||||
{{ c.name }}{% if let Some(p) = c.phone.as_deref() %} ({{ p }}){% endif %}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -132,6 +124,9 @@
|
|||||||
{% if let Some(cap) = m.caption.as_deref() %}
|
{% if let Some(cap) = m.caption.as_deref() %}
|
||||||
<div class="media-cap">{{ cap }}</div>
|
<div class="media-cap">{{ cap }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="visit-media-delete">
|
||||||
|
<button type="submit" form="visit-media-delete-{{ m.id.unwrap() }}" class="button is-small is-danger is-outlined">{{ t.media_delete }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -141,6 +136,9 @@
|
|||||||
|
|
||||||
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% for m in &media %}
|
||||||
|
<form id="visit-media-delete-{{ m.id.unwrap() }}" method="post" action="/admin/media/{{ m.id }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');"></form>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<hr style="margin:1rem 0;">
|
<hr style="margin:1rem 0;">
|
||||||
<form method="post" action="/admin/schedule/{{ visit.id }}/delete" onsubmit="return confirm('{{ t.schedule_delete_confirm }}');">
|
<form method="post" action="/admin/schedule/{{ visit.id }}/delete" onsubmit="return confirm('{{ t.schedule_delete_confirm }}');">
|
||||||
@@ -254,6 +252,14 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
.visit-media-delete {
|
||||||
|
padding: 0.25rem 0.4rem 0.4rem;
|
||||||
|
}
|
||||||
|
.visit-media-delete .button {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
min-height: 1.65rem;
|
||||||
|
}
|
||||||
.upload-modal-bg {
|
.upload-modal-bg {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user