1use crate::backend::{QtThread, Shared, json, load_config, say, tr};
8use serde::{Deserialize, Serialize};
9use sioul_core::papers::{Kind, Paper, Standing, Wallet};
10use sioul_core::tasks::{Link, TaskEdit};
11use std::path::Path;
12use std::sync::Arc;
13
14fn wallet() -> Result<Wallet, String> {
15 let root = load_config().notes_root_path().ok_or_else(|| tr().text("papers-no-store", None))?;
16 Wallet::load(&root)
17}
18
19#[derive(Serialize)]
20struct PaperView {
21 id: String,
22 kind: &'static str,
23 kind_label: String,
24 title: String,
25 holder: String,
26 notes: String,
27 file: String,
29 file_name: String,
30 file_there: bool,
32 issued: String,
33 until: String,
34 standing: &'static str,
36 line: String,
37 renewal: String,
38 can_renew: bool,
40}
41
42#[derive(Serialize)]
43struct Family {
44 id: &'static str,
45 label: String,
46 papers: Vec<PaperView>,
47}
48
49#[derive(Serialize)]
50struct Choice {
51 id: &'static str,
52 label: String,
53}
54
55#[derive(Serialize, Default)]
56struct PapersView {
57 store: bool,
59 problem: String,
60 kinds: Vec<Choice>,
61 families: Vec<Family>,
62}
63
64fn view_of(wallet: &Wallet, paper: &Paper, today: jiff::civil::Date) -> PaperView {
65 let file = wallet.file_path(paper);
66 let date = |d: jiff::civil::Date| tr().day_in(d, today);
67 let (standing, line) = match paper.standing(today) {
68 Standing::Valid(d) => ("valid", say("paper-valid", &[("date", date(d))])),
69 Standing::Renew(d) => ("renew", say("paper-renew", &[("date", date(d))])),
70 Standing::Ended(d) => ("ended", say("paper-ended", &[("date", date(d))])),
71 Standing::Fresh(d) => ("fresh", say("paper-fresh", &[("date", date(d))])),
72 Standing::Old(d) => ("old", say("paper-old", &[("date", date(d))])),
73 Standing::Undated => ("undated", String::new()),
74 };
75 PaperView {
76 id: paper.id.clone(),
77 kind: paper.kind.id(),
78 kind_label: tr().text(&format!("paper-kind-{}", paper.kind.id()), None),
79 title: paper.title.clone(),
80 holder: paper.holder.clone(),
81 notes: paper.notes.clone(),
82 file_name: file.as_ref().and_then(|f| f.file_name()).map(|n| n.to_string_lossy().to_string()).unwrap_or_default(),
83 file_there: file.as_ref().is_some_and(|f| f.is_file()),
84 file: file.map(|f| f.display().to_string()).unwrap_or_default(),
85 issued: paper.issued.map(|d| d.to_string()).unwrap_or_default(),
86 until: paper.until.map(|d| d.to_string()).unwrap_or_default(),
87 standing,
88 line,
89 renewal: paper.renewal.clone(),
90 can_renew: paper.renewal.is_empty() && paper.renew_from().is_some(),
91 }
92}
93
94pub(crate) fn view() -> String {
96 let kinds = Kind::ALL.iter().map(|k| Choice { id: k.id(), label: tr().text(&format!("paper-kind-{}", k.id()), None) }).collect();
97 let wallet = match wallet() {
98 Ok(wallet) => wallet,
99 Err(problem) => return json(&PapersView { store: load_config().notes_root_path().is_some(), problem, kinds, families: Vec::new() }),
100 };
101 let today = jiff::Zoned::now().date();
102 let mut families: Vec<Family> = Vec::new();
103 for family in ["identity", "health", "home", "money", "warranty", "other"] {
104 let mut papers: Vec<&Paper> = wallet.papers.iter().filter(|p| p.kind.family() == family).collect();
105 papers.sort_by(|a, b| (a.kind.id(), &a.holder, &a.title).cmp(&(b.kind.id(), &b.holder, &b.title)));
106 if !papers.is_empty() {
107 families.push(Family { id: family, label: tr().text(&format!("paper-family-{family}"), None), papers: papers.into_iter().map(|p| view_of(&wallet, p, today)).collect() });
108 }
109 }
110 json(&PapersView { store: true, problem: String::new(), kinds, families })
111}
112
113#[derive(Deserialize)]
115struct PaperEdit {
116 #[serde(default)]
117 kind: String,
118 #[serde(default)]
119 title: String,
120 #[serde(default)]
122 file: String,
123 #[serde(default)]
124 issued: String,
125 #[serde(default)]
126 until: String,
127 #[serde(default)]
128 holder: String,
129 #[serde(default)]
130 notes: String,
131}
132
133pub(crate) fn save(id: &str, edit: &str) -> String {
135 let edit: PaperEdit = match serde_json::from_str(edit) {
136 Ok(edit) => edit,
137 Err(e) => return e.to_string(),
138 };
139 let wallet = match wallet() {
140 Ok(wallet) => wallet,
141 Err(e) => return e,
142 };
143 let title = edit.title.trim().to_string();
144 if title.is_empty() {
145 return tr().text("papers-no-title", None);
146 }
147 let date = |text: &str| -> Result<Option<jiff::civil::Date>, String> {
148 let text = text.trim();
149 if text.is_empty() { Ok(None) } else { text.parse().map(Some).map_err(|_| say("papers-bad-date", &[("date", text.to_string())])) }
150 };
151 let (issued, until) = match (date(&edit.issued), date(&edit.until)) {
152 (Ok(issued), Ok(until)) => (issued, until),
153 (Err(e), _) | (_, Err(e)) => return e,
154 };
155 let kind = Kind::of(&edit.kind);
156 let old = wallet.get(id).cloned().unwrap_or_default();
157 let file = if edit.file.trim().is_empty() {
159 String::new()
160 } else {
161 let path = crate::backend::local_path(edit.file.trim());
162 let inside = wallet.file_path(&old).is_some_and(|f| f == path) || path.starts_with(wallet.root.join(sioul_core::papers::FOLDER));
163 if inside {
164 path.strip_prefix(&wallet.root).map_or_else(|_| path.display().to_string(), |p| p.to_string_lossy().replace('\\', "/"))
165 } else if path.is_file() {
166 let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
167 match wallet.keep_file(&path, &name) {
168 Ok(kept) => kept,
169 Err(e) => return e,
170 }
171 } else {
172 return say("papers-no-file", &[("path", path.display().to_string())]);
173 }
174 };
175 let until = until.or_else(|| issued.and_then(|d| sioul_core::papers::usual_end(kind, d)));
177 Wallet::change(&wallet.root, |wallet| {
179 let old = wallet.get(id).cloned().unwrap_or_default();
180 let paper = Paper {
181 id: old.id.clone(),
182 kind,
183 title,
184 file,
185 issued,
186 until,
187 holder: edit.holder.trim().to_string(),
188 notes: edit.notes.trim().to_string(),
189 renewal: if old.until == until { old.renewal.clone() } else { String::new() },
191 added: old.added.or_else(|| Some(jiff::Zoned::now().date())),
192 };
193 wallet.put(paper);
194 })
195 .err()
196 .unwrap_or_default()
197}
198
199pub(crate) fn remove(id: &str) -> String {
201 let Some(root) = load_config().notes_root_path() else { return tr().text("papers-no-store", None) };
202 Wallet::change(&root, |wallet| wallet.remove(id)).err().unwrap_or_default()
203}
204
205pub(crate) fn plan_renewal(qt: &QtThread, shared: &Arc<Shared>, id: &str) -> String {
208 let wallet = match wallet() {
209 Ok(wallet) => wallet,
210 Err(e) => return e,
211 };
212 let Some(paper) = wallet.get(id).cloned() else { return tr().text("papers-gone", None) };
213 let (Some(from), Some(until)) = (paper.renew_from(), paper.until) else { return String::new() };
214 let today = jiff::Zoned::now().date();
215 let steps = format!("paper-steps-{}", paper.kind.id());
216 let notes = tr().text(&steps, None);
217 let edit = TaskEdit {
218 title: say("paper-renew-task", &[("title", paper.title.clone())]),
219 notes: if notes == steps { String::new() } else { notes },
220 start: from.max(today).to_string(),
221 due: until.to_string(),
222 estimate: 30,
223 links: vec![Link { uri: sioul_core::links::paper_uri(&paper.id), label: paper.title.clone(), rel: "related".into() }],
224 ..TaskEdit::default()
225 };
226 let list = sioul_core::tasks::default_list().map(|l| format!("{}/{}", l.account, l.id));
227 let made = match list {
228 Some(list) => crate::work::create_task(qt, shared, &edit, &list),
229 None => crate::work::local_task(qt, shared, &tr().text("papers-list", None), &edit),
230 };
231 match made {
232 Ok(uid) => Wallet::change(&wallet.root, |wallet| {
234 if let Some(mut paper) = wallet.get(id).cloned() {
235 paper.renewal = uid;
236 wallet.put(paper);
237 }
238 })
239 .err()
240 .unwrap_or_default(),
241 Err(e) => e,
242 }
243}
244
245pub(crate) fn keep_checked(file: &Path, name: &str, subject: &str) -> Result<(String, String, &'static str), String> {
248 let wallet = wallet()?;
249 let kept = wallet.keep_file(file, name)?;
250 let title = Path::new(name).file_stem().map(|s| s.to_string_lossy().replace(['_', '-'], " ")).unwrap_or_default();
251 let looked = sioul_core::words::Words::of(&load_config());
252 let kind = Kind::guess(&looked, name).or_else(|| Kind::guess(&looked, subject)).unwrap_or_default();
253 Ok((wallet.root.join(&kept).display().to_string(), title, kind.id()))
254}