1use askama::Template;
2use axum::{
3 Extension, Json,
4 extract::{Path, State},
5 http::StatusCode,
6 response::IntoResponse,
7};
8use serde::Deserialize;
9use std::sync::Arc;
10use uuid::Uuid;
11
12use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
13
14fn fail(message: &str, err: impl std::fmt::Debug) -> (StatusCode, Json<serde_json::Value>) {
15 log::error!("{message}: {err:?}");
16 (
17 StatusCode::INTERNAL_SERVER_ERROR,
18 Json(serde_json::json!({ "status": "fail", "message": message })),
19 )
20}
21
22fn validate_source(source: &str) -> Result<(), (StatusCode, Json<serde_json::Value>)> {
26 rpc::compile_template(source).map(|_| ()).map_err(|e| {
27 (
28 StatusCode::BAD_REQUEST,
29 Json(serde_json::json!({
30 "status": "fail",
31 "message": format!("Template does not compile: {e}"),
32 })),
33 )
34 })
35}
36
37#[derive(Template)]
38#[template(path = "pages/template/create.html")]
39struct TemplateCreatePage;
40
41pub async fn template_create_page() -> impl IntoResponse {
42 HtmlTemplate(TemplateCreatePage {})
43}
44
45#[derive(Deserialize)]
46pub struct TemplateCreateForm {
47 name: Option<String>,
48 source: String,
49}
50
51pub async fn create_template(
52 State(_data): State<Arc<AppState>>,
53 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
54 Json(form): Json<TemplateCreateForm>,
55) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
56 validate_source(&form.source)?;
57
58 let server_user = server::user::User {
59 id: jwt_auth.user.id,
60 };
61 let name = form.name.filter(|n| !n.trim().is_empty());
62 let id = server_user
63 .create_template(form.source, name)
64 .await
65 .map_err(|e| fail("Failed to create template", e))?;
66
67 Ok(format!("{}: {}", t!("Template created"), id))
68}
69
70#[derive(Deserialize)]
71pub struct TemplateTestForm {
72 source: String,
73}
74
75pub async fn test_template(
81 State(_data): State<Arc<AppState>>,
82 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
83 Json(form): Json<TemplateTestForm>,
84) -> impl IntoResponse {
85 if let Err(e) = rpc::compile_template(&form.source) {
86 return Json(serde_json::json!({
87 "ok": false,
88 "stage": "compile",
89 "message": format!("{e}"),
90 }));
91 }
92
93 let ctx = rpc::ScriptCtx::new(jwt_auth.user.id);
94 let draft = match rpc::render_template(&ctx, &form.source).await {
95 Ok(d) => d,
96 Err(e) => {
97 return Json(serde_json::json!({
98 "ok": false,
99 "stage": "render",
100 "message": format!("{e}"),
101 }));
102 }
103 };
104
105 let splits = draft.splits.len();
106 let tags = draft.tags.len();
107 let note = draft.note.clone();
108 let date = draft.date.clone();
109 let prefillable =
110 super::super::transaction::create::prefill::draft_to_prefilled(draft).is_some();
111
112 Json(serde_json::json!({
113 "ok": true,
114 "stage": "render",
115 "prefillable": prefillable,
116 "note": note,
117 "date": date,
118 "splits": splits,
119 "tags": tags,
120 "message": if prefillable {
121 format!("Renders OK: {splits} split(s), {tags} tag(s). Ready to pre-fill the create form.")
122 } else {
123 format!("Renders OK ({splits} split(s)), but the draft can't pre-fill the form: \
124 splits must be consecutive from→to pairs with exact, balancing amounts. \
125 It will still save; the create form just won't auto-fill.")
126 },
127 }))
128}
129
130#[derive(Template)]
131#[template(path = "pages/template/edit.html")]
132struct TemplateEditPage {
133 id: Uuid,
134 name: Option<String>,
135 source: String,
136}
137
138pub async fn template_edit_page(
139 State(_data): State<Arc<AppState>>,
140 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
141 Path(id): Path<Uuid>,
142) -> Result<impl IntoResponse, (StatusCode, String)> {
143 let server_user = server::user::User {
144 id: jwt_auth.user.id,
145 };
146 let template = server_user
147 .get_template(id)
148 .await
149 .map_err(|e| (StatusCode::NOT_FOUND, format!("Template not found: {e:?}")))?;
150
151 Ok(HtmlTemplate(TemplateEditPage {
152 id: template.id,
153 name: template.name,
154 source: template.source,
155 }))
156}
157
158#[derive(Deserialize)]
159pub struct TemplateEditForm {
160 id: Uuid,
161 name: Option<String>,
162 source: String,
163}
164
165pub async fn edit_template(
166 State(_data): State<Arc<AppState>>,
167 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
168 Json(form): Json<TemplateEditForm>,
169) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
170 validate_source(&form.source)?;
171
172 let server_user = server::user::User {
173 id: jwt_auth.user.id,
174 };
175 server_user
176 .update_template_source(form.id, form.source)
177 .await
178 .map_err(|e| fail("Failed to update template source", e))?;
179 server_user
180 .update_template_name(form.id, form.name.filter(|n| !n.trim().is_empty()))
181 .await
182 .map_err(|e| fail("Failed to update template name", e))?;
183
184 Ok(format!("{}: {}", t!("Template updated"), form.id))
185}
186
187pub async fn delete_template(
188 State(_data): State<Arc<AppState>>,
189 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
190 Path(id): Path<Uuid>,
191) -> Result<impl IntoResponse, (StatusCode, String)> {
192 let server_user = server::user::User {
193 id: jwt_auth.user.id,
194 };
195 server_user
196 .delete_template(id)
197 .await
198 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
199
200 Ok(format!("{}: {}", t!("Template deleted"), id))
201}