Files
dreport/core/src/models.rs
2026-04-09 00:15:05 +03:00

793 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<f64>,
pub min_height: Option<f64>,
pub max_width: Option<f64>,
pub max_height: Option<f64>,
}
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<f64>,
pub font_weight: Option<String>,
pub font_style: Option<String>,
pub font_family: Option<String>,
pub color: Option<String>,
pub align: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct LineStyle {
pub stroke_color: Option<String>,
pub stroke_width: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ContainerStyle {
pub background_color: Option<String>,
pub border_color: Option<String>,
pub border_width: Option<f64>,
pub border_radius: Option<f64>,
pub border_style: Option<String>,
}
// --- 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<serde_json::Value>,
}
// --- 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<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct TableStyle {
pub header_bg: Option<String>,
pub header_color: Option<String>,
pub zebra_odd: Option<String>,
pub zebra_even: Option<String>,
pub border_color: Option<String>,
pub border_width: Option<f64>,
pub font_size: Option<f64>,
pub header_font_size: Option<f64>,
/// Hücre iç boşluğu — yatay (sol+sağ), mm cinsinden. Default: 2.0
pub cell_padding_h: Option<f64>,
/// Hücre iç boşluğu — dikey (üst+alt), mm cinsinden. Default: 1.0
pub cell_padding_v: Option<f64>,
/// Header hücre iç boşluğu — yatay (sol+sağ), mm cinsinden. Default: cell_padding_h
pub header_padding_h: Option<f64>,
/// Header hücre iç boşluğu — dikey (üst+alt), mm cinsinden. Default: cell_padding_v
pub header_padding_v: Option<f64>,
}
// --- Barcode ---
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct BarcodeStyle {
pub color: Option<String>,
pub include_text: Option<bool>,
}
// --- Rich Text ---
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichTextSpan {
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub binding: Option<ScalarBinding>,
#[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<f64>,
pub color: Option<String>,
pub align: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ChartLegend {
pub show: bool,
pub position: Option<String>,
pub font_size: Option<f64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ChartLabels {
pub show: bool,
pub font_size: Option<f64>,
pub color: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ChartAxis {
pub x_label: Option<String>,
pub y_label: Option<String>,
pub show_grid: Option<bool>,
pub grid_color: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ChartStyle {
pub colors: Option<Vec<String>>,
pub background_color: Option<String>,
pub bar_gap: Option<f64>,
pub line_width: Option<f64>,
pub show_points: Option<bool>,
pub curve_type: Option<String>,
pub inner_radius: Option<f64>,
}
#[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<String>,
#[serde(default)]
pub group_mode: Option<GroupMode>,
#[serde(default)]
pub title: Option<ChartTitle>,
#[serde(default)]
pub legend: Option<ChartLegend>,
#[serde(default)]
pub labels: Option<ChartLabels>,
#[serde(default)]
pub axis: Option<ChartAxis>,
#[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<Condition>,
#[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<String>,
}
#[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<ChartElement>),
}
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<RichTextSpan>,
}
#[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<TemplateElement>,
#[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<String>,
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<String>,
pub binding: Option<ScalarBinding>,
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<String>,
}
#[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<String>,
pub binding: Option<ScalarBinding>,
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<TableColumn>,
pub style: TableStyle,
#[serde(default = "default_true")]
pub repeat_header: Option<bool>,
}
fn default_true() -> Option<bool> {
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<String>,
}
#[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<f64>, // mm — kare boyutu
pub check_color: Option<String>, // checkmark rengi
pub border_color: Option<String>, // kare kenar rengi
pub border_width: Option<f64>, // kenar kalınlığı
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckboxElement {
#[serde(flatten)]
pub base: ElementBase,
pub checked: Option<bool>, // statik değer
pub binding: Option<ScalarBinding>, // 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<String>,
}
// --- Template ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Template {
pub id: String,
pub name: String,
pub page: PageSettings,
pub fonts: Vec<String>,
#[serde(default)]
pub header: Option<ContainerElement>,
#[serde(default)]
pub footer: Option<ContainerElement>,
pub root: ContainerElement,
#[serde(default)]
pub format_config: Option<FormatConfig>,
/// 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<String>,
}
/// 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()
}
}
}