wip: pause checkpoint — P6 feature complete, gate interrupted (not for merge)

This commit is contained in:
swaits committed 2026-10-27 14:41:34 -06:00
1 parent cc4f104f06
commit ce36640874
68 files changed
+4592 -374

No files matched your search

+102 -9
View File
@@ -18,6 +18,9 @@ use ledger_store::{Attachment, Subject};
#[derive(Default)]
pub struct State {
pub category: Option<String>,
/// One subject's files only (plan P6 step 6: grouped by category and
/// subject).
pub subject: Option<Subject>,
pub show_withdrawn: bool,
pub query: String,
}
@@ -72,6 +75,38 @@ fn grid(ui: &mut Ui, b: &Books, selected: Option<u64>, shell: &mut Shell<'_>) {
st.category = Some(c.clone());
}
}
// One subject's files (plan P6 step 6): every subject any
// document is evidence of, and the company's own.
let mut subjects: Vec<Subject> = Vec::new();
for a in &b.attachments {
if let Some(e) = a.entry_id {
subjects.push(Subject::Entry(e));
}
subjects.extend(b.links_of(a.id));
}
subjects.sort();
subjects.dedup();
egui::ComboBox::from_id_salt("documents-subject")
.selected_text(match st.subject {
None => "Any subject".to_string(),
Some(s) => b.subject_label(s),
})
.show_ui(ui, |ui| {
if ui
.selectable_label(st.subject.is_none(), "Any subject")
.clicked()
{
st.subject = None;
}
for s in subjects {
if ui
.selectable_label(st.subject == Some(s), b.subject_label(s))
.clicked()
{
st.subject = Some(s);
}
}
});
ui.checkbox(&mut st.show_withdrawn, "Show withdrawn");
});
ui.add_space(12.0);
@@ -82,6 +117,12 @@ fn grid(ui: &mut Ui, b: &Books, selected: Option<u64>, shell: &mut Shell<'_>) {
.rev()
.filter(|a| st.show_withdrawn || a.withdrawn_at.is_none())
.filter(|a| st.category.as_ref().is_none_or(|c| &a.category == c))
.filter(|a| match st.subject {
None => true,
Some(s) => {
a.entry_id.map(Subject::Entry) == Some(s) || b.links_of(a.id).contains(&s)
}
})
.filter(|a| {
q.is_empty()
|| a.file_name.to_lowercase().contains(&q)
@@ -101,16 +142,34 @@ fn grid(ui: &mut Ui, b: &Books, selected: Option<u64>, shell: &mut Shell<'_>) {
let tile_w =
((ui.available_width() - 16.0 * (per_row as f32 - 1.0)) / per_row as f32).min(280.0);
let mut go = None;
for row in docs.chunks(per_row) {
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = 16.0;
for a in row {
if tile(ui, b, a, tile_w, selected == Some(a.id), shell).clicked() {
go = Some(a.id);
// Grouped by category unless one is chosen (plan P6 step 6): a
// heading, then its files; the subject filter narrows each group.
let by_category = st.category.is_none();
let mut groups: std::collections::BTreeMap<String, Vec<&Attachment>> =
std::collections::BTreeMap::new();
for a in docs {
groups.entry(a.category.clone()).or_default().push(a);
}
for (category, files) in groups {
if by_category {
ui.label(
RichText::new(fmt::label(&category))
.font(theme::semibold(12.5))
.color(pal(ui.ctx()).muted),
);
ui.add_space(6.0);
}
for row in files.chunks(per_row) {
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = 16.0;
for a in row {
if tile(ui, b, a, tile_w, selected == Some(a.id), shell).clicked() {
go = Some(a.id);
}
}
}
});
ui.add_space(16.0);
});
ui.add_space(16.0);
}
}
if let Some(id) = go {
shell.go(Route::Document(id));
@@ -625,6 +684,40 @@ mod tests {
use super::*;
use crate::test_support::{Build, d, find, render};
/// The list groups by category and offers one subject's files (plan P6
/// step 6).
#[test]
fn documents_group_by_category_and_subject() {
let mut b = Build::new("documents-groups");
let (cash, capital) = (b.cash, b.capital);
let e = b.post(d(2026, 1, 2), cash, capital, 100_000);
let (accounting, _, _) =
b.s.file_document(
"accounting",
"ledger.pdf",
"application/pdf",
b"%PDF ledger",
None,
)
.unwrap();
b.s.link_attachment(accounting, ledger_store::Subject::Entry(e))
.unwrap();
b.s.file_document(
"notices",
"notice.pdf",
"application/pdf",
b"%PDF notice",
None,
)
.unwrap();
let (_dir, books) = b.books();
let (texts, _) = render(books, Route::Documents, |_| {});
for group in ["Receipts", "Accounting", "Notices"] {
find(&texts, group);
}
find(&texts, "Any subject");
}
/// A document's page lists what it is linked to: an entry with its date
/// and amount.
#[test]
+55 -1
View File
@@ -391,9 +391,63 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
}
"attachment.withdrawn" => ev(
icon::ARCHIVE,
format!("Withdrew {}: {}", attachment_name(b, att), s(p, "reason")),
format!(
"Withdrew {}{}",
attachment_name(b, att),
match p.get("duplicate_of").and_then(|v| v.as_u64()) {
Some(first) => format!(", a duplicate of #{first}: {}", s(p, "reason")),
None => format!(": {}", s(p, "reason")),
}
),
Some(Route::Document(att)),
),
"attachment.text_extracted" => ev(
icon::FILE_TEXT,
format!(
"Read {}: {}",
attachment_name(b, att),
match s(p, "status") {
"ok" => "its text".to_string(),
"none" => "no text to read".to_string(),
_ => "the reader failed".to_string(),
}
),
Some(Route::Document(att)),
),
"attachment.annotated" => ev(
icon::PENCIL_SIMPLE,
match p
.get("fields")
.and_then(|f| f.get("receipt_number"))
.and_then(|v| v.as_u64())
{
Some(n) => format!("Annotated {} as receipt {n}", attachment_name(b, att)),
None => format!("Annotated {}", attachment_name(b, att)),
},
Some(Route::Document(att)),
),
"entry.settles" => {
let settles: Vec<String> = p
.get("settles")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_u64())
.map(|e| format!("#{e}"))
.collect()
})
.unwrap_or_default();
let text = if settles.is_empty() {
format!("Cleared what #{} settles", entry_id.unwrap_or(0))
} else {
format!(
"Entry #{} settles {}",
entry_id.unwrap_or(0),
settles.join(", ")
)
};
ev(icon::CHECK, text, entry_id.map(Route::Entry))
}
"account.added" => ev(
icon::PLUS_CIRCLE,
format!("Added account {} {}", s(p, "code"), s(p, "name")),