mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
add elements
This commit is contained in:
@@ -2,6 +2,72 @@ use dreport_core::models::*;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Şu anki tarihi verilen format string'ine göre formatla.
|
||||
/// Desteklenen tokenlar: YYYY, MM, DD, HH, mm, ss
|
||||
/// WASM'da js_sys::Date, native'de SystemTime kullanır.
|
||||
fn format_current_date(fmt: &str) -> String {
|
||||
let (year, month, day, hour, minute, second) = current_datetime_parts();
|
||||
fmt.replace("YYYY", &format!("{:04}", year))
|
||||
.replace("MM", &format!("{:02}", month))
|
||||
.replace("DD", &format!("{:02}", day))
|
||||
.replace("HH", &format!("{:02}", hour))
|
||||
.replace("mm", &format!("{:02}", minute))
|
||||
.replace("ss", &format!("{:02}", second))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn current_datetime_parts() -> (i32, u32, u32, u32, u32, u32) {
|
||||
let d = js_sys::Date::new_0();
|
||||
(
|
||||
d.get_full_year() as i32,
|
||||
d.get_month() as u32 + 1, // JS months are 0-based
|
||||
d.get_date() as u32,
|
||||
d.get_hours() as u32,
|
||||
d.get_minutes() as u32,
|
||||
d.get_seconds() as u32,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn current_datetime_parts() -> (i32, u32, u32, u32, u32, u32) {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
// Simple UTC date calculation (no timezone dependency)
|
||||
let days = (secs / 86400) as i64;
|
||||
let time_of_day = secs % 86400;
|
||||
let hour = (time_of_day / 3600) as u32;
|
||||
let minute = ((time_of_day % 3600) / 60) as u32;
|
||||
let second = (time_of_day % 60) as u32;
|
||||
|
||||
// Days since 1970-01-01 → year/month/day (civil calendar)
|
||||
// Algorithm from Howard Hinnant's chrono-compatible date library
|
||||
let z = days + 719468;
|
||||
let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
|
||||
let doe = (z - era * 146097) as u32;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe as i64 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
|
||||
(y as i32, m, d, hour, minute, second)
|
||||
}
|
||||
|
||||
/// Çözümlenmiş rich text span'ı
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedRichSpan {
|
||||
pub text: String,
|
||||
pub font_size: Option<f64>,
|
||||
pub font_weight: Option<String>,
|
||||
pub font_family: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
/// Her element ID'si için çözümlenmiş text içeriğini tutar.
|
||||
/// Table ve barcode gibi özel tipler de burada çözülür.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -14,6 +80,10 @@ pub struct ResolvedData {
|
||||
pub barcodes: HashMap<String, String>,
|
||||
/// element_id → çözümlenmiş image src
|
||||
pub images: HashMap<String, String>,
|
||||
/// page_number element_id → format string (sayfa bölme sonrası çözülecek)
|
||||
pub page_number_formats: HashMap<String, String>,
|
||||
/// element_id → çözümlenmiş rich text span listesi
|
||||
pub rich_texts: HashMap<String, Vec<ResolvedRichSpan>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -51,8 +121,16 @@ pub fn resolve_template(template: &Template, data: &Value) -> ResolvedData {
|
||||
tables: HashMap::new(),
|
||||
barcodes: HashMap::new(),
|
||||
images: HashMap::new(),
|
||||
page_number_formats: HashMap::new(),
|
||||
rich_texts: HashMap::new(),
|
||||
};
|
||||
if let Some(ref header) = template.header {
|
||||
resolve_element(&TemplateElement::Container(header.clone()), data, &mut resolved);
|
||||
}
|
||||
resolve_element(&TemplateElement::Container(template.root.clone()), data, &mut resolved);
|
||||
if let Some(ref footer) = template.footer {
|
||||
resolve_element(&TemplateElement::Container(footer.clone()), data, &mut resolved);
|
||||
}
|
||||
resolved
|
||||
}
|
||||
|
||||
@@ -70,8 +148,10 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
|
||||
resolved.texts.insert(e.id.clone(), text);
|
||||
}
|
||||
TemplateElement::PageNumber(e) => {
|
||||
// Sayfa numarası layout sonrasında çözülecek, placeholder koy
|
||||
let fmt = e.format.as_deref().unwrap_or("{current} / {total}");
|
||||
// Format string'i sakla — sayfa bölme sonrası gerçek değerlerle çözülecek
|
||||
let fmt = e.format.as_deref().unwrap_or("{current} / {total}").to_string();
|
||||
resolved.page_number_formats.insert(e.id.clone(), fmt.clone());
|
||||
// Placeholder koy (tek sayfalık fallback)
|
||||
resolved.texts.insert(e.id.clone(), fmt.replace("{current}", "1").replace("{total}", "1"));
|
||||
}
|
||||
TemplateElement::Barcode(e) => {
|
||||
@@ -116,7 +196,59 @@ fn resolve_element(el: &TemplateElement, data: &Value, resolved: &mut ResolvedDa
|
||||
resolve_element(child, data, resolved);
|
||||
}
|
||||
}
|
||||
TemplateElement::CurrentDate(e) => {
|
||||
let fmt = e.format.as_deref().unwrap_or("DD.MM.YYYY");
|
||||
let text = format_current_date(fmt);
|
||||
resolved.texts.insert(e.id.clone(), text);
|
||||
}
|
||||
TemplateElement::Checkbox(e) => {
|
||||
let checked = if let Some(binding) = &e.binding {
|
||||
let val = resolve_path(data, &binding.path);
|
||||
match val {
|
||||
Value::Bool(b) => *b,
|
||||
Value::Number(n) => n.as_f64().unwrap_or(0.0) != 0.0,
|
||||
Value::String(s) => s == "true" || s == "1",
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
e.checked.unwrap_or(false)
|
||||
};
|
||||
// Store as "true"/"false" string in texts map
|
||||
resolved.texts.insert(e.id.clone(), checked.to_string());
|
||||
}
|
||||
TemplateElement::CalculatedText(e) => {
|
||||
let result = crate::expr_eval::evaluate_expression(&e.expression, data);
|
||||
let formatted = crate::expr_eval::apply_format(&result, e.format.as_deref());
|
||||
resolved.texts.insert(e.id.clone(), formatted);
|
||||
}
|
||||
TemplateElement::RichText(e) => {
|
||||
let spans: Vec<ResolvedRichSpan> = e
|
||||
.content
|
||||
.iter()
|
||||
.map(|span| {
|
||||
let text = if let Some(ref binding) = span.binding {
|
||||
let bound = value_to_string(resolve_path(data, &binding.path));
|
||||
match &span.text {
|
||||
Some(prefix) if !prefix.is_empty() => format!("{}{}", prefix, bound),
|
||||
_ => bound,
|
||||
}
|
||||
} else {
|
||||
span.text.clone().unwrap_or_default()
|
||||
};
|
||||
ResolvedRichSpan {
|
||||
text,
|
||||
font_size: span.style.font_size.or(e.style.font_size),
|
||||
font_weight: span.style.font_weight.clone().or(e.style.font_weight.clone()),
|
||||
font_family: span.style.font_family.clone().or(e.style.font_family.clone()),
|
||||
color: span.style.color.clone().or(e.style.color.clone()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
resolved.rich_texts.insert(e.id.clone(), spans);
|
||||
}
|
||||
TemplateElement::Line(_) => {}
|
||||
TemplateElement::Shape(_) => {}
|
||||
TemplateElement::PageBreak(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +324,8 @@ mod tests {
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings { width: 210.0, height: 297.0 },
|
||||
fonts: vec![],
|
||||
header: None,
|
||||
footer: None,
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
@@ -202,6 +336,7 @@ mod tests {
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
break_inside: "auto".to_string(),
|
||||
children: vec![
|
||||
TemplateElement::Text(TextElement {
|
||||
id: "el_name".to_string(),
|
||||
@@ -233,6 +368,8 @@ mod tests {
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings { width: 210.0, height: 297.0 },
|
||||
fonts: vec![],
|
||||
header: None,
|
||||
footer: None,
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
@@ -243,6 +380,7 @@ mod tests {
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
break_inside: "auto".to_string(),
|
||||
children: vec![
|
||||
TemplateElement::Text(TextElement {
|
||||
id: "el_no".to_string(),
|
||||
@@ -274,6 +412,8 @@ mod tests {
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings { width: 210.0, height: 297.0 },
|
||||
fonts: vec![],
|
||||
header: None,
|
||||
footer: None,
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
@@ -284,6 +424,7 @@ mod tests {
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
break_inside: "auto".to_string(),
|
||||
children: vec![
|
||||
TemplateElement::StaticText(StaticTextElement {
|
||||
id: "title".to_string(),
|
||||
@@ -307,6 +448,8 @@ mod tests {
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings { width: 210.0, height: 297.0 },
|
||||
fonts: vec![],
|
||||
header: None,
|
||||
footer: None,
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
@@ -317,6 +460,7 @@ mod tests {
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
break_inside: "auto".to_string(),
|
||||
children: vec![
|
||||
TemplateElement::RepeatingTable(RepeatingTableElement {
|
||||
id: "tbl".to_string(),
|
||||
@@ -342,6 +486,7 @@ mod tests {
|
||||
},
|
||||
],
|
||||
style: TableStyle::default(),
|
||||
repeat_header: Some(true),
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -368,6 +513,8 @@ mod tests {
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings { width: 210.0, height: 297.0 },
|
||||
fonts: vec![],
|
||||
header: None,
|
||||
footer: None,
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
@@ -378,6 +525,7 @@ mod tests {
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
break_inside: "auto".to_string(),
|
||||
children: vec![
|
||||
TemplateElement::RepeatingTable(RepeatingTableElement {
|
||||
id: "tbl".to_string(),
|
||||
@@ -395,6 +543,7 @@ mod tests {
|
||||
},
|
||||
],
|
||||
style: TableStyle::default(),
|
||||
repeat_header: Some(true),
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -413,6 +562,8 @@ mod tests {
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings { width: 210.0, height: 297.0 },
|
||||
fonts: vec![],
|
||||
header: None,
|
||||
footer: None,
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
@@ -423,6 +574,7 @@ mod tests {
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
break_inside: "auto".to_string(),
|
||||
children: vec![
|
||||
TemplateElement::Text(TextElement {
|
||||
id: "el_missing".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user