mod subticket;
mod subticket_content;
mod ticket;
pub use subticket::*;
pub use subticket_content::*;
pub use ticket::*;
use super::*;
use crate::media::*;
use crate::prelude::ticket_tally::TicketTally;
use hconf::palette::{Palette, Rgb};
use mkutil::dialog;
use chrono::prelude::{DateTime, Utc};
use crossbeam_channel::Sender;
pub const MODIFIER_LAZY_HINT: &str =
"Hold Alt and click for a lazy send (delaying full UI refresh)";
#[derive(Debug, Clone)]
pub enum TicketAction {
CreateTicket {
ticket: Ticket,
typ: TicketAuthorSource,
},
DeleteTicket {
ticket: Option<ObjectId>,
},
AddSubTicket {
ticket: Option<ObjectId>,
typ: TicketAuthorSource,
subticket: SubTicket,
tally: TicketTally,
},
SetSubTicketStatus {
subticket_id: Option<ObjectId>,
subticket_number: u8,
typ: TicketAuthorSource,
status: SubTicketStatus,
tally: TicketTally,
lazy: bool,
},
EditSubTicket {
existing: Option<ObjectId>,
updated: SubTicket,
},
DeleteSubTicket {
ticket: Option<ObjectId>,
subticket: Option<ObjectId>,
},
AddReply {
subticket: Option<ObjectId>,
reply: Reply,
lazy: bool,
},
EditReply {
subticket: Option<ObjectId>,
existing: Option<Reply>,
updated: Reply,
lazy: bool,
},
DeleteReply {
subticket: Option<ObjectId>,
reply: Reply,
lazy: bool,
},
}
#[async_trait]
pub trait TicketTracking: DynClone + fmt::Debug + Send + Sync {
async fn add_container_ticket(
&self,
project: &Project,
ticket: BloatedTicket,
) -> Result<ObjectId, ModificationError>;
async fn add_subtickets_to_container(
&self,
project: &Project,
ticket: &ObjectId,
subtickets: &[SubTicket],
) -> Result<TicketTally, ModificationError>;
async fn expanded_tickets(
&self,
project: &Project,
asset: &ProductionAsset,
order: &CreatedAtOrdering,
chrono_fmt: &ChronoFormat,
current_user: &Staff,
layout: &EmbedLayoutPref,
preload_images: bool,
) -> Result<Vec<Ticket>, DatabaseError>;
async fn subtickets(
&self,
project: &Project,
ticket: &Ticket,
) -> Result<Vec<SubTicket>, DatabaseError>;
async fn delete_ticket(
&self,
project: &Project,
ticket: &ObjectId,
) -> Result<(), ModificationError>;
async fn add_subticket(
&self,
project: &Project,
ticket: &ObjectId,
subticket: &SubTicket,
) -> Result<ObjectId, ModificationError>;
async fn edit_subticket(
&self,
project: &Project,
existing: Option<&SubTicket>,
updated: &SubTicket,
) -> Result<(), ModificationError>;
async fn delete_subticket(
&self,
project: &Project,
ticket: Option<&ObjectId>,
subticket: &ObjectId,
) -> Result<(), ModificationError>;
async fn set_subticket_status(
&self,
project: &Project,
subticket: &ObjectId,
status: &SubTicketStatus,
modified_by: &Staff,
) -> Result<(), ModificationError>;
async fn add_reply(
&self,
project: &Project,
subticket: &ObjectId,
reply: &Reply,
) -> Result<(), ModificationError>;
async fn edit_reply(
&self,
project: &Project,
subticket: &ObjectId,
existing: Option<&Reply>,
updated: &Reply,
) -> Result<(), ModificationError>;
async fn delete_reply(
&self,
project: &Project,
subticket: &ObjectId,
reply: &Reply,
) -> Result<(), ModificationError>;
}
dyn_clone::clone_trait_object!(TicketTracking);
#[derive(
Deserialize_repr,
Serialize_repr,
Debug,
Clone,
Default,
PartialEq,
Eq,
strum::AsRefStr,
strum::EnumIter,
)]
#[repr(u8)]
pub enum TicketAuthorSource {
#[default]
#[strum(serialize = "📣 AD")]
ArtDirector = 0,
#[strum(serialize = "🚔 Client")]
Client = 1,
#[strum(serialize = "🔔 TeamLead")]
TeamLead = 2,
}
#[cfg(feature = "gui")]
impl TicketAuthorSource {
pub fn color32(&self, palette: &Palette) -> Color32 {
match self {
Self::ArtDirector => AssetStatus::AdFeedback.color32(palette),
Self::Client => AssetStatus::ClientFeedback.color32(palette),
Self::TeamLead => Color32::LIGHT_BLUE,
}
}
pub fn colored_label(&self, palette: &Palette) -> RichText {
RichText::new(self.as_ref())
.color(Color32::BLACK)
.background_color(self.color32(palette))
}
}
#[cfg(feature = "gui")]
pub fn ticket_author_source_options_ui(ui: &mut egui::Ui, source: &mut TicketAuthorSource) {
ui.horizontal(|ui| {
ui.label("Agent:");
for src in TicketAuthorSource::iter() {
ui.selectable_value(source, src.clone(), src.as_ref());
}
});
}
#[derive(
Deserialize_repr,
Serialize_repr,
Debug,
Clone,
Default,
PartialEq,
Eq,
strum::AsRefStr,
strum::EnumIter,
)]
#[repr(u8)]
pub enum SubTicketClass {
#[default]
#[strum(serialize = "🚧 Defect")]
Defect = 0,
#[strum(serialize = "🔀 Direction")]
Direction = 1,
}
#[cfg(feature = "gui")]
impl SubTicketClass {
fn color_rgb<'a>(&self, palette: &'a Palette) -> &'a Rgb {
let p = &palette.dark;
match self {
Self::Defect => &p.rgb.SUBTICKET_DEFECT,
Self::Direction => &p.rgb.SUBTICKET_DIRECTION,
}
}
pub fn color32(&self, palette: &Palette) -> Color32 {
let color = self.color_rgb(palette);
Color32::from_rgb(color.r, color.g, color.b)
}
pub fn colored_label(&self, palette: &Palette) -> RichText {
RichText::new(self.as_ref())
.color(Color32::BLACK)
.background_color(self.color32(palette))
}
}
#[cfg(feature = "gui")]
pub fn subticket_class_options_ui(
ui: &mut egui::Ui,
class: &mut SubTicketClass,
palette: &Palette,
) {
ui.horizontal(|ui| {
ui.label("Classification:");
for cls in SubTicketClass::iter() {
let text = if *class == cls {
cls.colored_label(palette)
} else {
RichText::new(cls.as_ref())
};
ui.radio_value(class, cls, text);
}
});
}
#[derive(Debug, Clone, Default)]
struct SubTicketDiscardableCompose {
subject: String,
class: SubTicketClass,
composer: DiscardableComposer,
}
impl SubTicketDiscardableCompose {
fn first() -> Self {
Self {
composer: DiscardableComposer::new("Write subticket #1:"),
..Default::default()
}
}
fn next(number: usize) -> Self {
Self {
composer: DiscardableComposer::new(&format!("Write subticket #{}:", number)),
..Default::default()
}
}
#[cfg(feature = "gui")]
fn unwrap_ui(&mut self, ui: &mut egui::Ui, idx: usize, palette: &Palette) {
subticket_class_options_ui(ui, &mut self.class, palette);
ui.horizontal(|ui| {
ui.heading("Subject:");
ui.add(egui::TextEdit::singleline(&mut self.subject).desired_width(f32::INFINITY));
});
self.composer.draft_unwrap_with_discard_ui(ui, idx);
}
}
#[derive(Debug, Clone, Default)]
pub struct SubTicketCompose {
subject: String,
class: SubTicketClass,
composer: Composer,
}
impl SubTicketCompose {
pub fn upload_images(
&mut self,
project: &Project,
asset: &ProductionAsset,
upload: impl Fn(&Path, &Project, &ProductionAsset) -> AnyResult<PathBuf>,
) -> ImageUploaded {
self.composer.upload_images(project, asset, upload)
}
}
impl TryFrom<SubTicketDiscardableCompose> for SubTicketCompose {
type Error = &'static str;
fn try_from(discardable: SubTicketDiscardableCompose) -> Result<Self, Self::Error> {
match discardable.composer.0 {
Some(composer) => Ok(Self {
subject: discardable.subject,
class: discardable.class,
composer,
}),
None => Err("Composer was discarded by user"),
}
}
}
#[derive(Debug, Serialize)]
pub struct BloatedTicket {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
id: Option<ObjectId>,
soup_format: MsgTopic,
project: String,
sender: String,
subject: String,
content: Vec<ObjectId>,
topical_assets: Vec<ObjectId>,
recipients: Vec<String>,
seen_by: Vec<String>,
source_type: TicketAuthorSource,
orig_locale: Locale,
#[cfg_attr(
feature = "mongo",
serde(with = "bson::serde_helpers::chrono_datetime_as_bson_datetime")
)]
datetime: DateTime<Utc>,
instantiated_for: Vec<String>,
as_reply_to: Option<Vec<ObjectId>>,
hyperlinks: Option<Vec<HtmlLink>>,
is_plain_text: bool,
is_important: bool,
is_urgent: bool,
}
#[derive(Debug)]
pub struct BloatedTicketBuilder(Ticket);
impl BloatedTicketBuilder {
pub fn new(ticket: Ticket) -> Self {
Self(ticket)
}
pub fn finish(
self,
project: &Project,
assignees: &[Staff],
role_map: &RoleMap,
) -> AnyResult<BloatedTicket> {
let recipients: Vec<String> = self
.0
.topic
.receipt_recipients(assignees, role_map)?
.iter()
.map(|s| s.name_unwrap().to_owned())
.collect();
Ok(BloatedTicket {
id: self.0.id,
soup_format: self.0.topic,
project: project.as_str().to_owned(),
sender: self.0.sender,
subject: self.0.name,
content: self.0.content,
topical_assets: self.0.topical_assets,
recipients,
seen_by: vec![],
source_type: self.0.typ,
orig_locale: self.0.composed_with_locale,
datetime: self.0.created_at,
instantiated_for: self.0.instantiated_for,
as_reply_to: None,
hyperlinks: None,
is_plain_text: false, is_important: false,
is_urgent: false,
})
}
}