import 'dart:convert'; import 'package:flutter/material.dart'; enum BarcodeType { code128, ean13, code39, upca, qrCode, } enum LabelOrientation { portrait, landscape, } enum ElementType { text, dynamicText, barcode, qrCode, rectangle, line, } class LabelConfiguration { final double widthMm; final double heightMm; final double marginTopMm; final double marginBottomMm; final double marginLeftMm; final double marginRightMm; final int dpi; // 203, 300, 600 final LabelOrientation orientation; final String printerLanguage; // ZPL, TSPL, ESCPOS, PCL final int columnsAcross; // 1-Up (single), 2-Up (twin track), 3-Up, 4-Up final double horizontalGapMm; // Gap between side-by-side labels in mm final double verticalGapMm; // Gap between consecutive labels along roll feed in mm const LabelConfiguration({ this.widthMm = 50.0, this.heightMm = 25.0, this.marginTopMm = 1.0, this.marginBottomMm = 1.0, this.marginLeftMm = 1.0, this.marginRightMm = 1.0, this.dpi = 203, this.orientation = LabelOrientation.portrait, this.printerLanguage = 'ZPL', this.columnsAcross = 1, this.horizontalGapMm = 2.0, this.verticalGapMm = 2.0, }); /// Total roll web width including all columns and gaps between them double get totalWebWidthMm { final cols = columnsAcross.clamp(1, 4); if (cols <= 1) return widthMm; return (widthMm * cols) + (horizontalGapMm * (cols - 1)); } int get totalWebWidthDots => mmToDots(totalWebWidthMm); LabelConfiguration copyWith({ double? widthMm, double? heightMm, double? marginTopMm, double? marginBottomMm, double? marginLeftMm, double? marginRightMm, int? dpi, LabelOrientation? orientation, String? printerLanguage, int? columnsAcross, double? horizontalGapMm, double? verticalGapMm, }) { return LabelConfiguration( widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, marginTopMm: marginTopMm ?? this.marginTopMm, marginBottomMm: marginBottomMm ?? this.marginBottomMm, marginLeftMm: marginLeftMm ?? this.marginLeftMm, marginRightMm: marginRightMm ?? this.marginRightMm, dpi: dpi ?? this.dpi, orientation: orientation ?? this.orientation, printerLanguage: printerLanguage ?? this.printerLanguage, columnsAcross: columnsAcross ?? this.columnsAcross, horizontalGapMm: horizontalGapMm ?? this.horizontalGapMm, verticalGapMm: verticalGapMm ?? this.verticalGapMm, ); } Map toJson() => { 'widthMm': widthMm, 'heightMm': heightMm, 'marginTopMm': marginTopMm, 'marginBottomMm': marginBottomMm, 'marginLeftMm': marginLeftMm, 'marginRightMm': marginRightMm, 'dpi': dpi, 'orientation': orientation.name, 'printerLanguage': printerLanguage, 'columnsAcross': columnsAcross, 'horizontalGapMm': horizontalGapMm, 'verticalGapMm': verticalGapMm, }; factory LabelConfiguration.fromJson(Map json) { return LabelConfiguration( widthMm: (json['widthMm'] as num?)?.toDouble() ?? 50.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 25.0, marginTopMm: (json['marginTopMm'] as num?)?.toDouble() ?? 1.0, marginBottomMm: (json['marginBottomMm'] as num?)?.toDouble() ?? 1.0, marginLeftMm: (json['marginLeftMm'] as num?)?.toDouble() ?? 1.0, marginRightMm: (json['marginRightMm'] as num?)?.toDouble() ?? 1.0, dpi: json['dpi'] as int? ?? 203, orientation: json['orientation'] == 'landscape' ? LabelOrientation.landscape : LabelOrientation.portrait, printerLanguage: json['printerLanguage'] as String? ?? 'ZPL', columnsAcross: (json['columnsAcross'] as num?)?.toInt() ?? 1, horizontalGapMm: (json['horizontalGapMm'] as num?)?.toDouble() ?? 2.0, verticalGapMm: (json['verticalGapMm'] as num?)?.toDouble() ?? 2.0, ); } int mmToDots(double mm) => ((mm / 25.4) * dpi).round(); int get widthDots => mmToDots(widthMm); int get heightDots => mmToDots(heightMm); } abstract class LabelElement { final String id; final ElementType type; final double xMm; final double yMm; final double widthMm; final double heightMm; final double rotation; // 0, 90, 180, 270 final bool isLocked; final int zIndex; const LabelElement({ required this.id, required this.type, required this.xMm, required this.yMm, required this.widthMm, required this.heightMm, this.rotation = 0.0, this.isLocked = false, this.zIndex = 0, }); LabelElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, }); double get borderWidthMm => 0.0; Map toJson(); static LabelElement fromJson(Map json) { final typeStr = json['type'] as String? ?? 'text'; switch (typeStr) { case 'dynamicText': return DynamicTextElement.fromJson(json); case 'barcode': return BarcodeElement.fromJson(json); case 'qrCode': return QrCodeElement.fromJson(json); case 'rectangle': return RectangleElement.fromJson(json); case 'line': return LineElement.fromJson(json); case 'text': default: return TextElement.fromJson(json); } } } class TextElement extends LabelElement { final String text; final String fontFamily; final double fontSizePt; final bool isBold; final bool isItalic; final TextAlign alignment; @override final double borderWidthMm; final double paddingMm; const TextElement({ required super.id, required super.xMm, required super.yMm, required super.widthMm, required super.heightMm, super.rotation = 0.0, super.isLocked = false, super.zIndex = 0, this.text = 'kifi', this.fontFamily = 'Roboto', this.fontSizePt = 9.0, this.isBold = false, this.isItalic = false, this.alignment = TextAlign.left, this.borderWidthMm = 0.0, this.paddingMm = 0.0, }) : super(type: ElementType.text); @override TextElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, String? text, String? fontFamily, double? fontSizePt, bool? isBold, bool? isItalic, TextAlign? alignment, double? borderWidthMm, double? paddingMm, }) { return TextElement( id: id, xMm: xMm ?? this.xMm, yMm: yMm ?? this.yMm, widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, rotation: rotation ?? this.rotation, isLocked: isLocked ?? this.isLocked, zIndex: zIndex ?? this.zIndex, text: text ?? this.text, fontFamily: fontFamily ?? this.fontFamily, fontSizePt: fontSizePt ?? this.fontSizePt, isBold: isBold ?? this.isBold, isItalic: isItalic ?? this.isItalic, alignment: alignment ?? this.alignment, borderWidthMm: borderWidthMm ?? this.borderWidthMm, paddingMm: paddingMm ?? this.paddingMm, ); } @override Map toJson() => { 'id': id, 'type': 'text', 'xMm': xMm, 'yMm': yMm, 'widthMm': widthMm, 'heightMm': heightMm, 'rotation': rotation, 'isLocked': isLocked, 'zIndex': zIndex, 'text': text, 'fontFamily': fontFamily, 'fontSizePt': fontSizePt, 'isBold': isBold, 'isItalic': isItalic, 'alignment': alignment.name, 'borderWidthMm': borderWidthMm, 'paddingMm': paddingMm, }; factory TextElement.fromJson(Map json) { return TextElement( id: json['id'] as String, xMm: (json['xMm'] as num?)?.toDouble() ?? 0.0, yMm: (json['yMm'] as num?)?.toDouble() ?? 0.0, widthMm: (json['widthMm'] as num?)?.toDouble() ?? 20.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 5.0, rotation: (json['rotation'] as num?)?.toDouble() ?? 0.0, isLocked: json['isLocked'] as bool? ?? false, zIndex: json['zIndex'] as int? ?? 0, text: json['text'] as String? ?? json['value'] as String? ?? 'kifi', fontFamily: json['fontFamily'] as String? ?? 'Roboto', fontSizePt: (json['fontSizePt'] as num?)?.toDouble() ?? 9.0, isBold: json['isBold'] as bool? ?? false, isItalic: json['isItalic'] as bool? ?? false, alignment: TextAlign.values.where((a) => a.name == json['alignment']).firstOrNull ?? TextAlign.left, borderWidthMm: (json['borderWidthMm'] as num?)?.toDouble() ?? 0.0, paddingMm: (json['paddingMm'] as num?)?.toDouble() ?? 0.0, ); } } class DynamicTextElement extends LabelElement { final String source; // "product" final String fieldKey; // "name", "sellingPrice", etc. final String? prefix; final String? suffix; final String fallbackValue; final String fontFamily; final double fontSizePt; final bool isBold; final bool isItalic; final TextAlign alignment; @override final double borderWidthMm; final double paddingMm; final bool autoFit; final int maxLines; const DynamicTextElement({ required super.id, required super.xMm, required super.yMm, required super.widthMm, required super.heightMm, super.rotation = 0.0, super.isLocked = false, super.zIndex = 0, this.source = 'product', this.fieldKey = '', this.prefix, this.suffix, this.fallbackValue = 'N/A', this.fontFamily = 'Roboto', this.fontSizePt = 9.0, this.isBold = false, this.isItalic = false, this.alignment = TextAlign.left, this.borderWidthMm = 0.0, this.paddingMm = 0.0, this.autoFit = true, this.maxLines = 2, }) : super(type: ElementType.dynamicText); String get placeholder => fieldKey.isNotEmpty ? '{{$source.$fieldKey}}' : '{{select_field}}'; String formatValue(String? resolvedRaw) { if (resolvedRaw == null || resolvedRaw.trim().isEmpty) { return fallbackValue; } final p = prefix ?? ''; final s = suffix ?? ''; return '$p$resolvedRaw$s'; } @override DynamicTextElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, String? source, String? fieldKey, String? prefix, String? suffix, String? fallbackValue, String? fontFamily, double? fontSizePt, bool? isBold, bool? isItalic, TextAlign? alignment, double? borderWidthMm, double? paddingMm, bool? autoFit, int? maxLines, }) { return DynamicTextElement( id: id, xMm: xMm ?? this.xMm, yMm: yMm ?? this.yMm, widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, rotation: rotation ?? this.rotation, isLocked: isLocked ?? this.isLocked, zIndex: zIndex ?? this.zIndex, source: source ?? this.source, fieldKey: fieldKey ?? this.fieldKey, prefix: prefix ?? this.prefix, suffix: suffix ?? this.suffix, fallbackValue: fallbackValue ?? this.fallbackValue, fontFamily: fontFamily ?? this.fontFamily, fontSizePt: fontSizePt ?? this.fontSizePt, isBold: isBold ?? this.isBold, isItalic: isItalic ?? this.isItalic, alignment: alignment ?? this.alignment, borderWidthMm: borderWidthMm ?? this.borderWidthMm, paddingMm: paddingMm ?? this.paddingMm, autoFit: autoFit ?? this.autoFit, maxLines: maxLines ?? this.maxLines, ); } @override Map toJson() => { 'id': id, 'type': 'dynamicText', 'xMm': xMm, 'yMm': yMm, 'widthMm': widthMm, 'heightMm': heightMm, 'rotation': rotation, 'isLocked': isLocked, 'zIndex': zIndex, 'source': source, 'fieldKey': fieldKey, 'placeholder': placeholder, 'prefix': prefix, 'suffix': suffix, 'fallbackValue': fallbackValue, 'fontFamily': fontFamily, 'fontSizePt': fontSizePt, 'isBold': isBold, 'isItalic': isItalic, 'alignment': alignment.name, 'borderWidthMm': borderWidthMm, 'paddingMm': paddingMm, 'autoFit': autoFit, 'maxLines': maxLines, }; factory DynamicTextElement.fromJson(Map json) { return DynamicTextElement( id: json['id'] as String, xMm: (json['xMm'] as num?)?.toDouble() ?? 0.0, yMm: (json['yMm'] as num?)?.toDouble() ?? 0.0, widthMm: (json['widthMm'] as num?)?.toDouble() ?? 30.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 5.0, rotation: (json['rotation'] as num?)?.toDouble() ?? 0.0, isLocked: json['isLocked'] as bool? ?? false, zIndex: json['zIndex'] as int? ?? 0, source: json['source'] as String? ?? 'product', fieldKey: json['fieldKey'] as String? ?? json['field'] as String? ?? '', prefix: json['prefix'] as String?, suffix: json['suffix'] as String?, fallbackValue: json['fallbackValue'] as String? ?? json['fallback'] as String? ?? 'N/A', fontFamily: json['fontFamily'] as String? ?? 'Roboto', fontSizePt: (json['fontSizePt'] as num?)?.toDouble() ?? 9.0, isBold: json['isBold'] as bool? ?? false, isItalic: json['isItalic'] as bool? ?? false, alignment: TextAlign.values.where((a) => a.name == json['alignment']).firstOrNull ?? TextAlign.left, borderWidthMm: (json['borderWidthMm'] as num?)?.toDouble() ?? 0.0, paddingMm: (json['paddingMm'] as num?)?.toDouble() ?? 0.0, autoFit: json['autoFit'] as bool? ?? true, maxLines: json['maxLines'] as int? ?? 2, ); } } class BarcodeElement extends LabelElement { final String valueType; // "static" or "dynamic" final String staticValue; final String source; // "product" final String fieldKey; // "barcode" or "sku" final BarcodeType barcodeType; final bool showText; @override final double borderWidthMm; final double paddingMm; const BarcodeElement({ required super.id, required super.xMm, required super.yMm, required super.widthMm, required super.heightMm, super.rotation = 0.0, super.isLocked = false, super.zIndex = 0, this.valueType = 'dynamic', this.staticValue = '123456789012', this.source = 'product', this.fieldKey = 'barcode', this.barcodeType = BarcodeType.code128, this.showText = true, this.borderWidthMm = 0.0, this.paddingMm = 0.5, }) : super(type: ElementType.barcode); String get placeholder => valueType == 'dynamic' ? '{{$source.$fieldKey}}' : staticValue; @override BarcodeElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, String? valueType, String? staticValue, String? source, String? fieldKey, BarcodeType? barcodeType, bool? showText, double? borderWidthMm, double? paddingMm, }) { return BarcodeElement( id: id, xMm: xMm ?? this.xMm, yMm: yMm ?? this.yMm, widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, rotation: rotation ?? this.rotation, isLocked: isLocked ?? this.isLocked, zIndex: zIndex ?? this.zIndex, valueType: valueType ?? this.valueType, staticValue: staticValue ?? this.staticValue, source: source ?? this.source, fieldKey: fieldKey ?? this.fieldKey, barcodeType: barcodeType ?? this.barcodeType, showText: showText ?? this.showText, borderWidthMm: borderWidthMm ?? this.borderWidthMm, paddingMm: paddingMm ?? this.paddingMm, ); } @override Map toJson() => { 'id': id, 'type': 'barcode', 'xMm': xMm, 'yMm': yMm, 'widthMm': widthMm, 'heightMm': heightMm, 'rotation': rotation, 'isLocked': isLocked, 'zIndex': zIndex, 'valueType': valueType, 'staticValue': staticValue, 'source': source, 'fieldKey': fieldKey, 'placeholder': placeholder, 'barcodeType': barcodeType.name, 'showText': showText, 'borderWidthMm': borderWidthMm, 'paddingMm': paddingMm, }; factory BarcodeElement.fromJson(Map json) { return BarcodeElement( id: json['id'] as String, xMm: (json['xMm'] as num?)?.toDouble() ?? 0.0, yMm: (json['yMm'] as num?)?.toDouble() ?? 0.0, widthMm: (json['widthMm'] as num?)?.toDouble() ?? 40.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 10.0, rotation: (json['rotation'] as num?)?.toDouble() ?? 0.0, isLocked: json['isLocked'] as bool? ?? false, zIndex: json['zIndex'] as int? ?? 0, valueType: json['valueType'] as String? ?? 'dynamic', staticValue: json['staticValue'] as String? ?? '123456789012', source: json['source'] as String? ?? 'product', fieldKey: json['fieldKey'] as String? ?? json['field'] as String? ?? 'barcode', barcodeType: BarcodeType.values.where((b) => b.name.toLowerCase() == (json['barcodeType'] as String? ?? '').toLowerCase()).firstOrNull ?? BarcodeType.code128, showText: json['showText'] as bool? ?? true, borderWidthMm: (json['borderWidthMm'] as num?)?.toDouble() ?? 0.0, paddingMm: (json['paddingMm'] as num?)?.toDouble() ?? 0.5, ); } } class QrCodeElement extends LabelElement { final String valueType; // "static" or "dynamic" final String staticValue; final String source; // "product" final String fieldKey; const QrCodeElement({ required super.id, required super.xMm, required super.yMm, required super.widthMm, required super.heightMm, super.rotation = 0.0, super.isLocked = false, super.zIndex = 0, this.valueType = 'dynamic', this.staticValue = 'https://kifi.app', this.source = 'product', this.fieldKey = 'barcode', }) : super(type: ElementType.qrCode); String get placeholder => valueType == 'dynamic' ? '{{$source.$fieldKey}}' : staticValue; @override QrCodeElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, String? valueType, String? staticValue, String? source, String? fieldKey, }) { return QrCodeElement( id: id, xMm: xMm ?? this.xMm, yMm: yMm ?? this.yMm, widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, rotation: rotation ?? this.rotation, isLocked: isLocked ?? this.isLocked, zIndex: zIndex ?? this.zIndex, valueType: valueType ?? this.valueType, staticValue: staticValue ?? this.staticValue, source: source ?? this.source, fieldKey: fieldKey ?? this.fieldKey, ); } @override Map toJson() => { 'id': id, 'type': 'qrCode', 'xMm': xMm, 'yMm': yMm, 'widthMm': widthMm, 'heightMm': heightMm, 'rotation': rotation, 'isLocked': isLocked, 'zIndex': zIndex, 'valueType': valueType, 'staticValue': staticValue, 'source': source, 'fieldKey': fieldKey, 'placeholder': placeholder, }; factory QrCodeElement.fromJson(Map json) { return QrCodeElement( id: json['id'] as String, xMm: (json['xMm'] as num?)?.toDouble() ?? 0.0, yMm: (json['yMm'] as num?)?.toDouble() ?? 0.0, widthMm: (json['widthMm'] as num?)?.toDouble() ?? 12.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 12.0, rotation: (json['rotation'] as num?)?.toDouble() ?? 0.0, isLocked: json['isLocked'] as bool? ?? false, zIndex: json['zIndex'] as int? ?? 0, valueType: json['valueType'] as String? ?? 'dynamic', staticValue: json['staticValue'] as String? ?? 'https://kifi.app', source: json['source'] as String? ?? 'product', fieldKey: json['fieldKey'] as String? ?? json['field'] as String? ?? 'barcode', ); } } class RectangleElement extends LabelElement { @override final double borderWidthMm; final bool isFilled; final double cornerRadiusMm; const RectangleElement({ required super.id, required super.xMm, required super.yMm, required super.widthMm, required super.heightMm, super.rotation = 0.0, super.isLocked = false, super.zIndex = 0, this.borderWidthMm = 0.3, this.isFilled = false, this.cornerRadiusMm = 0.0, }) : super(type: ElementType.rectangle); @override RectangleElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, double? borderWidthMm, bool? isFilled, double? cornerRadiusMm, }) { return RectangleElement( id: id, xMm: xMm ?? this.xMm, yMm: yMm ?? this.yMm, widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, rotation: rotation ?? this.rotation, isLocked: isLocked ?? this.isLocked, zIndex: zIndex ?? this.zIndex, borderWidthMm: borderWidthMm ?? this.borderWidthMm, isFilled: isFilled ?? this.isFilled, cornerRadiusMm: cornerRadiusMm ?? this.cornerRadiusMm, ); } @override Map toJson() => { 'id': id, 'type': 'rectangle', 'xMm': xMm, 'yMm': yMm, 'widthMm': widthMm, 'heightMm': heightMm, 'rotation': rotation, 'isLocked': isLocked, 'zIndex': zIndex, 'borderWidthMm': borderWidthMm, 'isFilled': isFilled, 'cornerRadiusMm': cornerRadiusMm, }; factory RectangleElement.fromJson(Map json) { return RectangleElement( id: json['id'] as String, xMm: (json['xMm'] as num?)?.toDouble() ?? 0.0, yMm: (json['yMm'] as num?)?.toDouble() ?? 0.0, widthMm: (json['widthMm'] as num?)?.toDouble() ?? 20.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 10.0, rotation: (json['rotation'] as num?)?.toDouble() ?? 0.0, isLocked: json['isLocked'] as bool? ?? false, zIndex: json['zIndex'] as int? ?? 0, borderWidthMm: (json['borderWidthMm'] as num?)?.toDouble() ?? 0.3, isFilled: json['isFilled'] as bool? ?? false, cornerRadiusMm: (json['cornerRadiusMm'] as num?)?.toDouble() ?? 0.0, ); } } class LineElement extends LabelElement { final double thicknessMm; final bool isVertical; const LineElement({ required super.id, required super.xMm, required super.yMm, required super.widthMm, required super.heightMm, super.rotation = 0.0, super.isLocked = false, super.zIndex = 0, this.thicknessMm = 0.3, this.isVertical = false, }) : super(type: ElementType.line); @override LineElement copyWithPosition({ double? xMm, double? yMm, double? widthMm, double? heightMm, double? rotation, bool? isLocked, int? zIndex, double? thicknessMm, bool? isVertical, }) { return LineElement( id: id, xMm: xMm ?? this.xMm, yMm: yMm ?? this.yMm, widthMm: widthMm ?? this.widthMm, heightMm: heightMm ?? this.heightMm, rotation: rotation ?? this.rotation, isLocked: isLocked ?? this.isLocked, zIndex: zIndex ?? this.zIndex, thicknessMm: thicknessMm ?? this.thicknessMm, isVertical: isVertical ?? this.isVertical, ); } @override Map toJson() => { 'id': id, 'type': 'line', 'xMm': xMm, 'yMm': yMm, 'widthMm': widthMm, 'heightMm': heightMm, 'rotation': rotation, 'isLocked': isLocked, 'zIndex': zIndex, 'thicknessMm': thicknessMm, 'isVertical': isVertical, }; factory LineElement.fromJson(Map json) { return LineElement( id: json['id'] as String, xMm: (json['xMm'] as num?)?.toDouble() ?? 0.0, yMm: (json['yMm'] as num?)?.toDouble() ?? 0.0, widthMm: (json['widthMm'] as num?)?.toDouble() ?? 20.0, heightMm: (json['heightMm'] as num?)?.toDouble() ?? 1.0, rotation: (json['rotation'] as num?)?.toDouble() ?? 0.0, isLocked: json['isLocked'] as bool? ?? false, zIndex: json['zIndex'] as int? ?? 0, thicknessMm: (json['thicknessMm'] as num?)?.toDouble() ?? 0.3, isVertical: json['isVertical'] as bool? ?? false, ); } } class LabelDocument { final String id; final String name; final String? description; final int version; final LabelConfiguration config; final List elements; const LabelDocument({ required this.id, required this.name, this.description, this.version = 1, this.config = const LabelConfiguration(), this.elements = const [], }); LabelDocument copyWith({ String? id, String? name, String? description, int? version, LabelConfiguration? config, List? elements, }) { return LabelDocument( id: id ?? this.id, name: name ?? this.name, description: description ?? this.description, version: version ?? this.version, config: config ?? this.config, elements: elements ?? this.elements, ); } Map toJson() => { 'version': version, 'name': name, 'description': description, 'label': config.toJson(), 'elements': elements.map((e) => e.toJson()).toList(), }; String toJsonString() => jsonEncode(toJson()); factory LabelDocument.fromJson(Map json, {String? id, String? name, String? description}) { final labelConfig = json['label'] != null ? LabelConfiguration.fromJson(json['label'] as Map) : const LabelConfiguration(); final rawElements = json['elements'] as List? ?? []; final elementsList = rawElements .map((e) => LabelElement.fromJson(e as Map)) .toList(); return LabelDocument( id: id ?? (json['id'] as String? ?? 'template_1'), name: name ?? (json['name'] as String? ?? 'Untitled Template'), description: description ?? (json['description'] as String?), version: json['version'] as int? ?? 1, config: labelConfig, elements: elementsList, ); } static LabelDocument createDefault() { return const LabelDocument( id: 'default_template', name: '50x25 Product Barcode', description: 'Standard jewellery and product barcode label (50x25mm)', config: LabelConfiguration( widthMm: 50.0, heightMm: 25.0, dpi: 203, ), elements: [ TextElement( id: 'elem_brand', xMm: 2.0, yMm: 1.8, widthMm: 46.0, heightMm: 3.5, text: 'kifi', fontSizePt: 8.5, isBold: true, alignment: TextAlign.center, ), DynamicTextElement( id: 'elem_product_name', xMm: 2.0, yMm: 5.5, widthMm: 46.0, heightMm: 4.0, source: 'product', fieldKey: 'name', fontSizePt: 8.0, alignment: TextAlign.center, ), BarcodeElement( id: 'elem_barcode', xMm: 5.0, yMm: 10.0, widthMm: 40.0, heightMm: 8.0, valueType: 'dynamic', source: 'product', fieldKey: 'barcode', showText: true, ), DynamicTextElement( id: 'elem_price', xMm: 2.0, yMm: 19.5, widthMm: 23.0, heightMm: 3.8, source: 'product', fieldKey: 'sellingPrice', prefix: '₹ ', fontSizePt: 8.0, isBold: true, alignment: TextAlign.left, ), DynamicTextElement( id: 'elem_weight', xMm: 26.0, yMm: 19.5, widthMm: 22.0, heightMm: 3.8, source: 'product', fieldKey: 'weightInG', prefix: 'Wt: ', suffix: ' g', fontSizePt: 7.5, alignment: TextAlign.right, ), ], ); } }