Parties, attachment links, statements, reconciliations (store), TIN vault crate
Parties: one directory for vendors, contractors, customers, employees, members, platforms, agencies, banks, landlords (ledger-core parties.rs). W-9/W-4 facts without the number (last four only; any nine-digit run in a party field is refused, Treas. Reg. 301.6109-4). Vendor and customer records link to parties on replay; core tags every bill, invoice, payment, and reversal with its counterparty. Journal events party.*, entry.party_set; payroll lines name the employee party. MCP: books.add_party, update_party, set_party_roles, set_w9, set_w4, parties, party, set_entry_party; draft_entry party; run_payroll employee; show party. Viewer: Parties directory and party pages, search, journal text, W-9 attention, demo data. One definition of a party's money (ledger_core::parties::activity) for MCP and viewer. 1099 amounts count payments from cash and financing accounts (a member paying for the company counts; a card doesn't; a prepayment counts when made): vendor_1099_report_with, B-19. Store: attachment links to any subject, statements, reconciliations (schema v11), with replay and projection. ledger-vault: encrypted TIN and W-9 records (ChaCha20-Poly1305, key file outside the books). Spec-blind tests for parties (core, store, MCP, viewer) and attachments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
2a188bd039
commit
a299077991
54 files changed
+10961
-178
No files matched your search
@@ -281,7 +281,7 @@ pub fn status_text(disposed: Option<(NaiveDate, Decimal)>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn notice_box(ui: &mut Ui, n: &Notice) {
|
||||
pub(crate) fn notice_box(ui: &mut Ui, n: &Notice) {
|
||||
let p = pal(ui.ctx());
|
||||
let (fg, bg) = if n.problem {
|
||||
(p.warning, p.warning_soft)
|
||||
@@ -310,7 +310,7 @@ fn notice_box(ui: &mut Ui, n: &Notice) {
|
||||
/// One painted table row: `cells` are (text, width, right-aligned,
|
||||
/// color); the cell whose width is 0 takes what's left. A second line
|
||||
/// (`sub`) goes under the flexible cell.
|
||||
fn row(
|
||||
pub(crate) fn row(
|
||||
ui: &mut Ui,
|
||||
w: f32,
|
||||
cells: &[(String, f32, bool, Color32)],
|
||||
@@ -1345,7 +1345,7 @@ fn tax_card(ui: &mut Ui, a: &Asset) {
|
||||
}
|
||||
|
||||
/// A two-column grid of facts; `add` gets the label painter.
|
||||
fn facts(ui: &mut Ui, add: impl FnOnce(&mut Ui, &dyn Fn(&mut Ui, &str))) {
|
||||
pub(crate) fn facts(ui: &mut Ui, add: impl FnOnce(&mut Ui, &dyn Fn(&mut Ui, &str))) {
|
||||
let p = pal(ui.ctx());
|
||||
egui::Grid::new(ui.next_auto_id())
|
||||
.num_columns(2)
|
||||
|
||||
@@ -183,7 +183,7 @@ pub fn show(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
assets(ui, b, shell);
|
||||
schedules(ui, b);
|
||||
recurring(ui, b);
|
||||
parties(ui, b);
|
||||
parties(ui, b, shell);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -421,8 +421,18 @@ fn recurring(ui: &mut Ui, b: &Books) {
|
||||
ui.add_space(16.0);
|
||||
}
|
||||
|
||||
fn parties(ui: &mut Ui, b: &Books) {
|
||||
fn parties(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
let p = pal(ui.ctx());
|
||||
let mut go: Option<crate::nav::Route> = None;
|
||||
// A record's name opens its party.
|
||||
let mut name = |ui: &mut Ui, text: &str, party: Option<u64>| {
|
||||
let r = widgets::link(ui, RichText::new(text).font(theme::semibold(13.0)));
|
||||
if let Some(id) = party
|
||||
&& r.on_hover_text("Open this party").clicked()
|
||||
{
|
||||
go = Some(crate::nav::Route::Party(id));
|
||||
}
|
||||
};
|
||||
let year = chrono::Datelike::year(&b.today);
|
||||
let r1099 = vendor_1099_report(&b.ledger, year, None).ok();
|
||||
columns(ui, 980.0, &[1.0, 1.0], |i, ui| match i {
|
||||
@@ -444,7 +454,7 @@ fn parties(ui: &mut Ui, b: &Books) {
|
||||
.map(|x| x.paid)
|
||||
.unwrap_or_default();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(RichText::new(&v.name).font(theme::semibold(13.0)));
|
||||
name(ui, &v.name, b.ledger.party_of_vendor(v.id));
|
||||
if let Some(f) = v.form_1099 {
|
||||
widgets::pill(ui, f.label(), p.accent, p.accent_soft);
|
||||
if v.tin_on_file {
|
||||
@@ -490,7 +500,7 @@ fn parties(ui: &mut Ui, b: &Books) {
|
||||
.map(|i| i.balance())
|
||||
.sum();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(RichText::new(&c.name).font(theme::semibold(13.0)));
|
||||
name(ui, &c.name, b.ledger.party_of_customer(c.id));
|
||||
ui.with_layout(
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
@@ -509,4 +519,7 @@ fn parties(ui: &mut Ui, b: &Books) {
|
||||
);
|
||||
}
|
||||
});
|
||||
if let Some(r) = go {
|
||||
shell.go(r);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
|
||||
}
|
||||
};
|
||||
widgets::pill(ui, e.origin, p.muted, p.hover);
|
||||
if let Some(v) = &e.vendor {
|
||||
if let Some(v) = &e.vendor
|
||||
&& e.party_id.is_none()
|
||||
{
|
||||
widgets::pill(ui, &format!("{} {v}", icon::STOREFRONT), p.muted, p.hover);
|
||||
}
|
||||
if e.missing_receipt {
|
||||
@@ -85,6 +87,26 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) {
|
||||
shell.go(Route::Entry(r));
|
||||
}
|
||||
}
|
||||
if let Some(party) = e.party_id.and_then(|id| b.ledger.get_party(id)) {
|
||||
ui.add_space(4.0);
|
||||
if widgets::link(
|
||||
ui,
|
||||
RichText::new(format!(
|
||||
"{} {}",
|
||||
super::parties::party_icon(party),
|
||||
party.label()
|
||||
))
|
||||
.color(p.accent),
|
||||
)
|
||||
.on_hover_text(format!(
|
||||
"The party this entry is with ({}): open them",
|
||||
super::parties::roles_text(party).to_lowercase()
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
shell.go(Route::Party(party.id));
|
||||
}
|
||||
}
|
||||
if let Some(&asset) = b.entry_assets.get(&e.id) {
|
||||
ui.add_space(4.0);
|
||||
if widgets::link(
|
||||
|
||||
@@ -92,6 +92,74 @@ fn tax_basis_text(b: &Books, asset_id: Option<u64>, p: &Value) -> String {
|
||||
text
|
||||
}
|
||||
|
||||
/// What an attachment link's subject (`{"kind": .., "id": ..}`) is, in
|
||||
/// words, and where it opens.
|
||||
fn subject_text(b: &Books, subject: Option<&Value>) -> (String, Option<Route>) {
|
||||
let kind = subject
|
||||
.and_then(|s| s.get("kind"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let id = subject.and_then(|s| s.get("id")).and_then(|v| v.as_u64());
|
||||
match (kind, id) {
|
||||
("entry", Some(e)) => (format!("#{e}"), Some(Route::Entry(e))),
|
||||
("account", Some(a)) => (b.account_label(a), Some(Route::Account(a))),
|
||||
("asset", Some(a)) => (b.asset_label(a), Some(Route::Asset(a))),
|
||||
("party", Some(p)) => (b.party_label(p), Some(Route::Party(p))),
|
||||
("company", _) => ("the company".into(), Some(Route::Company)),
|
||||
("payroll_run", Some(i)) => (format!("payroll run {i}"), None),
|
||||
(k, Some(i)) if !k.is_empty() => (format!("{} {i}", k.replace('_', " ")), None),
|
||||
_ => ("a subject".into(), None),
|
||||
}
|
||||
}
|
||||
|
||||
/// A party's label, or "a party" when the event names none.
|
||||
fn party_name(b: &Books, id: Option<u64>) -> String {
|
||||
match id {
|
||||
Some(id) => b.party_label(id),
|
||||
None => "a party".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// "Display name of Gumroad, Inc. set to Gumroad", "Archived Etsy": one
|
||||
/// `party.updated` event in words. Names are today's (the display name's
|
||||
/// change names the legal name, so it reads the same afterwards).
|
||||
fn party_update_text(b: &Books, id: Option<u64>, change: Option<&Value>) -> String {
|
||||
let name = party_name(b, id);
|
||||
let legal = id
|
||||
.and_then(|i| b.ledger.get_party(i))
|
||||
.map_or_else(|| name.clone(), |p| p.name.clone());
|
||||
let field = change
|
||||
.and_then(|c| c.get("field"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let value = change.and_then(|c| c.get("value"));
|
||||
let text = value.and_then(|v| v.as_str());
|
||||
let words = match field {
|
||||
"name" => "Name",
|
||||
"display_name" => "Display name",
|
||||
"website" => "Website",
|
||||
"address" => "Address",
|
||||
"notes" => "Notes",
|
||||
_ => "",
|
||||
};
|
||||
match (field, text) {
|
||||
("archived", _) => {
|
||||
if value.and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
format!("Archived {name}")
|
||||
} else {
|
||||
format!("Unarchived {name}")
|
||||
}
|
||||
}
|
||||
("name", Some(t)) => format!("Renamed a party to {t}"),
|
||||
("display_name", Some(t)) => format!("Display name of {legal} set to {t}"),
|
||||
("display_name", None) => format!("Display name of {legal} cleared"),
|
||||
("notes", Some(_)) => format!("Notes of {name} updated"),
|
||||
("", _) => format!("Updated {name}"),
|
||||
(_, Some(t)) => format!("{words} of {name} set to {t}"),
|
||||
(_, None) => format!("{words} of {name} cleared"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A sentence for a meaningful event; `None` for the intermediate steps of
|
||||
/// building an entry (created, line added), which the posted event covers.
|
||||
pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
@@ -177,6 +245,67 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
),
|
||||
entry_id.map(Route::Entry),
|
||||
),
|
||||
"attachment.linked" | "attachment.unlinked" => {
|
||||
let (what, route) = subject_text(b, p.get("subject"));
|
||||
let name = attachment_name(b, att);
|
||||
Some(Event {
|
||||
icon: if line.line_type == "attachment.linked" {
|
||||
icon::LINK
|
||||
} else {
|
||||
icon::LINK_BREAK
|
||||
},
|
||||
text: if line.line_type == "attachment.linked" {
|
||||
format!("Linked {name} to {what}")
|
||||
} else {
|
||||
format!("Unlinked {name} from {what}: {}", s(p, "reason"))
|
||||
},
|
||||
route: Some(Route::Document(att)),
|
||||
also: route,
|
||||
})
|
||||
}
|
||||
"statement.recorded" => {
|
||||
let account = p.get("account_id").and_then(|v| v.as_u64());
|
||||
let closing = s(p, "closing_balance")
|
||||
.parse()
|
||||
.map(fmt::dollars)
|
||||
.unwrap_or_default();
|
||||
Some(Event {
|
||||
icon: icon::FILE_TEXT,
|
||||
text: format!(
|
||||
"Statement for {}, {} to {}: closing balance {closing}",
|
||||
account.map_or("an account".into(), |a| b.account_label(a)),
|
||||
s(p, "period_start"),
|
||||
s(p, "period_end")
|
||||
),
|
||||
route: Some(Route::Document(att)),
|
||||
also: account.map(Route::Account),
|
||||
})
|
||||
}
|
||||
"reconciliation.completed" => {
|
||||
let account = p.get("account_id").and_then(|v| v.as_u64());
|
||||
let balance = s(p, "statement_balance")
|
||||
.parse()
|
||||
.map(fmt::dollars)
|
||||
.unwrap_or_default();
|
||||
Some(Event {
|
||||
icon: icon::CHECK_SQUARE,
|
||||
text: format!(
|
||||
"Reconciled {} through {} at {balance}",
|
||||
account.map_or("an account".into(), |a| b.account_label(a)),
|
||||
s(p, "statement_date")
|
||||
),
|
||||
route: account.map(Route::Account),
|
||||
also: p
|
||||
.get("attachment_id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(Route::Document),
|
||||
})
|
||||
}
|
||||
"reconciliation.voided" => ev(
|
||||
icon::PROHIBIT,
|
||||
format!("Voided reconciliation {id}: {}", s(p, "reason")),
|
||||
None,
|
||||
),
|
||||
"attachment.withdrawn" => ev(
|
||||
icon::ARCHIVE,
|
||||
format!("Withdrew {}: {}", attachment_name(b, att), s(p, "reason")),
|
||||
@@ -236,24 +365,169 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
||||
"vendor.added" => ev(
|
||||
icon::STOREFRONT,
|
||||
format!("Added vendor {}", s(p, "name")),
|
||||
Some(Route::Company),
|
||||
Some(
|
||||
p.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(|v| b.ledger.party_of_vendor(v))
|
||||
.map_or(Route::Company, Route::Party),
|
||||
),
|
||||
),
|
||||
"customer.added" => ev(
|
||||
icon::USER_PLUS,
|
||||
format!("Added customer {}", s(p, "name")),
|
||||
Some(Route::Company),
|
||||
Some(
|
||||
p.get("id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(|c| b.ledger.party_of_customer(c))
|
||||
.map_or(Route::Company, Route::Party),
|
||||
),
|
||||
),
|
||||
"class.added" => ev(icon::TAG, format!("Added class {}", s(p, "name")), None),
|
||||
"vendor.1099_set" => ev(
|
||||
icon::IDENTIFICATION_CARD,
|
||||
"Updated a vendor's 1099 details".into(),
|
||||
Some(Route::Company),
|
||||
),
|
||||
"entry.vendor_set" => ev(
|
||||
icon::STOREFRONT,
|
||||
format!("Tagged #{} with a vendor", entry_id.unwrap_or(0)),
|
||||
entry_id.map(Route::Entry),
|
||||
),
|
||||
"party.added" => {
|
||||
let roles: Vec<String> = p
|
||||
.get("roles")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|rs| {
|
||||
rs.iter()
|
||||
.filter_map(|r| r.as_str().and_then(ledger_core::Role::parse))
|
||||
.map(|r| super::parties::role_label(r).to_lowercase())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let name = match s(p, "name") {
|
||||
"" => party_name(b, p.get("id").and_then(|v| v.as_u64())),
|
||||
n => n.to_string(),
|
||||
};
|
||||
ev(
|
||||
icon::ADDRESS_BOOK,
|
||||
if roles.is_empty() {
|
||||
format!("Added party {name}")
|
||||
} else {
|
||||
format!("Added party {name} ({})", roles.join(", "))
|
||||
},
|
||||
p.get("id").and_then(|v| v.as_u64()).map(Route::Party),
|
||||
)
|
||||
}
|
||||
"party.updated" => {
|
||||
let pid = p.get("id").and_then(|v| v.as_u64());
|
||||
ev(
|
||||
icon::PENCIL_SIMPLE,
|
||||
party_update_text(b, pid, p.get("change")),
|
||||
pid.map(Route::Party),
|
||||
)
|
||||
}
|
||||
"party.role_set" => {
|
||||
let pid = p.get("id").and_then(|v| v.as_u64());
|
||||
let name = party_name(b, pid);
|
||||
let role = ledger_core::Role::parse(s(p, "role"));
|
||||
let on = p.get("on").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
let text = match (role, on) {
|
||||
(Some(r), true) => format!("{name} is now {}", super::parties::role_noun(r)),
|
||||
(Some(r), false) => format!("{name} is no longer {}", super::parties::role_noun(r)),
|
||||
(None, _) => format!("Changed the roles of {name}"),
|
||||
};
|
||||
ev(icon::USER_GEAR, text, pid.map(Route::Party))
|
||||
}
|
||||
"party.w9_set" => {
|
||||
let pid = p.get("id").and_then(|v| v.as_u64());
|
||||
let name = party_name(b, pid);
|
||||
let text = match p.get("facts").filter(|f| !f.is_null()) {
|
||||
Some(f) => {
|
||||
let mut t = format!("W-9 for {name} recorded");
|
||||
if let Some(d) = f.get("received").and_then(|v| v.as_str()) {
|
||||
t.push_str(&format!(", received {d}"));
|
||||
}
|
||||
if let Some(e) = f.get("entity_type").and_then(|v| v.as_str()) {
|
||||
t.push_str(&format!(": {e}"));
|
||||
}
|
||||
if let Some(l4) = f.get("tin_last4").and_then(|v| v.as_str()) {
|
||||
t.push_str(&format!(", TIN ending {l4}"));
|
||||
}
|
||||
t
|
||||
}
|
||||
None => format!("W-9 facts for {name} cleared"),
|
||||
};
|
||||
ev(icon::IDENTIFICATION_CARD, text, pid.map(Route::Party))
|
||||
}
|
||||
"party.w4_set" => {
|
||||
let pid = p.get("id").and_then(|v| v.as_u64());
|
||||
let name = party_name(b, pid);
|
||||
let text = match p.get("facts").filter(|f| !f.is_null()) {
|
||||
Some(f) => {
|
||||
let mut t = format!("W-4 for {name} recorded");
|
||||
if let Some(d) = f.get("received").and_then(|v| v.as_str()) {
|
||||
t.push_str(&format!(", received {d}"));
|
||||
}
|
||||
if let Some(st) = f.get("state").and_then(|v| v.as_str()) {
|
||||
t.push_str(&format!(" (works in {st})"));
|
||||
}
|
||||
t
|
||||
}
|
||||
None => format!("W-4 facts for {name} cleared"),
|
||||
};
|
||||
ev(icon::IDENTIFICATION_BADGE, text, pid.map(Route::Party))
|
||||
}
|
||||
"entry.party_set" => {
|
||||
let pid = p.get("party_id").and_then(|v| v.as_u64());
|
||||
let e = entry_id.unwrap_or(0);
|
||||
Some(Event {
|
||||
icon: icon::ADDRESS_BOOK,
|
||||
text: match pid {
|
||||
Some(_) => format!("Tagged #{e} with {}", party_name(b, pid)),
|
||||
None => format!("Cleared the party of #{e}"),
|
||||
},
|
||||
route: entry_id.map(Route::Entry),
|
||||
also: pid.map(Route::Party),
|
||||
})
|
||||
}
|
||||
"vendor.1099_set" => {
|
||||
let vendor = p.get("vendor_id").and_then(|v| v.as_u64());
|
||||
let name = vendor
|
||||
.and_then(|v| b.ledger.get_vendor(v))
|
||||
.map(|v| v.name.clone());
|
||||
let form = p
|
||||
.get("form_1099")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(ledger_core::Form1099::parse)
|
||||
.map(|f| f.label());
|
||||
let w9 = p.get("tin_on_file").and_then(|v| v.as_bool());
|
||||
let text = match (name, form, w9) {
|
||||
(Some(n), Some(f), Some(w)) => format!(
|
||||
"1099 details of {n}: {f}, W-9 {}",
|
||||
if w { "on file" } else { "not on file" }
|
||||
),
|
||||
(Some(n), None, Some(_)) => format!("{n} no longer gets a 1099"),
|
||||
(Some(n), _, None) => format!("Updated the 1099 details of {n}"),
|
||||
(None, ..) => "Updated a vendor's 1099 details".into(),
|
||||
};
|
||||
ev(
|
||||
icon::IDENTIFICATION_CARD,
|
||||
text,
|
||||
Some(
|
||||
vendor
|
||||
.and_then(|v| b.ledger.party_of_vendor(v))
|
||||
.map_or(Route::Company, Route::Party),
|
||||
),
|
||||
)
|
||||
}
|
||||
"entry.vendor_set" => {
|
||||
let vendor = p.get("vendor_id").and_then(|v| v.as_u64());
|
||||
let e = entry_id.unwrap_or(0);
|
||||
Some(Event {
|
||||
icon: icon::STOREFRONT,
|
||||
text: match vendor.and_then(|v| b.ledger.get_vendor(v)) {
|
||||
Some(v) => format!("Tagged #{e} with vendor {}", v.name),
|
||||
None if vendor.is_none() && p.get("vendor_id").is_some() => {
|
||||
format!("Cleared the vendor of #{e}")
|
||||
}
|
||||
None => format!("Tagged #{e} with a vendor"),
|
||||
},
|
||||
route: entry_id.map(Route::Entry),
|
||||
also: vendor
|
||||
.and_then(|v| b.ledger.party_of_vendor(v))
|
||||
.map(Route::Party),
|
||||
})
|
||||
}
|
||||
"invoice.created" => ev(
|
||||
icon::INVOICE,
|
||||
format!("Invoice {id} created"),
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod documents;
|
||||
pub mod entry;
|
||||
pub mod journal;
|
||||
pub mod overview;
|
||||
pub mod parties;
|
||||
pub mod profit;
|
||||
pub mod reports;
|
||||
pub mod transactions;
|
||||
@@ -55,6 +56,8 @@ pub fn show(ui: &mut Ui, b: &Books, route: &Route, shell: &mut Shell<'_>) {
|
||||
Route::Account(id) => accounts::show_account(ui, b, *id, shell),
|
||||
Route::Assets => assets::show_register(ui, b, shell),
|
||||
Route::Asset(id) => assets::show_asset(ui, b, *id, shell),
|
||||
Route::Parties => parties::show_list(ui, b, shell),
|
||||
Route::Party(id) => parties::show_party(ui, b, *id, shell),
|
||||
Route::Documents => documents::show(ui, b, None, shell),
|
||||
Route::Document(id) => documents::show(ui, b, Some(*id), shell),
|
||||
Route::Company => company::show(ui, b, shell),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -78,7 +78,7 @@ fn list(ui: &mut Ui, b: &Books, selected: Option<u64>, shell: &mut Shell<'_>) {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut st.query)
|
||||
.hint_text(format!(
|
||||
"{} Search description, account, vendor, amount",
|
||||
"{} Search description, account, party, amount",
|
||||
icon::MAGNIFYING_GLASS
|
||||
))
|
||||
.desired_width(if narrow { ui.available_width() } else { 340.0 }),
|
||||
@@ -204,10 +204,11 @@ fn row(ui: &mut Ui, b: &Books, e: &EntryInfo, on: bool, since_update: f64) -> eg
|
||||
max_w,
|
||||
);
|
||||
let accounts: Vec<String> = e.accounts.iter().map(|a| b.account_name(*a)).collect();
|
||||
let mut sub = format!("#{} · {} · {}", e.id, e.origin, accounts.join(" → "));
|
||||
if let Some(v) = &e.party {
|
||||
sub.push_str(&format!(" · {v}"));
|
||||
}
|
||||
// The party first: it's what's cut last when the row is narrow.
|
||||
let sub = match &e.party {
|
||||
Some(v) => format!("#{} · {} · {v} · {}", e.id, e.origin, accounts.join(" → ")),
|
||||
None => format!("#{} · {} · {}", e.id, e.origin, accounts.join(" → ")),
|
||||
};
|
||||
widgets::clipped_text(
|
||||
&painter,
|
||||
egui::pos2(text_left, rect.center().y + 10.0),
|
||||
|
||||
Reference in new issue
Block a user