add elements

This commit is contained in:
2026-04-03 01:26:54 +03:00
parent f0a1835fa2
commit 7684a2a871
29 changed files with 3600 additions and 177 deletions

View File

@@ -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(),