diff --git a/src/trainer/build.rs b/src/trainer/build.rs index 40a1b41..41663fb 100644 --- a/src/trainer/build.rs +++ b/src/trainer/build.rs @@ -343,6 +343,14 @@ pub struct PlanNeed { /// Groups that see these cases on their desk and move nothing. #[serde(default)] pub watched_by: Vec, + /// Groups whose members each see *their own* cases and nobody + /// else's: the ones that carry the address they are signed in + /// with. A customer follows their assignment; a member sees what + /// they sent. The form must collect a required `email`, and the + /// group is usually one a stage `grants`, so the person who sent + /// the case is who ends up able to follow it. + #[serde(default)] + pub followed_by: Vec, /// The event name a stage is entered by, where it must not be the /// builder's default (the stage's own name; `received` for the /// first). A live site replays its records by event name, so a @@ -432,7 +440,7 @@ impl Field { /// What a public listing may show: everything but contact details /// and what the plan marks private. pub fn is_public(&self) -> bool { - !self.private && self.kind != "email" && self.kind != "tel" + !self.private && self.kind != "email" && self.kind != "tel" && self.kind != "file" } } @@ -474,7 +482,7 @@ impl Plan { } } -const FIELD_TYPES: &[&str] = &["text", "email", "tel", "textarea", "date", "select", "location"]; +const FIELD_TYPES: &[&str] = &["text", "email", "tel", "textarea", "date", "select", "location", "file"]; fn is_id(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-') @@ -687,6 +695,14 @@ impl Plan { bail!(at("a stage that grants a group makes an account for the person, so the form must collect a required `name` (text) and a required `email` (type email)")); } } + for group in &need.followed_by { + if !is_id(group) || group == PUBLIC || group == SUBMITTER { + bail!(at(&format!("followed_by must name groups, not {group:?}"))); + } + if !need.collects.iter().any(|f| f.name == "email" && f.kind == "email" && !f.optional) { + bail!(at("followed_by shows a person the cases that carry their address, so the form must collect a required `email` of type email")); + } + } for (stage, e) in &need.escalate { if e.days == 0 { bail!(at(&format!("escalate from {stage:?} must wait at least one day"))); @@ -1184,7 +1200,9 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap { // The list itself says what a row is and what the // buttons under it do; the card's own description says // what the cases are. - let note = if moves.is_empty() { + let note = if follows_own(needs[0], &group) { + loc(lang, "Each row is one of your own cases and the stage it has reached.", "Hver rad er én av dine egne saker og hvor langt den har kommet.") + } else if moves.is_empty() { loc(lang, "Each row is one case and the stage it has reached. Another desk moves these.", "Hver rad er én sak og hvor langt den har kommet. En annen pult flytter disse.") } else { loc(lang, "Each row is one case. The buttons record what you have just done with it.", "Hver rad er én sak. Knappene fører opp det du nettopp har gjort med den.") @@ -1480,7 +1498,16 @@ fn escalated_states_for(need: &PlanNeed, group: &str) -> Vec { /// A group's desk shows a bucket when it moves something in it, is /// answerable for it, or watches it. fn on_desk(need: &PlanNeed, group: &str) -> bool { - group != PUBLIC && (need.handled_by == group || need.watched_by.iter().any(|g| g == group) || !moves_of(need, group).is_empty()) + group != PUBLIC && (need.handled_by == group || need.watched_by.iter().any(|g| g == group) || need.followed_by.iter().any(|g| g == group) || !moves_of(need, group).is_empty()) +} + +/// A group that has the bucket only to follow its own cases: its list +/// is narrowed to the signed-in person's address. +fn follows_own(need: &PlanNeed, group: &str) -> bool { + need.followed_by.iter().any(|g| g == group) + && need.handled_by != group + && !need.watched_by.iter().any(|g| g == group) + && !need.moves_by.values().flatten().any(|m| m == group) } fn groups(plan: &Plan) -> BTreeSet { @@ -1493,6 +1520,7 @@ fn groups(plan: &Plan) -> BTreeSet { .chain(n.watched_by.iter().filter(|g| *g != PUBLIC).cloned()) .chain(n.moves_by.values().flatten().filter(|m| *m != SUBMITTER).cloned()) .chain(n.escalate.values().map(|e| e.to.clone())) + .chain(n.followed_by.iter().cloned()) }) .collect() } @@ -2154,6 +2182,10 @@ pub fn render(plan: &Plan, words: &BTreeMap) -> anyhow::Result