hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
use super::*;

const NO_SUBJECT: &str = "No Subject";
const NO_TOPICAL_ERR: &str = "No \"topical asset\" found in your message. Message must be about some Production Asset. Please choose one and try again.";
const NO_TOPICAL_RECEIPT_ERR: &str =
    "Expected at least one \"topical asset\" for system message. Found none";

#[derive(Debug)]
/// Responsible to perform all necessary steps to make a [`QueryMsg`] ready for being read
/// and displayed into the UI.
pub struct MsgReadBuilder(QueryMsg);

impl MsgReadBuilder {
    pub fn new(msg: QueryMsg) -> Self {
        Self(msg)
    }

    fn created_at_local(mut self) -> Self {
        let local: DateTime<Local> = DateTime::from(self.0.created_at);
        self.0.ext.created_at_local = format!("{}", local.format(YEAR_MONTH_DAY_FORMAT));
        self
    }

    fn optimize_hyperlinks(mut self) -> Self {
        if let Some(links) = &self.0.hyperlinks {
            if links.is_empty() {
                self.0.hyperlinks.take();
            };
        };
        self
    }

    fn subject(mut self) -> Self {
        // only for system messages
        if !self.0.is_handwritten() && self.0.subject.is_empty() {
            self.0.subject = NO_SUBJECT.to_owned();
        };
        self
    }

    fn seen_by_str(mut self) -> Self {
        self.0.ext.seen_by_str = match self.0.seen_by_names.is_empty() {
            true => "Seen by no one".to_string(),
            false => format!("👁 Seen by: {}", self.0.seen_by_names.join(", ")),
        };
        self
    }

    fn content_with_sender(mut self) -> Self {
        // only for system messages
        if !self.0.is_handwritten() {
            if self.0.content.is_empty() {
                self.0.content = format!("sent by {}", self.0.sender_name);
            } else {
                self.0.content = format!("{}; sent by {}", self.0.content, self.0.sender_name);
            };
        };
        self
    }

    fn build(self) -> Self {
        self
            // `created_at` field
            .created_at_local()
            // `seen_by` field
            .seen_by_str()
            // `hyperlinks` field
            .optimize_hyperlinks()
            // `subject` field
            .subject()
    }

    /// "Offline"-preparation of a [`QueryMsg`] to make it ready for [`MediaMode::Read`] display, e.g.
    /// making `QueryMsg::created_at_local` from `QueryMsg::created_at`,
    /// and cleaning up the `QueryMsg::hyperlinks`.
    pub fn finish(self) -> QueryMsg {
        self.build()
            // `content` field
            .content_with_sender()
            .0
    }

    pub fn finish_as_sent(self) -> QueryMsg {
        // skips concatenating content
        self.build().0
    }
}

/// Responsible to perform all necessary steps to make a [`QueryMsg`] ready for being edited
/// and displayed into the UI.
pub struct MsgEditBuilder(QueryMsg);

impl MsgEditBuilder {
    pub fn new(msg: QueryMsg) -> Self {
        Self(msg)
    }

    // /// NOTE: this is for legacy handwritten messages only.
    // /// Removes current composer for less confusion.
    // fn pop_composer(mut self) -> Self {
    //     self.0.ext.pop_composer();
    //     self
    // }

    pub fn finish(self) -> QueryMsg {
        // self.pop_composer().0
        self.0
    }
}
pub struct SystemMsgWriteBuilder {
    msg: QueryMsg,
    working_step: Option<AssetStatus>,
    phase: Option<VcsLiteSession>,
}

impl SystemMsgWriteBuilder {
    pub fn new(user: Staff, topic: MsgTopic, asset: ProductionAsset) -> AnyResult<Self> {
        let topical: Option<AssetExcerpt> = asset.into();
        let msg = QueryMsg::sent_by_current_user(user)
            .with_topic(topic)
            .with_topical_asset_id(topical.as_ref())?
            .with_topical_asset(topical);
        Ok(Self {
            msg,
            working_step: None,
            phase: None,
        })
    }

    pub fn working_step(mut self, working_step: Option<AssetStatus>) -> Self {
        self.working_step = working_step;
        self
    }

    pub fn phase(mut self, phase: Option<VcsLiteSession>) -> Self {
        self.phase = phase;
        self
    }

    /// Makes `subject` field
    fn receipt_subject_from_topic(mut self) -> AnyResult<Self> {
        let asset = self
            .msg
            .ext
            .topical_asset()
            .context(NO_TOPICAL_RECEIPT_ERR)?;
        // subject based on topic
        self.msg.subject = self.msg.topic.subject(asset);
        Ok(self)
    }

    /// Makes `content` field.
    /// Invoke this to build the content for system messages.
    fn receipt_content(
        mut self,
        assignees: &[Staff],
        ticket_tally: Option<&TicketTally>,
    ) -> AnyResult<Self> {
        // content based on topic
        let content = self.msg.topic.receipt_content(
            assignees,
            self.working_step.as_ref(),
            self.phase.as_ref(),
            ticket_tally,
        )?;
        self.msg.content = content;
        Ok(self)
    }

    /// Makes `recipients` field.
    fn receipt_recipients(mut self, assignees: &[Staff], layout: &RoleMap) -> AnyResult<Self> {
        // recipients based on topic
        let recipients = self.msg.topic.receipt_recipients(assignees, layout)?;
        self.msg.ext.recipients_mut(recipients);
        Ok(self)
    }

