use serde::{Deserialize, Serialize}; // --- Boyut sistemi --- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum SizeValue { #[serde(rename = "fixed")] Fixed { value: f64 }, #[serde(rename = "auto")] Auto, #[serde(rename = "fr")] Fr { value: f64 }, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct SizeConstraint { pub width: SizeValue, pub height: SizeValue, pub min_width: Option, pub min_height: Option, pub max_width: Option, pub max_height: Option, } impl Default for SizeConstraint { fn default() -> Self { Self { width: SizeValue::Auto, height: SizeValue::Auto, min_width: None, min_height: None, max_width: None, max_height: None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PageSettings { pub width: f64, pub height: f64, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Padding { #[serde(default)] pub top: f64, #[serde(default)] pub right: f64, #[serde(default)] pub bottom: f64, #[serde(default)] pub left: f64, } // --- Positioning --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(tag = "type")] pub enum PositionMode { #[default] #[serde(rename = "flow")] Flow, #[serde(rename = "absolute")] Absolute { x: f64, y: f64 }, } // --- Stil --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct TextStyle { pub font_size: Option, pub font_weight: Option, pub font_style: Option, pub font_family: Option, pub color: Option, pub align: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct LineStyle { pub stroke_color: Option, pub stroke_width: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ContainerStyle { pub background_color: Option, pub border_color: Option, pub border_width: Option, pub border_radius: Option, pub border_style: Option, } // --- Condition (v-if benzeri koşullu gösterim) --- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Condition { /// Data JSON'daki alan yolu (ör: "toplamlar.iskonto") pub path: String, /// Karşılaştırma operatörü: eq, neq, gt, gte, lt, lte, empty, not_empty pub operator: String, /// Karşılaştırılacak değer (empty/not_empty için gerekmez) #[serde(default)] pub value: Option, } // --- Binding --- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScalarBinding { pub path: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ArrayBinding { pub path: String, } // --- Tablo --- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TableColumn { pub id: String, pub field: String, pub title: String, pub width: SizeValue, pub align: String, pub format: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct TableStyle { pub header_bg: Option, pub header_color: Option, pub zebra_odd: Option, pub zebra_even: Option, pub border_color: Option, pub border_width: Option, pub font_size: Option, pub header_font_size: Option, /// Hücre iç boşluğu — yatay (sol+sağ), mm cinsinden. Default: 2.0 pub cell_padding_h: Option, /// Hücre iç boşluğu — dikey (üst+alt), mm cinsinden. Default: 1.0 pub cell_padding_v: Option, /// Header hücre iç boşluğu — yatay (sol+sağ), mm cinsinden. Default: cell_padding_h pub header_padding_h: Option, /// Header hücre iç boşluğu — dikey (üst+alt), mm cinsinden. Default: cell_padding_v pub header_padding_v: Option, } // --- Barcode --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct BarcodeStyle { pub color: Option, pub include_text: Option, } // --- Rich Text --- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RichTextSpan { #[serde(default)] pub text: Option, #[serde(default)] pub binding: Option, #[serde(default)] pub style: TextStyle, } // --- Chart --- #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ChartType { Bar, Line, Pie, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum GroupMode { Grouped, Stacked, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ChartTitle { pub text: String, pub font_size: Option, pub color: Option, pub align: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ChartLegend { pub show: bool, pub position: Option, pub font_size: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ChartLabels { pub show: bool, pub font_size: Option, pub color: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ChartAxis { pub x_label: Option, pub y_label: Option, pub show_grid: Option, pub grid_color: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ChartStyle { pub colors: Option>, pub background_color: Option, pub bar_gap: Option, pub line_width: Option, pub show_points: Option, pub curve_type: Option, pub inner_radius: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChartElement { #[serde(flatten)] pub base: ElementBase, pub chart_type: ChartType, pub data_source: ArrayBinding, pub category_field: String, pub value_field: String, #[serde(default)] pub group_field: Option, #[serde(default)] pub group_mode: Option, #[serde(default)] pub title: Option, #[serde(default)] pub legend: Option, #[serde(default)] pub labels: Option, #[serde(default)] pub axis: Option, #[serde(default)] pub style: ChartStyle, } // --- Element Base (ortak alanlar) --- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ElementBase { pub id: String, #[serde(default)] pub condition: Option, #[serde(default)] pub position: PositionMode, #[serde(default)] pub size: SizeConstraint, } impl ElementBase { /// Flow pozisyonlu, condition'sız, verilen size ile base oluştur pub fn flow(id: String, size: SizeConstraint) -> Self { Self { id, condition: None, position: PositionMode::Flow, size, } } } pub trait HasBase { fn base(&self) -> &ElementBase; fn base_mut(&mut self) -> &mut ElementBase; } macro_rules! impl_has_base { ($($t:ty),+ $(,)?) => { $(impl HasBase for $t { fn base(&self) -> &ElementBase { &self.base } fn base_mut(&mut self) -> &mut ElementBase { &mut self.base } })+ }; } impl_has_base!( ContainerElement, StaticTextElement, TextElement, LineElement, ImageElement, PageNumberElement, BarcodeElement, RepeatingTableElement, PageBreakElement, CurrentDateElement, ShapeElement, CheckboxElement, CalculatedTextElement, RichTextElement, ChartElement, ); pub trait ElementTypeStr { fn type_str(&self) -> &'static str; } macro_rules! impl_type_str { ($($t:ty => $s:literal),+ $(,)?) => { $(impl ElementTypeStr for $t { fn type_str(&self) -> &'static str { $s } })+ }; } impl_type_str!( ContainerElement => "container", StaticTextElement => "static_text", TextElement => "text", LineElement => "line", ImageElement => "image", PageNumberElement => "page_number", BarcodeElement => "barcode", RepeatingTableElement => "repeating_table", PageBreakElement => "page_break", CurrentDateElement => "current_date", ShapeElement => "shape", CheckboxElement => "checkbox", CalculatedTextElement => "calculated_text", RichTextElement => "rich_text", ChartElement => "chart", ); pub trait HasTextStyle { fn text_style(&self) -> &TextStyle; } macro_rules! impl_has_text_style { ($($t:ty),+ $(,)?) => { $(impl HasTextStyle for $t { fn text_style(&self) -> &TextStyle { &self.style } })+ }; } impl_has_text_style!( StaticTextElement, TextElement, PageNumberElement, CurrentDateElement, CalculatedTextElement, RichTextElement, ); pub trait HasOptionalBinding { fn binding(&self) -> Option<&ScalarBinding>; fn static_value(&self) -> Option<&str>; } impl HasOptionalBinding for ImageElement { fn binding(&self) -> Option<&ScalarBinding> { self.binding.as_ref() } fn static_value(&self) -> Option<&str> { self.src.as_deref() } } impl HasOptionalBinding for BarcodeElement { fn binding(&self) -> Option<&ScalarBinding> { self.binding.as_ref() } fn static_value(&self) -> Option<&str> { self.value.as_deref() } } // --- Element tipleri --- #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct ImageStyle { pub object_fit: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum TemplateElement { #[serde(rename = "container")] Container(ContainerElement), #[serde(rename = "static_text")] StaticText(StaticTextElement), #[serde(rename = "text")] Text(TextElement), #[serde(rename = "line")] Line(LineElement), #[serde(rename = "repeating_table")] RepeatingTable(RepeatingTableElement), #[serde(rename = "image")] Image(ImageElement), #[serde(rename = "page_number")] PageNumber(PageNumberElement), #[serde(rename = "barcode")] Barcode(BarcodeElement), #[serde(rename = "page_break")] PageBreak(PageBreakElement), #[serde(rename = "current_date")] CurrentDate(CurrentDateElement), #[serde(rename = "shape")] Shape(ShapeElement), #[serde(rename = "checkbox")] Checkbox(CheckboxElement), #[serde(rename = "calculated_text")] CalculatedText(CalculatedTextElement), #[serde(rename = "rich_text")] RichText(RichTextElement), #[serde(rename = "chart")] Chart(Box), } impl TemplateElement { fn inner_base(&self) -> &ElementBase { match self { Self::Container(e) => e.base(), Self::StaticText(e) => e.base(), Self::Text(e) => e.base(), Self::Line(e) => e.base(), Self::RepeatingTable(e) => e.base(), Self::Image(e) => e.base(), Self::PageNumber(e) => e.base(), Self::Barcode(e) => e.base(), Self::PageBreak(e) => e.base(), Self::CurrentDate(e) => e.base(), Self::Shape(e) => e.base(), Self::Checkbox(e) => e.base(), Self::CalculatedText(e) => e.base(), Self::RichText(e) => e.base(), Self::Chart(e) => e.base(), } } pub fn id(&self) -> &str { &self.inner_base().id } pub fn position(&self) -> &PositionMode { &self.inner_base().position } pub fn condition(&self) -> Option<&Condition> { self.inner_base().condition.as_ref() } pub fn size(&self) -> &SizeConstraint { &self.inner_base().size } pub fn type_str(&self) -> &'static str { match self { Self::Container(e) => e.type_str(), Self::StaticText(e) => e.type_str(), Self::Text(e) => e.type_str(), Self::Line(e) => e.type_str(), Self::RepeatingTable(e) => e.type_str(), Self::Image(e) => e.type_str(), Self::PageNumber(e) => e.type_str(), Self::Barcode(e) => e.type_str(), Self::PageBreak(e) => e.type_str(), Self::CurrentDate(e) => e.type_str(), Self::Shape(e) => e.type_str(), Self::Checkbox(e) => e.type_str(), Self::CalculatedText(e) => e.type_str(), Self::RichText(e) => e.type_str(), Self::Chart(e) => e.type_str(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RichTextElement { #[serde(flatten)] pub base: ElementBase, #[serde(default)] pub style: TextStyle, // varsayilan stil (span'lar override edebilir) pub content: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContainerElement { #[serde(flatten)] pub base: ElementBase, #[serde(default = "default_column")] pub direction: String, #[serde(default)] pub gap: f64, #[serde(default)] pub padding: Padding, #[serde(default = "default_stretch")] pub align: String, #[serde(default = "default_start")] pub justify: String, #[serde(default)] pub style: ContainerStyle, #[serde(default)] pub children: Vec, #[serde(default = "default_auto")] pub break_inside: String, } fn default_auto() -> String { "auto".to_string() } fn default_column() -> String { "column".to_string() } fn default_stretch() -> String { "stretch".to_string() } fn default_start() -> String { "start".to_string() } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StaticTextElement { #[serde(flatten)] pub base: ElementBase, pub style: TextStyle, pub content: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TextElement { #[serde(flatten)] pub base: ElementBase, pub style: TextStyle, pub content: Option, pub binding: ScalarBinding, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LineElement { #[serde(flatten)] pub base: ElementBase, pub style: LineStyle, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImageElement { #[serde(flatten)] pub base: ElementBase, pub src: Option, pub binding: Option, pub style: ImageStyle, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PageNumberElement { #[serde(flatten)] pub base: ElementBase, pub style: TextStyle, pub format: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BarcodeElement { #[serde(flatten)] pub base: ElementBase, pub format: String, // qr, ean13, ean8, code128, code39 pub value: Option, pub binding: Option, pub style: BarcodeStyle, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RepeatingTableElement { #[serde(flatten)] pub base: ElementBase, pub data_source: ArrayBinding, pub columns: Vec, pub style: TableStyle, #[serde(default = "default_true")] pub repeat_header: Option, } fn default_true() -> Option { Some(true) } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PageBreakElement { #[serde(flatten)] pub base: ElementBase, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CurrentDateElement { #[serde(flatten)] pub base: ElementBase, pub style: TextStyle, pub format: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShapeElement { #[serde(flatten)] pub base: ElementBase, pub shape_type: String, // rectangle, ellipse, rounded_rectangle pub style: ContainerStyle, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct CheckboxStyle { pub size: Option, // mm — kare boyutu pub check_color: Option, // checkmark rengi pub border_color: Option, // kare kenar rengi pub border_width: Option, // kenar kalınlığı } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CheckboxElement { #[serde(flatten)] pub base: ElementBase, pub checked: Option, // statik değer pub binding: Option, // dinamik boolean binding pub style: CheckboxStyle, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CalculatedTextElement { #[serde(flatten)] pub base: ElementBase, pub style: TextStyle, pub expression: String, pub format: Option, } // --- Template --- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Template { pub id: String, pub name: String, pub page: PageSettings, pub fonts: Vec, #[serde(default)] pub header: Option, #[serde(default)] pub footer: Option, pub root: ContainerElement, #[serde(default)] pub format_config: Option, /// Lokalizasyon: "tr-TR", "en-US", "de-DE", "fr-FR" vb. /// Belirtilirse ve format_config yoksa, locale'den FormatConfig türetilir. #[serde(default)] pub locale: Option, } /// Sayı/para birimi formatlama ayarları. /// Belirtilmezse Türk Lirası varsayılan (. binlik, , ondalık, ₺ sembol). #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct FormatConfig { /// Binlik ayırıcı (varsayılan ".") #[serde(default = "FormatConfig::default_thousands_sep")] pub thousands_separator: String, /// Ondalık ayırıcı (varsayılan ",") #[serde(default = "FormatConfig::default_decimal_sep")] pub decimal_separator: String, /// Para birimi sembolü (varsayılan "₺") #[serde(default = "FormatConfig::default_currency_symbol")] pub currency_symbol: String, /// Para birimi sembolü pozisyonu: "suffix" (varsayılan) veya "prefix" #[serde(default = "FormatConfig::default_currency_position")] pub currency_position: String, } impl FormatConfig { fn default_thousands_sep() -> String { ".".to_string() } fn default_decimal_sep() -> String { ",".to_string() } fn default_currency_symbol() -> String { "₺".to_string() } fn default_currency_position() -> String { "suffix".to_string() } } impl Default for FormatConfig { fn default() -> Self { Self { thousands_separator: Self::default_thousands_sep(), decimal_separator: Self::default_decimal_sep(), currency_symbol: Self::default_currency_symbol(), currency_position: Self::default_currency_position(), } } } impl FormatConfig { /// Locale string'inden FormatConfig türet. /// Desteklenen locale'ler: tr-TR, en-US, de-DE, fr-FR. /// Bilinmeyen locale → Türk formatı (varsayılan). pub fn from_locale(locale: &str) -> Self { match locale { "en-US" | "en" => Self { thousands_separator: ",".to_string(), decimal_separator: ".".to_string(), currency_symbol: "$".to_string(), currency_position: "prefix".to_string(), }, "de-DE" | "de" => Self { thousands_separator: ".".to_string(), decimal_separator: ",".to_string(), currency_symbol: "€".to_string(), currency_position: "suffix".to_string(), }, "fr-FR" | "fr" => Self { thousands_separator: " ".to_string(), decimal_separator: ",".to_string(), currency_symbol: "€".to_string(), currency_position: "suffix".to_string(), }, "en-GB" | "gb" => Self { thousands_separator: ",".to_string(), decimal_separator: ".".to_string(), currency_symbol: "£".to_string(), currency_position: "prefix".to_string(), }, // tr-TR veya bilinmeyen → Türk formatı _ => Self::default(), } } } impl Template { /// Template'in etkin FormatConfig'ini döndür. /// Öncelik: format_config > locale > varsayılan (tr-TR). pub fn effective_format_config(&self) -> FormatConfig { if let Some(ref fc) = self.format_config { fc.clone() } else if let Some(ref locale) = self.locale { FormatConfig::from_locale(locale) } else { FormatConfig::default() } } }