use super::*;
#[cfg(feature = "gui")]
use crossbeam_channel::Sender;
#[cfg(feature = "gui")]
use mkutil::record_binary_state;
#[derive(Debug, Clone)]
pub struct PanProjectReviewTotal(pub ProjReviewStats);
impl From<&PanProjectReviewStats> for PanProjectReviewTotal {
fn from(pp_stats: &PanProjectReviewStats) -> Self {
let total = |typ: Supervision| {
let series = pp_stats
.0
.iter()
.map(|(_, proj_stats)| match &typ {
Supervision::Wip => &proj_stats.wip,
Supervision::Feedback => &proj_stats.feedback,
Supervision::Approval => &proj_stats.approval,
Supervision::Client => &proj_stats.client,
})
.collect::<Vec<&SupervisionStats>>();
SupervisionStats::total(&series, typ)
};
Self(ProjReviewStats {
wip: total(Supervision::Wip),
feedback: total(Supervision::Feedback),
approval: total(Supervision::Approval),
client: total(Supervision::Client),
})
}
}
#[derive(Debug, Clone)]
pub struct PanProjectReviewStats(pub Vec<(Project, ProjReviewStats)>);
impl Default for PanProjectReviewStats {
fn default() -> Self {
Self(vec![])
}
}
#[derive(Debug, Clone)]
pub struct SupervisionStats {
id: String,
typ: Supervision,
#[cfg(feature = "gui")]
window_pos: egui::Pos2,
reviewed: BinaryStats,
not_reviewed: BinaryStats,
stats_line: StatsLine,
}
impl SupervisionStats {
fn id(project: &Project, typ: &Supervision) -> String {
format!("{} {}", project, typ.as_ref())
}
fn from_project_verdicts(
project: &Project,
verdicts: &ProjReviewVerdict,
typ: Supervision,
) -> SupervisionStats {
let mut reviewed = vec![];
let mut not_reviewed = vec![];
verdicts.iter().for_each(|(asset, verdict)| {
match verdict.review_item(&typ) {
Reviewed::Yes => {
reviewed.push(asset.clone());
}
Reviewed::No => {
not_reviewed.push(asset.clone());
}
_ => {
}
}
});
reviewed.sort_by(|a, b| a.partial_cmp(b).unwrap());
not_reviewed.sort_by(|a, b| a.partial_cmp(b).unwrap());
let stats_line = StatsLine::from_count(reviewed.len(), not_reviewed.len());
SupervisionStats {
id: Self::id(project, &typ),
typ: typ,
#[cfg(feature = "gui")]
window_pos: Default::default(),
reviewed: BinaryStats::Reviewed(Stats::with_assets(reviewed)),
not_reviewed: BinaryStats::NotReviewed(Stats::with_assets(not_reviewed)),
stats_line,
}
}
fn total(series: &[&Self], typ: Supervision) -> Self {
let reviewed: usize = series.iter().map(|s| s.reviewed.inner().count).sum();
let not_reviewed: usize = series.iter().map(|s| s.not_reviewed.inner().count).sum();
let stats_line = StatsLine::total_from_count(reviewed, not_reviewed);
Self {
id: String::new(),
typ,
#[cfg(feature = "gui")]
window_pos: Default::default(),
reviewed: BinaryStats::Reviewed(Stats::count_only(reviewed)),
not_reviewed: BinaryStats::NotReviewed(Stats::count_only(not_reviewed)),
stats_line,
}
}
fn debug_count(&self) -> String {
format!(
"Not Reviewed ({}): {}\nReviewed ({}): {}",
self.not_reviewed.count(),
AssetExcerpt::debug_name(&self.not_reviewed.inner().assets),
self.reviewed.count(),
AssetExcerpt::debug_name(&self.reviewed.inner().assets)
)
}
}
#[cfg(feature = "gui")]
impl SupervisionStats {
pub fn show_summary_label(
&mut self,
ui: &mut egui::Ui,
project: &Project,
trim: &DateRange,
tx: &Sender<ReviewAction>,
open_state: &mut BTreeSet<String>,
) {
match &self.stats_line {
StatsLine::Zero(_) => {
ui.label(self.stats_line.text());
}
_ => {
let mut show_details = open_state.contains(&self.id);
let response = ui.toggle_value(&mut show_details, self.stats_line.text());
if response.clicked() {
if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) {
self.window_pos = pos;
};
};
self.details_ui(&mut show_details, ui, project, trim, tx);
record_binary_state(open_state, &self.id, show_details);
}
}
}
fn details_ui(
&mut self,
show_details: &mut bool,
ui: &mut egui::Ui,
project: &Project,
trim: &DateRange,
tx: &Sender<ReviewAction>,
) {
egui::Window::new(&self.id)
.open(show_details)
.vscroll(true)
.default_pos(self.window_pos)
.show(ui.ctx(), |ui| {
ui.vertical_centered(|ui| {
ui.label(
RichText::new(format!("Showing {}-day range", trim.days_difference()))
.small()
.strong(),
);
});
ui.separator();
egui::Grid::new("stats_grid")
.min_col_width(150.)
.show(ui, |ui| {
let Self {
id,
typ,
window_pos: _,
reviewed,
not_reviewed,
stats_line,
} = self;
if let StatsLine::All(_) = stats_line {
not_reviewed.ui(ui, project, &Header::NOT_REVIEWED_EMPTY, typ, id, tx);
} else {
not_reviewed.ui(ui, project, &Header::NOT_REVIEWED, typ, id, tx);
};
reviewed.ui(ui, project, &Header::REVIEWED, typ, id, tx);
ui.end_row();
});
});
}
pub fn total_ui(&self, ui: &mut egui::Ui) {
ui.label(self.stats_line.total_text());
}
}
#[derive(Debug, Clone)]
pub struct ProjReviewStats {
pub wip: SupervisionStats,
pub feedback: SupervisionStats,
pub approval: SupervisionStats,
pub client: SupervisionStats,
}
impl ProjReviewStats {
pub fn new(project: &Project, verdicts: &ProjReviewVerdict) -> Self {
Self {
wip: SupervisionStats::from_project_verdicts(project, verdicts, Supervision::Wip),
feedback: SupervisionStats::from_project_verdicts(
project,
verdicts,
Supervision::Feedback,
),
approval: SupervisionStats::from_project_verdicts(
project,
verdicts,
Supervision::Approval,
),
client: SupervisionStats::from_project_verdicts(project, verdicts, Supervision::Client),
}
}
pub fn debug_count(&self) {
eprintln!("\n👀 Wip: {}", self.wip.debug_count());
eprintln!("\n👀 Feedback: {}", self.feedback.debug_count());
eprintln!("\n👀 Approval: {}", self.approval.debug_count());
eprintln!("\n👀 Client: {}", self.client.debug_count());
}
}
#[derive(Debug, Clone)]
enum BinaryStats {
Reviewed(Stats),
NotReviewed(Stats),
}
impl BinaryStats {
fn inner(&self) -> &Stats {
match &self {
Self::Reviewed(inner) | Self::NotReviewed(inner) => inner,
}
}
fn count(&self) -> &usize {
match &self {
Self::Reviewed(inner) | Self::NotReviewed(inner) => &inner.count,
}
}
fn is_positive(&self) -> bool {
matches!(&self, Self::Reviewed(_))
}
#[cfg(feature = "gui")]
fn ui(
&self,
ui: &mut egui::Ui,
project: &Project,
header: &Header,
typ: &Supervision,
window_id: &String,
tx: &Sender<ReviewAction>,
) {
ui.vertical(|ui| {
ui.label(header.text());
ui.separator();
egui::Grid::new(header.inner())
.striped(true)
.min_col_width(150.)
.show(ui, |ui| {
let is_reviewed_group = self.is_positive();
for asset in self.inner().assets.iter() {
ui.horizontal(|ui| {
panproject_pilot_button(ui, project, asset, tx);
if is_reviewed_group {
panproject_mark_not_viewed_button(ui, project, asset, typ, tx);
} else {
panproject_mark_viewed_button(
ui,
project,
asset,
typ,
self.count(),
window_id,
tx,
);
};
#[cfg(debug_assertions)]
panproject_debug_notice_sequence_button(ui, project, asset, tx);
asset.preview_name(ui);
});
ui.end_row();
}
});
});
}
}
#[derive(Debug, Clone)]
struct Stats {
assets: Vec<AssetExcerpt>,
count: usize,
}
impl Stats {
fn with_assets(assets: Vec<AssetExcerpt>) -> Self {
Self {
count: assets.len(),
assets,
}
}
fn count_only(count: usize) -> Self {
Self {
assets: vec![],
count,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum StatsLine {
Zero(&'static str),
Partial(String),
All(String),
}
impl StatsLine {
fn from_count(reviewed: usize, not_reviewed: usize) -> Self {
let total = reviewed + not_reviewed;
if total == 0 {
return Self::Zero("Nothing to review");
};
if reviewed == total {
Self::All(format!("🍻 All {} reviewed", total))
} else {
Self::Partial(format!("Reviewed {} of {}", reviewed, total))
}
}
fn total_from_count(reviewed: usize, not_reviewed: usize) -> Self {
let total = reviewed + not_reviewed;
if total == 0 {
return Self::Zero("None");
};
if reviewed == total {
Self::All(format!("🍻 All {} cleared", total))
} else {
Self::Partial(format!("{} of {} left", not_reviewed, total))
}
}
#[cfg(feature = "gui")]
fn text(&self) -> RichText {
match self {
Self::Zero(line) => RichText::new(*line).weak().small(),
Self::Partial(line) => RichText::new(line).color(Color32::LIGHT_RED),
Self::All(line) => RichText::new(line).color(Color32::LIGHT_GREEN).small(),
}
}
#[cfg(feature = "gui")]
fn total_text(&self) -> RichText {
match self {
Self::Zero(line) => RichText::new(*line).weak(),
Self::Partial(line) => RichText::new(line).color(Color32::LIGHT_RED),
Self::All(line) => RichText::new(line).color(Color32::LIGHT_GREEN),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Header {
Reviewed(&'static str),
NotReviewed(&'static str),
NotReviewedEmpty(&'static str),
}
impl Header {
const REVIEWED: Header = Self::Reviewed("Reviewed");
const NOT_REVIEWED: Header = Self::NotReviewed("Not Reviewed");
const NOT_REVIEWED_EMPTY: Header = Self::NotReviewedEmpty("Not Reviewed");
fn inner(&self) -> &'static str {
match self {
Self::Reviewed(txt) | Self::NotReviewed(txt) | Self::NotReviewedEmpty(txt) => txt,
}
}
#[cfg(feature = "gui")]
fn text(&self) -> RichText {
match self {
Self::Reviewed(txt) => RichText::new(*txt).color(Color32::LIGHT_GREEN).strong(),
Self::NotReviewed(txt) => RichText::new(*txt).color(Color32::LIGHT_RED).strong(),
Self::NotReviewedEmpty(txt) => RichText::new(*txt),
}
}
}
#[cfg(feature = "gui")]
fn panproject_pilot_button(
ui: &mut egui::Ui,
project: &Project,
asset: &AssetExcerpt,
tx: &Sender<ReviewAction>,
) {
if ui
.button(
RichText::new("✈")
.color(Color32::BLACK)
.background_color(Color32::LIGHT_GRAY),
)
.on_hover_text(RichText::new("Pilot this asset").color(Color32::YELLOW))
.clicked()
{
tx.send(ReviewAction::pilot(project, asset))
.expect(&format!(
"Failed to send ReviewAction::Pilot via channel for {:?}",
asset
));
};
}
#[cfg(feature = "gui")]
fn panproject_mark_viewed_button(
ui: &mut egui::Ui,
project: &Project,
asset: &AssetExcerpt,
typ: &Supervision,
existing_unreviewed: &usize,
window_id: &String,
tx: &Sender<ReviewAction>,
) {
if ui
.button("Y")
.on_hover_text(RichText::new("Mark this asset as reviewed").color(Color32::LIGHT_GREEN))
.clicked()
{
tx.send(ReviewAction::mark_reviewed(
project,
asset,
typ,
existing_unreviewed,
window_id,
))
.expect(&format!(
"Failed to send ReviewAction::MarkReviewed via channel for {:?}",
asset
));
};
}
#[cfg(feature = "gui")]
fn panproject_mark_not_viewed_button(
ui: &mut egui::Ui,
project: &Project,
asset: &AssetExcerpt,
typ: &Supervision,
tx: &Sender<ReviewAction>,
) {
if ui
.button("N")
.on_hover_text(RichText::new("Mark this asset as NOT reviewed").color(Color32::LIGHT_RED))
.clicked()
{
tx.send(ReviewAction::mark_not_reviewed(project, asset, typ))
.expect(&format!(
"Failed to send ReviewAction::MarkNotReviewed via channel for {:?}",
asset
));
};
}
#[cfg(debug_assertions)]
#[cfg(feature = "gui")]
fn panproject_debug_notice_sequence_button(
ui: &mut egui::Ui,
project: &Project,
asset: &AssetExcerpt,
tx: &Sender<ReviewAction>,
) {
if ui
.button(RichText::new("👓").color(Color32::RED))
.on_hover_text("Examine this asset's NoticeSequence")
.clicked()
{
tx.send(ReviewAction::debug_notice_sequence(project, asset))
.expect(&format!(
"Failed to send ReviewAction::DebugNoticeSequence via channel for {:?}",
asset
));
};
}