use serde::{Deserialize, Serialize};
use super::common::{Gid, StatusColor};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CustomFieldDefinition {
pub gid: Gid,
pub name: String,
pub resource_subtype: Option<CustomFieldType>,
#[serde(default)]
pub is_required: bool,
pub description: Option<String>,
pub precision: Option<u32>,
pub format: Option<String>,
pub currency_code: Option<String>,
#[serde(default)]
pub enum_options: Vec<EnumOption>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CustomFieldType {
Text,
Number,
Enum,
MultiEnum,
Date,
People,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnumOption {
pub gid: Gid,
#[serde(default)]
pub name: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
pub color: Option<String>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CustomFieldSetting {
pub gid: Gid,
pub custom_field: CustomFieldDefinition,
#[serde(default)]
pub is_important: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CustomFieldValue {
pub gid: Gid,
pub name: Option<String>,
pub resource_subtype: Option<CustomFieldType>,
pub display_value: Option<String>,
pub text_value: Option<String>,
pub number_value: Option<f64>,
pub enum_value: Option<EnumOption>,
#[serde(default)]
pub multi_enum_values: Vec<EnumOption>,
pub date_value: Option<DateValue>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DateValue {
pub date: Option<String>,
pub date_time: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ExtractedStatus {
pub field_name: String,
pub value: Option<String>,
pub color: Option<String>,
pub status_color: StatusColor,
}
#[derive(Debug, Clone, Default)]
pub struct StatusExtractionOptions {
pub exact_name: Option<String>,
pub contains: Option<String>,
}
pub fn extract_status_field(
custom_fields: &[CustomFieldValue],
options: Option<StatusExtractionOptions>,
) -> Option<ExtractedStatus> {
let options = options.unwrap_or_default();
let field = if let Some(exact_name) = &options.exact_name {
custom_fields
.iter()
.find(|f| f.name.as_ref() == Some(exact_name))
} else if let Some(contains) = &options.contains {
let contains_lower = contains.to_lowercase();
custom_fields.iter().find(|f| {
f.name
.as_ref()
.map(|n| n.to_lowercase().contains(&contains_lower))
.unwrap_or(false)
})
} else {
custom_fields
.iter()
.find(|f| f.name.as_deref() == Some("Status"))
.or_else(|| {
custom_fields.iter().find(|f| {
f.name
.as_ref()
.map(|n| n.to_lowercase().contains("status"))
.unwrap_or(false)
})
})
.or_else(|| {
custom_fields
.iter()
.find(|f| matches!(f.resource_subtype, Some(CustomFieldType::Enum)))
})
};
field.and_then(extract_from_field)
}
fn extract_from_field(field: &CustomFieldValue) -> Option<ExtractedStatus> {
let field_name = field.name.clone().unwrap_or_default();
let (value, color, status_color) = match field.resource_subtype {
Some(CustomFieldType::Enum) => {
let value = field
.enum_value
.as_ref()
.and_then(|e| e.name.clone())
.or_else(|| field.display_value.clone());
let color = field.enum_value.as_ref().and_then(|e| e.color.clone());
let status_color = map_color_to_status(&color, &value);
(value, color, status_color)
}
Some(CustomFieldType::Text) => {
let value = field
.text_value
.clone()
.or_else(|| field.display_value.clone());
(value, None, StatusColor::None)
}
_ => (field.display_value.clone(), None, StatusColor::None),
};
Some(ExtractedStatus {
field_name,
value,
color,
status_color,
})
}
fn map_color_to_status(color: &Option<String>, value: &Option<String>) -> StatusColor {
if let Some(c) = color {
let c_lower = c.to_lowercase();
if c_lower.contains("green") {
return StatusColor::Green;
}
if c_lower.contains("yellow") || c_lower.contains("orange") {
return StatusColor::Yellow;
}
if c_lower.contains("red") {
return StatusColor::Red;
}
if c_lower.contains("blue") {
return StatusColor::Blue;
}
}
if let Some(v) = value {
let v_lower = v.to_lowercase();
if v_lower.contains("on track")
|| v_lower.contains("complete")
|| v_lower.contains("done")
|| v_lower.contains("good")
{
return StatusColor::Green;
}
if v_lower.contains("at risk") || v_lower.contains("warning") || v_lower.contains("behind")
{
return StatusColor::Yellow;
}
if v_lower.contains("off track")
|| v_lower.contains("blocked")
|| v_lower.contains("critical")
{
return StatusColor::Red;
}
}
StatusColor::None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_custom_field_definition() {
let json = r#"{
"gid": "123",
"name": "Priority",
"resource_subtype": "enum",
"is_required": true,
"enum_options": [
{"gid": "1", "name": "High", "enabled": true, "color": "red"},
{"gid": "2", "name": "Medium", "enabled": true, "color": "yellow"},
{"gid": "3", "name": "Low", "enabled": true, "color": "green"}
]
}"#;
let field: CustomFieldDefinition = serde_json::from_str(json).unwrap();
assert_eq!(field.gid, "123");
assert_eq!(field.name, "Priority");
assert_eq!(field.resource_subtype, Some(CustomFieldType::Enum));
assert!(field.is_required);
assert_eq!(field.enum_options.len(), 3);
}
#[test]
fn test_deserialize_enum_option_without_name() {
let json = r#"{
"gid": "456",
"name": "Status",
"resource_subtype": "enum",
"display_value": "On Track",
"enum_value": {
"gid": "789",
"enabled": true,
"color": "green"
}
}"#;
let value: CustomFieldValue = serde_json::from_str(json).unwrap();
let ev = value.enum_value.unwrap();
assert_eq!(ev.gid, "789");
assert_eq!(ev.name, None);
assert!(ev.enabled);
}
#[test]
fn test_deserialize_custom_field_value() {
let json = r#"{
"gid": "456",
"name": "Status",
"resource_subtype": "enum",
"display_value": "On Track",
"enum_value": {
"gid": "789",
"name": "On Track",
"enabled": true,
"color": "green"
}
}"#;
let value: CustomFieldValue = serde_json::from_str(json).unwrap();
assert_eq!(value.gid, "456");
assert_eq!(value.name, Some("Status".to_string()));
assert!(value.enum_value.is_some());
}
#[test]
fn test_extract_status_field_exact_match() {
let fields = vec![
CustomFieldValue {
gid: "1".to_string(),
name: Some("Priority".to_string()),
resource_subtype: Some(CustomFieldType::Enum),
display_value: Some("High".to_string()),
text_value: None,
number_value: None,
enum_value: Some(EnumOption {
gid: "e1".to_string(),
name: Some("High".to_string()),
enabled: true,
color: Some("red".to_string()),
}),
multi_enum_values: vec![],
date_value: None,
},
CustomFieldValue {
gid: "2".to_string(),
name: Some("Status".to_string()),
resource_subtype: Some(CustomFieldType::Enum),
display_value: Some("On Track".to_string()),
text_value: None,
number_value: None,
enum_value: Some(EnumOption {
gid: "e2".to_string(),
name: Some("On Track".to_string()),
enabled: true,
color: Some("green".to_string()),
}),
multi_enum_values: vec![],
date_value: None,
},
];
let status = extract_status_field(&fields, None).unwrap();
assert_eq!(status.field_name, "Status");
assert_eq!(status.value, Some("On Track".to_string()));
assert_eq!(status.status_color, StatusColor::Green);
}
#[test]
fn test_extract_status_field_contains() {
let fields = vec![CustomFieldValue {
gid: "1".to_string(),
name: Some("Project Status".to_string()),
resource_subtype: Some(CustomFieldType::Enum),
display_value: Some("At Risk".to_string()),
text_value: None,
number_value: None,
enum_value: Some(EnumOption {
gid: "e1".to_string(),
name: Some("At Risk".to_string()),
enabled: true,
color: Some("yellow".to_string()),
}),
multi_enum_values: vec![],
date_value: None,
}];
let status = extract_status_field(&fields, None).unwrap();
assert_eq!(status.field_name, "Project Status");
assert_eq!(status.status_color, StatusColor::Yellow);
}
#[test]
fn test_map_color_to_status() {
assert_eq!(
map_color_to_status(&Some("green".to_string()), &None),
StatusColor::Green
);
assert_eq!(
map_color_to_status(&Some("yellow-orange".to_string()), &None),
StatusColor::Yellow
);
assert_eq!(
map_color_to_status(&None, &Some("On Track".to_string())),
StatusColor::Green
);
assert_eq!(
map_color_to_status(&None, &Some("At Risk".to_string())),
StatusColor::Yellow
);
assert_eq!(
map_color_to_status(&None, &Some("Off Track".to_string())),
StatusColor::Red
);
}
}