Added reviews. Added pricing.
Build and Publish / Build and Publish Docker Image (push) Successful in 6m27s

This commit is contained in:
Ultradesu
2026-05-11 11:34:11 +01:00
parent ff32e6bbaf
commit d1ef66acc1
14 changed files with 839 additions and 194 deletions
+390 -128
View File
@@ -1,15 +1,15 @@
use cot::Template;
use cot::db::{Auto, Database, ForeignKey, Model, query};
use cot::html::Html;
use cot::request::extractors::Path;
use cot::request::Request;
use cot::request::extractors::Path;
use cot::response::{IntoResponse, Redirect, Response};
use cot::router::{Route, Router};
use cot::session::Session;
use cot::Template;
use serde::Deserialize;
use crate::i18n::{Lang, Translations};
use crate::models::{Client, Lead, Media, Setting, User, Visit};
use crate::models::{Client, Lead, Media, Setting, Testimonial, User, Visit};
use crate::telegram;
const SESSION_USER_ID: &str = "user_id";
@@ -81,16 +81,18 @@ fn has_query_flag(request: &Request, flag: &str) -> bool {
request
.uri()
.query()
.map(|q| q.split('&').any(|p| p == format!("{}=1", flag) || p == flag))
.map(|q| {
q.split('&')
.any(|p| p == format!("{}=1", flag) || p == flag)
})
.unwrap_or(false)
}
/// Soft pastel palette for client calendar colors.
const CLIENT_COLORS: &[&str] = &[
"#7c6ed4", "#5b9bd5", "#4caf93", "#e0915e", "#d46c8e",
"#8e6bbf", "#5cb8a5", "#c77c4f", "#a3729a", "#6b9e5e",
"#d48b6c", "#7a8fc4", "#c45d7c", "#5eab7d", "#b8864e",
"#9476b8", "#6aafb5", "#d4785e", "#7f8e5b", "#b56c9e",
"#7c6ed4", "#5b9bd5", "#4caf93", "#e0915e", "#d46c8e", "#8e6bbf", "#5cb8a5", "#c77c4f",
"#a3729a", "#6b9e5e", "#d48b6c", "#7a8fc4", "#c45d7c", "#5eab7d", "#b8864e", "#9476b8",
"#6aafb5", "#d4785e", "#7f8e5b", "#b56c9e",
];
fn rand_client_color() -> &'static str {
@@ -136,11 +138,7 @@ async fn get_admin_name(session: &Session) -> Option<String> {
/// Get admin user ID from session.
async fn get_admin_id(session: &Session) -> Option<i64> {
session
.get::<i64>(SESSION_USER_ID)
.await
.ok()
.flatten()
session.get::<i64>(SESSION_USER_ID).await.ok().flatten()
}
/// Redirect to login if not authenticated.
@@ -333,11 +331,7 @@ async fn has_any_admin(db: &Database) -> cot::Result<bool> {
Ok(count > 0)
}
async fn login_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn login_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
if get_admin_name(&session).await.is_some() {
return Redirect::new(format!("/admin/leads?lang={}", lang.code())).into_response();
@@ -380,11 +374,7 @@ struct SetupForm {
password_confirm: String,
}
async fn setup_submit(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn setup_submit(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let (lang, form): (_, SetupForm) = parse_form_from_request(request).await?;
// Block if admins already exist
@@ -431,11 +421,7 @@ struct LoginForm {
password: String,
}
async fn login_submit(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn login_submit(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let (lang, form): (_, LoginForm) = parse_form_from_request(request).await?;
let login = form.login.clone();
@@ -450,9 +436,7 @@ async fn login_submit(
.as_deref()
.unwrap_or(&user.login)
.to_string();
session
.insert(SESSION_USER_ID, user.id.unwrap())
.await?;
session.insert(SESSION_USER_ID, user.id.unwrap()).await?;
session.insert(SESSION_USER_NAME, display).await?;
return Redirect::new(format!("/admin/leads?lang={}", lang.code())).into_response();
}
@@ -477,11 +461,7 @@ async fn logout(request: Request, session: Session) -> cot::Result<Response> {
// GET Handlers (protected)
// ---------------------------------------------------------------------------
async fn admin_index(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn admin_index(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -503,7 +483,9 @@ async fn admin_index(
client_name: client.map(|c| c.name.clone()).unwrap_or_default(),
client_phone: client.and_then(|c| c.phone.clone()).unwrap_or_default(),
client_address: client.and_then(|c| c.address.clone()).unwrap_or_default(),
client_color: client.and_then(|c| c.color.clone()).unwrap_or_else(|| "#7c6ed4".to_string()),
client_color: client
.and_then(|c| c.color.clone())
.unwrap_or_else(|| "#7c6ed4".to_string()),
visit: v.clone(),
}
})
@@ -520,8 +502,11 @@ async fn admin_index(
})
.map(|v| {
let cid: i64 = v.client_id.primary_key().unwrap();
let client_name = clients.iter().find(|c| c.id.unwrap() == cid)
.map(|c| c.name.clone()).unwrap_or_default();
let client_name = clients
.iter()
.find(|c| c.id.unwrap() == cid)
.map(|c| c.name.clone())
.unwrap_or_default();
RecentFeedback {
visit_id: v.id.unwrap(),
client_name,
@@ -543,11 +528,7 @@ async fn admin_index(
html_response(body, lang)
}
async fn leads_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn leads_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -570,11 +551,7 @@ async fn leads_page(
html_response(body, lang)
}
async fn clients_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn clients_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -597,10 +574,7 @@ async fn clients_page(
html_response(body, lang)
}
async fn client_new_page(
request: Request,
session: Session,
) -> cot::Result<Response> {
async fn client_new_page(request: Request, session: Session) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -642,7 +616,9 @@ async fn client_edit_page(
};
let client = match query!(Client, $id == client_id).get(&db).await? {
Some(c) => c,
None => return Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response(),
None => {
return Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response();
}
};
let t = lang.t();
let action_url = format!("/admin/clients/{}/save", client.id);
@@ -694,11 +670,7 @@ async fn client_edit_submit(
Redirect::new(format!("/admin/clients?lang={}", lang.code())).into_response()
}
async fn users_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn users_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -716,11 +688,7 @@ async fn users_page(
html_response(body, lang)
}
async fn settings_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn settings_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -750,11 +718,7 @@ struct AddUserForm {
password_confirm: String,
}
async fn add_user(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn add_user(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let (lang, form): (_, AddUserForm) = parse_form_from_request(request).await?;
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -805,13 +769,10 @@ struct SettingsForm {
telegram_bot_token: String,
telegram_chat_id: String,
contact_info: String,
pricing_info: String,
}
async fn save_settings(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let (lang, form): (_, SettingsForm) = parse_form_from_request(request).await?;
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -822,6 +783,7 @@ async fn save_settings(
("telegram_bot_token", form.telegram_bot_token),
("telegram_chat_id", form.telegram_chat_id),
("contact_info", form.contact_info),
("pricing_info", form.pricing_info),
] {
let k = key.to_string();
let existing = query!(Setting, $key == k).get(&db).await?;
@@ -996,11 +958,7 @@ struct AddLeadForm {
comment: Option<String>,
}
async fn add_lead(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn add_lead(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let (lang, form): (_, AddLeadForm) = parse_form_from_request(request).await?;
if let Err(resp) = require_auth(&session, lang).await {
return Ok(resp);
@@ -1040,11 +998,7 @@ struct AddClientForm {
color: Option<String>,
}
async fn add_client(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn add_client(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let (lang, form): (_, AddClientForm) = parse_form_from_request(request).await?;
if let Err(resp) = require_auth(&session, lang).await {
return Ok(resp);
@@ -1072,10 +1026,7 @@ async fn add_client(
// Schedule Handlers
// ---------------------------------------------------------------------------
async fn schedule_page(
request: Request,
session: Session,
) -> cot::Result<Response> {
async fn schedule_page(request: Request, session: Session) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -1159,9 +1110,7 @@ async fn schedule_events(
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_address = client.and_then(|c| c.address.as_deref()).unwrap_or("");
let client_color = client
.and_then(|c| c.color.as_deref())
.unwrap_or("#7c6ed4");
let client_color = client.and_then(|c| c.color.as_deref()).unwrap_or("#7c6ed4");
let admin_name = user
.map(|u| u.display_name.as_deref().unwrap_or(&u.login))
.unwrap_or("?");
@@ -1276,7 +1225,9 @@ async fn schedule_edit_page(
};
let visit = match query!(Visit, $id == visit_id).get(&db).await? {
Some(v) => v,
None => return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response(),
None => {
return Redirect::new(format!("/admin/schedule?lang={}", lang.code())).into_response();
}
};
let clients = query!(Client, $status == "active").all(&db).await?;
let users = query!(User, $status == "active").all(&db).await?;
@@ -1395,11 +1346,7 @@ async fn visit_set_cancel(
// Media Handlers
// ---------------------------------------------------------------------------
async fn media_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
async fn media_page(request: Request, session: Session, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
@@ -1432,10 +1379,14 @@ async fn media_page(
.map(|m| {
let cid: i64 = m.client_id.primary_key().unwrap();
let client = clients_all.iter().find(|c| c.id.unwrap() == cid);
let visit_date = m.visit_id.as_ref().and_then(|fk| {
let vid: i64 = fk.primary_key().unwrap();
visits_all.iter().find(|v| v.id.unwrap() == vid)
}).map(|v| v.visit_date.to_string());
let visit_date = m
.visit_id
.as_ref()
.and_then(|fk| {
let vid: i64 = fk.primary_key().unwrap();
visits_all.iter().find(|v| v.id.unwrap() == vid)
})
.map(|v| v.visit_date.to_string());
MediaItem {
client_name: client.map(|c| c.name.clone()).unwrap_or_default(),
visit_date,
@@ -1444,7 +1395,10 @@ async fn media_page(
})
.collect();
let active_clients = clients_all.into_iter().filter(|c| c.status == "active").collect();
let active_clients = clients_all
.into_iter()
.filter(|c| c.status == "active")
.collect();
let body = MediaTemplate {
t: lang.t(),
@@ -1476,7 +1430,10 @@ async fn media_upload_page(
let cid: i64 = visit.client_id.primary_key().unwrap();
let client = query!(Client, $id == cid).get(&db).await?;
let client_name = client.map(|c| c.name).unwrap_or_default();
let visit_label = format!("{} {}{}", visit.visit_date, visit.time_start, visit.time_end);
let visit_label = format!(
"{} {}{}",
visit.visit_date, visit.time_start, visit.time_end
);
let body = MediaUploadTemplate {
t: lang.t(),
@@ -1492,11 +1449,11 @@ async fn media_upload_page(
fn extract_boundary(request: &Request) -> Option<String> {
let ct = request.headers().get("content-type")?.to_str().ok()?;
ct.split(';')
.find_map(|part| {
let part = part.trim();
part.strip_prefix("boundary=").map(|b| b.trim_matches('"').to_string())
})
ct.split(';').find_map(|part| {
let part = part.trim();
part.strip_prefix("boundary=")
.map(|b| b.trim_matches('"').to_string())
})
}
async fn media_upload_submit(
@@ -1521,20 +1478,30 @@ async fn media_upload_submit(
let client_id: i64 = visit.client_id.primary_key().unwrap();
let bytes = request.into_body().into_bytes().await?;
let stream = futures::stream::once(async move { Result::<_, std::convert::Infallible>::Ok(bytes) });
let stream =
futures::stream::once(async move { Result::<_, std::convert::Infallible>::Ok(bytes) });
let mut multipart = multer::Multipart::new(stream, boundary);
let upload_dir = format!("uploads/{}/{}", client_id, visit_id);
tokio::fs::create_dir_all(&upload_dir).await.map_err(|e| cot::Error::internal(e.to_string()))?;
tokio::fs::create_dir_all(&upload_dir)
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
let mut caption = String::new();
let mut saved_files: Vec<(String, String)> = Vec::new(); // (path, file_type)
while let Some(field) = multipart.next_field().await.map_err(|e| cot::Error::internal(e.to_string()))? {
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?
{
let field_name = field.name().unwrap_or("").to_string();
if field_name == "caption" {
caption = field.text().await.map_err(|e| cot::Error::internal(e.to_string()))?;
caption = field
.text()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
continue;
}
@@ -1559,17 +1526,26 @@ async fn media_upload_submit(
let file_id = uuid::Uuid::new_v4();
let file_path = format!("{}/{}.{}", upload_dir, file_id, ext);
let data = field.bytes().await.map_err(|e| cot::Error::internal(e.to_string()))?;
let data = field
.bytes()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
if data.is_empty() {
continue;
}
tokio::fs::write(&file_path, &data).await.map_err(|e| cot::Error::internal(e.to_string()))?;
tokio::fs::write(&file_path, &data)
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
saved_files.push((file_path, file_type.to_string()));
}
}
let caption_opt = if caption.trim().is_empty() { None } else { Some(caption) };
let caption_opt = if caption.trim().is_empty() {
None
} else {
Some(caption)
};
for (path, ftype) in saved_files {
let mut media = Media {
@@ -1638,7 +1614,224 @@ async fn serve_upload(
};
let body = cot::Body::fixed(data);
let mut resp = Response::new(body);
resp.headers_mut().insert("content-type", content_type.parse().unwrap());
resp.headers_mut()
.insert("content-type", content_type.parse().unwrap());
Ok(resp)
}
Err(_) => Html::new("404").into_response(),
}
}
// ---------------------------------------------------------------------------
// Testimonials
// ---------------------------------------------------------------------------
#[derive(Debug, Template)]
#[template(path = "admin/testimonials.html")]
struct TestimonialsTemplate<'a> {
t: &'a Translations,
lang: Lang,
admin_name: String,
testimonials: Vec<Testimonial>,
}
async fn testimonials_page(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
let lang = detect_lang(&request);
let admin_name = match require_auth(&session, lang).await {
Ok(name) => name,
Err(resp) => return Ok(resp),
};
let mut testimonials = Testimonial::objects().all(&db).await?;
testimonials.sort_by(|a, b| {
a.sort_order
.cmp(&b.sort_order)
.then(b.id.unwrap().cmp(&a.id.unwrap()))
});
let body = TestimonialsTemplate {
t: lang.t(),
lang,
admin_name,
testimonials,
}
.render()?;
html_response(body, lang)
}
async fn testimonial_add(
request: Request,
session: Session,
db: Database,
) -> cot::Result<Response> {
let lang = detect_lang(&request);
if let Err(resp) = require_auth(&session, lang).await {
return Ok(resp);
}
let boundary = extract_boundary(&request)
.ok_or_else(|| cot::Error::internal("missing multipart boundary"))?;
let bytes = request.into_body().into_bytes().await?;
let stream =
futures::stream::once(async move { Result::<_, std::convert::Infallible>::Ok(bytes) });
let mut multipart = multer::Multipart::new(stream, boundary);
let mut text = String::new();
let mut author_note = String::new();
let mut image_path: Option<String> = None;
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?
{
let field_name = field.name().unwrap_or("").to_string();
match field_name.as_str() {
"text" => {
text = field
.text()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
}
"author_note" => {
author_note = field
.text()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
}
"image" => {
let original_name = field.file_name().unwrap_or("").to_string();
if original_name.is_empty() {
continue;
}
let ext = original_name
.rsplit('.')
.next()
.unwrap_or("bin")
.to_lowercase();
match ext.as_str() {
"jpg" | "jpeg" | "png" | "webp" | "heic" | "heif" => {}
_ => continue,
}
let data = field
.bytes()
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
if data.is_empty() {
continue;
}
let upload_dir = "uploads/testimonials";
tokio::fs::create_dir_all(upload_dir)
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
let file_id = uuid::Uuid::new_v4();
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
tokio::fs::write(&path, &data)
.await
.map_err(|e| cot::Error::internal(e.to_string()))?;
image_path = Some(path);
}
_ => {}
}
}
if !text.trim().is_empty() {
let max_order: i32 = Testimonial::objects()
.all(&db)
.await?
.iter()
.map(|t| t.sort_order)
.max()
.unwrap_or(0);
let mut testimonial = Testimonial {
id: Auto::auto(),
text: text.trim().to_string(),
author_note: if author_note.trim().is_empty() {
None
} else {
Some(author_note.trim().to_string())
},
image_path,
status: "active".to_string(),
sort_order: max_order + 1,
created_at: now(),
};
testimonial.save(&db).await?;
}
Redirect::new(format!("/admin/testimonials?lang={}", lang.code())).into_response()
}
async fn testimonial_toggle(
request: Request,
session: Session,
db: Database,
Path(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 t) = query!(Testimonial, $id == id).get(&db).await? {
t.status = if t.status == "active" {
"hidden".to_string()
} else {
"active".to_string()
};
t.save(&db).await?;
}
Redirect::new(format!("/admin/testimonials?lang={}", lang.code())).into_response()
}
async fn testimonial_delete(
request: Request,
session: Session,
db: Database,
Path(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 t) = query!(Testimonial, $id == id).get(&db).await? {
t.status = "deleted".to_string();
t.save(&db).await?;
}
Redirect::new(format!("/admin/testimonials?lang={}", lang.code())).into_response()
}
/// Serve testimonial images (public, no auth needed for landing page).
async fn serve_testimonial_image(
_request: Request,
db: Database,
Path(id): Path<i64>,
) -> cot::Result<Response> {
let testimonial = match query!(Testimonial, $id == id).get(&db).await? {
Some(t) => t,
None => return Html::new("404").into_response(),
};
let path = match &testimonial.image_path {
Some(p) => p.clone(),
None => return Html::new("404").into_response(),
};
match tokio::fs::read(&path).await {
Ok(data) => {
let content_type = match path.rsplit('.').next().unwrap_or("") {
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"webp" => "image/webp",
"heic" | "heif" => "image/heic",
_ => "application/octet-stream",
};
let body = cot::Body::fixed(data);
let mut resp = Response::new(body);
resp.headers_mut()
.insert("content-type", content_type.parse().unwrap());
Ok(resp)
}
Err(_) => Html::new("404").into_response(),
@@ -1675,19 +1868,55 @@ pub fn admin_router() -> Router {
Route::with_handler_and_name("/clients", clients_page, "admin-clients"),
Route::with_handler_and_name("/clients/new", client_new_page, "admin-client-new"),
Route::with_handler_and_name("/clients/add", add_client, "admin-client-add"),
Route::with_handler_and_name("/clients/{client_id}/edit", client_edit_page, "admin-client-edit"),
Route::with_handler_and_name("/clients/{client_id}/save", client_edit_submit, "admin-client-edit-submit"),
Route::with_handler_and_name("/clients/{client_id}/archive", client_archive, "admin-client-archive"),
Route::with_handler_and_name("/clients/{client_id}/activate", client_activate, "admin-client-activate"),
Route::with_handler_and_name(
"/clients/{client_id}/edit",
client_edit_page,
"admin-client-edit",
),
Route::with_handler_and_name(
"/clients/{client_id}/save",
client_edit_submit,
"admin-client-edit-submit",
),
Route::with_handler_and_name(
"/clients/{client_id}/archive",
client_archive,
"admin-client-archive",
),
Route::with_handler_and_name(
"/clients/{client_id}/activate",
client_activate,
"admin-client-activate",
),
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/events", schedule_events, "admin-schedule-events"),
Route::with_handler_and_name("/schedule/create", schedule_create, "admin-schedule-create"),
Route::with_handler_and_name("/schedule/{visit_id}/edit", schedule_edit_page, "admin-visit-edit"),
Route::with_handler_and_name("/schedule/{visit_id}/save", schedule_edit_submit, "admin-visit-save"),
Route::with_handler_and_name("/schedule/{visit_id}/delete", visit_delete, "admin-visit-delete"),
Route::with_handler_and_name("/schedule/{visit_id}/done", visit_set_done, "admin-visit-done"),
Route::with_handler_and_name("/schedule/{visit_id}/cancel", visit_set_cancel, "admin-visit-cancel"),
Route::with_handler_and_name(
"/schedule/{visit_id}/edit",
schedule_edit_page,
"admin-visit-edit",
),
Route::with_handler_and_name(
"/schedule/{visit_id}/save",
schedule_edit_submit,
"admin-visit-save",
),
Route::with_handler_and_name(
"/schedule/{visit_id}/delete",
visit_delete,
"admin-visit-delete",
),
Route::with_handler_and_name(
"/schedule/{visit_id}/done",
visit_set_done,
"admin-visit-done",
),
Route::with_handler_and_name(
"/schedule/{visit_id}/cancel",
visit_set_cancel,
"admin-visit-cancel",
),
Route::with_handler_and_name("/users", users_page, "admin-users"),
Route::with_handler_and_name("/users/add", add_user, "admin-user-add"),
Route::with_handler_and_name(
@@ -1701,10 +1930,43 @@ pub fn admin_router() -> Router {
"admin-user-activate",
),
Route::with_handler_and_name("/media", media_page, "admin-media"),
Route::with_handler_and_name("/media/{visit_id}/upload", media_upload_page, "admin-media-upload"),
Route::with_handler_and_name("/media/{visit_id}/upload/submit", media_upload_submit, "admin-media-upload-submit"),
Route::with_handler_and_name("/media/{media_id}/delete", media_delete, "admin-media-delete"),
Route::with_handler_and_name(
"/media/{visit_id}/upload",
media_upload_page,
"admin-media-upload",
),
Route::with_handler_and_name(
"/media/{visit_id}/upload/submit",
media_upload_submit,
"admin-media-upload-submit",
),
Route::with_handler_and_name(
"/media/{media_id}/delete",
media_delete,
"admin-media-delete",
),
Route::with_handler_and_name("/uploads/{media_id}", serve_upload, "admin-uploads"),
Route::with_handler_and_name("/testimonials", testimonials_page, "admin-testimonials"),
Route::with_handler_and_name(
"/testimonials/add",
testimonial_add,
"admin-testimonial-add",
),
Route::with_handler_and_name(
"/testimonials/{id}/toggle",
testimonial_toggle,
"admin-testimonial-toggle",
),
Route::with_handler_and_name(
"/testimonials/{id}/delete",
testimonial_delete,
"admin-testimonial-delete",
),
Route::with_handler_and_name(
"/testimonials/{id}/image",
serve_testimonial_image,
"admin-testimonial-image",
),
Route::with_handler_and_name("/settings", settings_page, "admin-settings-get"),
Route::with_handler_and_name("/settings/save", save_settings, "admin-settings-save"),
])