    pub fn finish(
        mut self,
        project: &Project,
        assignees: &[Staff],
        layout: &RoleMap,
        ticket_tally: Option<&TicketTally>,
    ) -> AnyResult<QueryMsg> {
        self = self
            .receipt_content(assignees, ticket_tally)?
            .receipt_subject_from_topic()?
            .receipt_recipients(assignees, layout)?;
        // prepares all other fields
        self.msg = MsgWriteBuilder::new(self.msg)
            .created_now()
            .finish(project)?;
        Ok(self.msg)
    }
}

/// Responsible to perform all necessary steps to make a [`QueryMsg`] ready for being written
/// into the DB. Normally the [`MsgWriteHeadlessBuilder`] will piggybacking this implementation.
pub struct MsgWriteBuilder {
    msg: QueryMsg,
}

impl MsgWriteBuilder {
    pub fn new(msg: QueryMsg) -> Self {
        Self { msg }
    }

    fn sender_name(mut self) -> Self {
        self.msg.sender_name = self.msg.ext.sender.name_unwrap().to_owned();
        self
    }
    /// Chooses the topical asset depending on which source of the topical asset is favored.

    pub fn topical_asset_bson_ids(
        mut self,
        mut topical_alternative: Option<AssetExcerpt>,
        favor_alternative: bool,
    ) -> AnyResult<Self> {
        let asset = match favor_alternative {
            false => {
                // uses current "baked-in" topical asset
                self.msg.ext.topical_asset_clone()
            }
            true => {
                // uses the alternative
                topical_alternative.take()
            }
        };
        if let Some(asset) = asset {
            let id = asset.id.context("Bad AssetExcerpt with no ObjectId")?;
            self.msg.topical_asset_bson_ids = vec![id];
        };
        Ok(self)
    }

    fn as_reply_to_bson_ids(mut self) -> AnyResult<Self> {
        if let Some(reply) = self.msg.ext.as_reply_to.take() {
            let id = reply.id.context("Bad reply excerpt with no ObjectId")?;
            self.msg.as_reply_to_bson_ids = Some(vec![id]);
        } else {
            // avoids null type
            self.msg.as_reply_to_bson_ids = Some(vec![]);
        };
        Ok(self)
    }

    /// To support legacy clients.
    fn project(mut self, project: &Project) -> Self {
        self.msg.project = Some(format!("{}", project));
        self
    }

    pub fn created_now(mut self) -> Self {
        self.msg.created_at = Utc::now();
        self
    }

    // /// Remember to invoke this prior to [`Self::recipient_names`].
    // fn cc_composer(mut self) -> Self {
    //     // only for handwritten messages
    //     if self.msg.is_handwritten() {
    //         self.msg.ext.cc_composer();
    //     };
    //     self
    // }

    /// This behaves only correctly in the case where users don't alter the recipients
    /// during replying|editing messages. Normally we would want to allow them
    /// to do so, i.e. modifying the recipient selection. Therefore the `QueryMsg::recipients`
    /// must be set properly PRIOR to this method being invoked.
    fn recipient_names(mut self) -> Self {
        self.msg.recipient_names = self
            .msg
            .ext
            .recipients()
            .iter()
            .map(|s| s.name_unwrap().to_owned())
            .collect();
        self
    }

    /// Remember to invoke this after [`Self::sender_name`].
    fn instantiated_for(mut self) -> Self {
        self.msg
            .instantiated_for
            .push(self.msg.sender_name.to_owned());
        self
    }

    fn hyperlinks(mut self) -> Self {
        if self.msg.hyperlinks.is_none() {
            // avoids null type
            self.msg.hyperlinks = Some(vec![]);
        }
        self
    }

    /// Preparation of a [`QueryMsg`] prior to its being sent.
    pub fn finish(self, project: &Project) -> AnyResult<QueryMsg> {
        // `topical_assets` field
        Ok(self
            .sender_name()
            // `as_reply_to` field
            .as_reply_to_bson_ids()?
            // `project` field
            .project(project)
            // `recipients` field
            // .cc_composer()
            .recipient_names()
            .instantiated_for()
            // `hyperlinks` field
            .hyperlinks()
            .msg)
    }
}

/// Responsible to check if a [`QueryMsg`] contains enough required information
/// before it getting sent.
/// Remember to use this after using [`MsgWriteBuilder`], since the "topical asset"
/// is constructed in a complex way where "topical alternative" is provided, and so
/// the "topical asset" isn't known until a [`MsgWriteBuilder`] is finished.
pub struct MsgWriteScanBuilder<'a>(&'a QueryMsg);

impl<'a> MsgWriteScanBuilder<'a> {
    pub fn new(msg: &'a QueryMsg) -> Self {
        Self(msg)
    }

    /// Fails this if the message is not about any [`ProductionAsset`].
    fn has_topical_asset(self) -> AnyResult<Self> {
        // do not use rich type as it may have been moved out
        if self.0.topical_asset_bson_ids.is_empty() {
            Err(anyhow!(NO_TOPICAL_ERR))
        } else {
            Ok(self)
        }
    }

    /// Fails this if the message has no recipient.
    fn has_recipient(self) -> AnyResult<Self> {
        // only checks for handwritten messages
        if !self.0.is_handwritten() {
            return Ok(self);
        };
        if self.0.ext.is_recipient_empty() {
            Err(anyhow!(
                "Your message has no recipient. Message must have at least one recipient. Please select some and try again."
            ))
        } else {
            Ok(self)
        }
    }

    fn has_content(self) -> AnyResult<Self> {
        match self.0.content.is_empty() {
            true => Err(anyhow!(
                "Your message has no content. Please type your message and try again."
            )),
            false => Ok(self),
        }
    }

    pub fn scanned(self) -> AnyResult<bool> {
        self.has_topical_asset()?.has_recipient()?.has_content()?;
        Ok(true)
    }
}