P2 slice 6: the viewer

`wanderbooks gui [BOOK]` finds the data root, reads settings from the
journal, the vault of the book, presence through ops/, Save a copy /
Open on every document, Save the W-9, the Export menu, and colour
schemes. One builder for the CPA package and the 1099 worksheet
(ledger-package), shared by the server and the viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-20 11:14:21 -06:00
1 parent b26c099a68
commit 5bc0633cdd
77 files changed
+5833 -2427

No files matched your search

+11 -6
View File
@@ -147,13 +147,18 @@ pub fn show(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|_| {},
|ui| {
let kept = b.ledger.settings();
// Where the settings shown come from: a settings
// file only for an older books dir that keeps none
// in its journal and has one (data::settings).
let from_file =
!kept.is_journaled() && crate::data::legacy_toml(&b.data_dir).is_some();
// The currency is always known: facts are the rest.
if c.profile.iter().all(|(k, _)| k == "base_currency") {
ui.label(
RichText::new(if kept.is_journaled() {
"No company facts set yet: the agent records them as you give them."
} else {
RichText::new(if from_file {
"No company facts in the settings file yet: the agent records them as you give them."
} else {
"No company facts set yet: the agent records them as you give them."
})
.color(p.muted),
);
@@ -177,10 +182,10 @@ pub fn show(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
kv(ui, "Time zone", z);
}
ui.label(
RichText::new(if kept.is_journaled() {
"Kept in the books: the agent changes settings, with who decided and when."
} else {
RichText::new(if from_file {
"Read from the settings file: the agent can move it into the books."
} else {
"Kept in the books: the agent changes settings, with who decided and when."
})
.font(theme::regular(12.0))
.color(p.muted),
+40
View File
@@ -253,6 +253,8 @@ fn detail(ui: &mut Ui, b: &Books, a: &Attachment, shell: &mut Shell<'_>) {
RichText::new(&a.file_name).font(theme::bold(20.0)),
&a.file_name,
);
ui.add_space(6.0);
file_actions(ui, b, a);
ui.add_space(8.0);
egui::Grid::new(("docfacts", a.id))
.num_columns(2)
@@ -324,6 +326,44 @@ fn detail(ui: &mut Ui, b: &Books, a: &Attachment, shell: &mut Shell<'_>) {
});
}
/// "Save a copy" and "Open" (plan 3.6): a copy in the Downloads folder,
/// never over a file there, opened with the system's app on "Open". The
/// books are only read. What came of it shows under the buttons.
fn file_actions(ui: &mut Ui, b: &Books, a: &Attachment) {
let p = pal(ui.ctx());
let said = ui.id().with(("file-said", a.id));
ui.horizontal(|ui| {
let save = ui
.button(format!("{} Save a copy", icon::FILE_ARROW_UP))
.on_hover_text("A copy in your Downloads folder");
let open = ui
.button(format!("{} Open", icon::ARROW_SQUARE_OUT))
.on_hover_text("A copy in your Downloads folder, opened with its app");
if save.clicked() || open.clicked() {
let result = save_copy(b, a, open.clicked());
ui.data_mut(|d| d.insert_temp(said, result));
}
});
if let Some(result) = ui.data(|d| d.get_temp::<Result<String, String>>(said)) {
let (text, color) = match result {
Ok(path) => (format!("Saved {path}"), p.muted),
Err(why) => (format!("Couldn't save a copy: {why}"), p.warning),
};
ui.add(egui::Label::new(RichText::new(text).size(12.0).color(color)).wrap());
}
}
/// A copy of `a` in the Downloads folder (opened if `open`): its path.
fn save_copy(b: &Books, a: &Attachment, open: bool) -> Result<String, String> {
let bytes = b.attachment_bytes(a)?;
let (stem, ext) = crate::export::split_name(&a.file_name);
let path = crate::export::save(stem, ext, &bytes)?;
if open {
crate::export::open(&path);
}
Ok(path.display().to_string())
}
/// The icon for a subject's kind.
pub fn subject_icon(s: Subject) -> &'static str {
match s {
+31
View File
@@ -1053,6 +1053,9 @@ fn w9_card(ui: &mut Ui, b: &Books, party: &Party) {
h,
);
ui.end_row();
k(ui, "The form");
w9_save(ui, b, party.id);
ui.end_row();
}
});
},
@@ -1563,6 +1566,34 @@ fn months_card(ui: &mut Ui, b: &Books, id: PartyId, span: (NaiveDate, NaiveDate)
);
}
/// "Save the W-9": the signed form from the vault, a copy in the Downloads
/// folder (D-29: the viewer hands out what the agent can), never over a
/// file there; what came of it shows beside the button.
fn w9_save(ui: &mut Ui, b: &Books, party: PartyId) {
let p = pal(ui.ctx());
let said = ui.id().with(("w9-said", party));
ui.vertical(|ui| {
if ui
.button("Save the W-9")
.on_hover_text("A copy of the signed form in your Downloads folder")
.clicked()
{
let result = b.w9_file(party).and_then(|f| {
let (stem, ext) = crate::export::split_name(&f.file_name);
crate::export::save(stem, ext, &f.bytes).map(|path| path.display().to_string())
});
ui.data_mut(|d| d.insert_temp(said, result));
}
if let Some(result) = ui.data(|d| d.get_temp::<Result<String, String>>(said)) {
let (text, color) = match result {
Ok(path) => (format!("Saved {path}"), p.muted),
Err(why) => (why, p.warning),
};
ui.add(egui::Label::new(RichText::new(text).size(12.0).color(color)).wrap());
}
});
}
#[cfg(test)]
mod tests {
use super::*;
-2
View File
@@ -2191,7 +2191,6 @@ mod tests {
crate::demo::build(&dir, today).unwrap();
let b = crate::data::load_once(&crate::data::Source {
data_dir: dir.join("books-data"),
books_toml: Some(dir.join("wanderbooks.toml")),
today: Some(today),
})
.unwrap();
@@ -2257,7 +2256,6 @@ mod tests {
crate::demo::build(&dir, today).unwrap();
let b = crate::data::load_once(&crate::data::Source {
data_dir: dir.join("books-data"),
books_toml: Some(dir.join("wanderbooks.toml")),
today: Some(today),
})
.unwrap();