final receipt update
This commit is contained in:
@@ -41,78 +41,77 @@ public class TemplateInterpreter
|
||||
List<PrintCommand> commands,
|
||||
StyleContext style)
|
||||
{
|
||||
var lineBuffer = new StringBuilder();
|
||||
var hasInlineContent = false;
|
||||
|
||||
void FlushLineBuffer()
|
||||
{
|
||||
if (hasInlineContent)
|
||||
{
|
||||
var text = ApplyAlignment(lineBuffer.ToString(), style);
|
||||
commands.Add(CreateCommand(text, style));
|
||||
lineBuffer.Clear();
|
||||
hasInlineContent = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
InterpretNode(node, context, commands, style);
|
||||
switch (node)
|
||||
{
|
||||
// Inline nodes - accumulate into line buffer
|
||||
case TextNode textNode:
|
||||
lineBuffer.Append(textNode.Text);
|
||||
hasInlineContent = true;
|
||||
break;
|
||||
|
||||
case BindingNode bindingNode:
|
||||
var element = context.Resolve(bindingNode.Path);
|
||||
lineBuffer.Append(_formatter.Format(element, bindingNode.Format));
|
||||
hasInlineContent = true;
|
||||
break;
|
||||
|
||||
// Styled text - flush buffer, emit styled content, continue accumulating
|
||||
case StyledTextNode styledNode:
|
||||
FlushLineBuffer();
|
||||
InterpretStyledText(styledNode, context, commands, style);
|
||||
break;
|
||||
|
||||
// Block nodes - flush buffer first, then process
|
||||
case SeparatorNode separatorNode:
|
||||
FlushLineBuffer();
|
||||
InterpretSeparator(separatorNode, commands, style);
|
||||
break;
|
||||
|
||||
case EmptyLineNode:
|
||||
FlushLineBuffer();
|
||||
commands.Add(CreateCommand(string.Empty, style));
|
||||
break;
|
||||
|
||||
case IfNode ifNode:
|
||||
FlushLineBuffer();
|
||||
InterpretIf(ifNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case ForeachNode foreachNode:
|
||||
FlushLineBuffer();
|
||||
InterpretForeach(foreachNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case RowNode rowNode:
|
||||
FlushLineBuffer();
|
||||
InterpretRow(rowNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case ColumnNode columnNode:
|
||||
FlushLineBuffer();
|
||||
InterpretColumn(columnNode, context, commands, style);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InterpretNode(
|
||||
ITemplateNode node,
|
||||
DataContext context,
|
||||
List<PrintCommand> commands,
|
||||
StyleContext style)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case TextNode textNode:
|
||||
InterpretText(textNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case BindingNode bindingNode:
|
||||
InterpretBinding(bindingNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case StyledTextNode styledNode:
|
||||
InterpretStyledText(styledNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case SeparatorNode separatorNode:
|
||||
InterpretSeparator(separatorNode, commands, style);
|
||||
break;
|
||||
|
||||
case EmptyLineNode:
|
||||
commands.Add(CreateCommand(string.Empty, style));
|
||||
break;
|
||||
|
||||
case IfNode ifNode:
|
||||
InterpretIf(ifNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case ForeachNode foreachNode:
|
||||
InterpretForeach(foreachNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case RowNode rowNode:
|
||||
InterpretRow(rowNode, context, commands, style);
|
||||
break;
|
||||
|
||||
case ColumnNode columnNode:
|
||||
InterpretColumn(columnNode, context, commands, style);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void InterpretText(
|
||||
TextNode node,
|
||||
DataContext context,
|
||||
List<PrintCommand> commands,
|
||||
StyleContext style)
|
||||
{
|
||||
var text = ApplyAlignment(node.Text, style);
|
||||
commands.Add(CreateCommand(text, style));
|
||||
}
|
||||
|
||||
private void InterpretBinding(
|
||||
BindingNode node,
|
||||
DataContext context,
|
||||
List<PrintCommand> commands,
|
||||
StyleContext style)
|
||||
{
|
||||
var element = context.Resolve(node.Path);
|
||||
var text = _formatter.Format(element, node.Format);
|
||||
text = ApplyAlignment(text, style);
|
||||
commands.Add(CreateCommand(text, style));
|
||||
// Flush any remaining content
|
||||
FlushLineBuffer();
|
||||
}
|
||||
|
||||
private void InterpretStyledText(
|
||||
@@ -217,7 +216,11 @@ public class TemplateInterpreter
|
||||
List<PrintCommand> commands,
|
||||
StyleContext style)
|
||||
{
|
||||
var lineWidth = style.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
|
||||
// Apply row-level styles
|
||||
var rowStyle = style.Clone();
|
||||
ApplyStyles(node.Styles, rowStyle);
|
||||
|
||||
var lineWidth = rowStyle.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
|
||||
var rowBuilder = new StringBuilder();
|
||||
|
||||
foreach (var column in node.Columns)
|
||||
@@ -270,7 +273,7 @@ public class TemplateInterpreter
|
||||
rowBuilder.Append(aligned);
|
||||
}
|
||||
|
||||
commands.Add(CreateCommand(rowBuilder.ToString(), style));
|
||||
commands.Add(CreateCommand(rowBuilder.ToString(), rowStyle));
|
||||
}
|
||||
|
||||
private void InterpretColumn(
|
||||
|
||||
@@ -13,6 +13,7 @@ public class Lexer
|
||||
private readonly List<LexerError> _errors = new();
|
||||
private bool _inMultiLineComment;
|
||||
private SourcePosition _multiLineCommentStart;
|
||||
private bool _isCommentOnlyLine;
|
||||
|
||||
private static readonly Regex StyleStartRegex = new(@"^#([a-zA-Z0-9,:]+)#", RegexOptions.Compiled);
|
||||
private static readonly Regex BindingRegex = new(@"^\{([^}]+)\}", RegexOptions.Compiled);
|
||||
@@ -40,8 +41,14 @@ public class Lexer
|
||||
|
||||
while (_lineIndex < _lines.Count)
|
||||
{
|
||||
_isCommentOnlyLine = false;
|
||||
TokenizeLine(_lines[_lineIndex]);
|
||||
_tokens.Add(new Token(TokenType.NewLine, "\n", new SourcePosition(_lineIndex + 1, _columnIndex + 1)));
|
||||
|
||||
// Skip NewLine for comment-only lines and lines inside multi-line comments
|
||||
if (!_isCommentOnlyLine && !_inMultiLineComment)
|
||||
{
|
||||
_tokens.Add(new Token(TokenType.NewLine, "\n", new SourcePosition(_lineIndex + 1, _columnIndex + 1)));
|
||||
}
|
||||
_lineIndex++;
|
||||
_columnIndex = 0;
|
||||
}
|
||||
@@ -76,12 +83,14 @@ public class Lexer
|
||||
}
|
||||
else
|
||||
{
|
||||
// Comment close line with no content after
|
||||
_isCommentOnlyLine = true;
|
||||
_columnIndex = line.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Still in comment - skip entire line
|
||||
// Still in comment - skip entire line (handled by _inMultiLineComment check in Tokenize)
|
||||
_columnIndex = line.Length;
|
||||
}
|
||||
return;
|
||||
@@ -107,6 +116,8 @@ public class Lexer
|
||||
}
|
||||
else
|
||||
{
|
||||
// Comment-only line (no content after *@)
|
||||
_isCommentOnlyLine = true;
|
||||
_columnIndex = line.Length;
|
||||
}
|
||||
}
|
||||
@@ -114,12 +125,14 @@ public class Lexer
|
||||
{
|
||||
// Start of multi-line comment block
|
||||
_inMultiLineComment = true;
|
||||
_isCommentOnlyLine = true;
|
||||
_multiLineCommentStart = new SourcePosition(lineNumber, leadingWhitespace + 1);
|
||||
_columnIndex = line.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single-line comment to end of line
|
||||
_isCommentOnlyLine = true;
|
||||
_columnIndex = line.Length;
|
||||
}
|
||||
return;
|
||||
@@ -215,7 +228,7 @@ public class Lexer
|
||||
return true;
|
||||
|
||||
case "@row":
|
||||
_tokens.Add(new Token(TokenType.Row, string.Empty, position));
|
||||
_tokens.Add(new Token(TokenType.Row, value, position));
|
||||
_columnIndex = trimmed.Length + leadingWhitespace;
|
||||
return true;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Inspectron.Epson.Templates.Language.Nodes;
|
||||
|
||||
public record RowNode(List<ColumnNode> Columns, SourcePosition Position) : ITemplateNode
|
||||
public record RowNode(List<ColumnNode> Columns, IReadOnlyList<string> Styles, SourcePosition Position) : ITemplateNode
|
||||
{
|
||||
public IReadOnlyList<ITemplateNode> Children => Columns;
|
||||
}
|
||||
|
||||
@@ -254,6 +254,9 @@ public class Parser
|
||||
private RowNode ParseRow()
|
||||
{
|
||||
var rowToken = Current;
|
||||
var styles = string.IsNullOrWhiteSpace(rowToken.Value)
|
||||
? Array.Empty<string>()
|
||||
: rowToken.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
Advance(); // consume @row
|
||||
SkipNewlines();
|
||||
|
||||
@@ -300,7 +303,7 @@ public class Parser
|
||||
AddError("Expected @endrow to close @row block", Current.Position);
|
||||
}
|
||||
|
||||
return new RowNode(columns, rowToken.Position);
|
||||
return new RowNode(columns, styles, rowToken.Position);
|
||||
}
|
||||
|
||||
private ColumnNode ParseColumnDef()
|
||||
|
||||
64
Inspectron.Epson.Templates/Samples/bar-default.json
Normal file
64
Inspectron.Epson.Templates/Samples/bar-default.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"Title": "Bar",
|
||||
"TransactionDateTime": "2025-10-23T20:30:00",
|
||||
"ReceiptNumber": "132018300",
|
||||
"WaiterName": "Thomas Weber",
|
||||
"WaiterId": "28 (Bar Staff)",
|
||||
"TableNumber": "B5",
|
||||
"SpecialInstruction": "VIP Table",
|
||||
"Gangs": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "Round",
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 2,
|
||||
"Name": "Aperol Spritz",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": ["Extra Ice"]
|
||||
},
|
||||
"Comment": null
|
||||
},
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Negroni",
|
||||
"Modifications": {
|
||||
"Removed": ["Orange Peel"],
|
||||
"Added": []
|
||||
},
|
||||
"Comment": "Stirred, not shaken"
|
||||
},
|
||||
{
|
||||
"Number": 3,
|
||||
"Name": "San Pellegrino 50cl",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": []
|
||||
},
|
||||
"Comment": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 2,
|
||||
"Name": "Espresso",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": []
|
||||
},
|
||||
"Comment": null
|
||||
},
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Cappuccino",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": ["Oat Milk"]
|
||||
},
|
||||
"Comment": "Extra hot"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
Inspectron.Epson.Templates/Samples/fallback.json
Normal file
4
Inspectron.Epson.Templates/Samples/fallback.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"Title": "General Receipt",
|
||||
"TransactionDateTime": "2025-12-01T14:00:00"
|
||||
}
|
||||
108
Inspectron.Epson.Templates/Samples/final-receipt.json
Normal file
108
Inspectron.Epson.Templates/Samples/final-receipt.json
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"CompanyName": "Klingler Gastro AG",
|
||||
"Address1": "Munzplatz 3",
|
||||
"Address2": "CH-8001 Zurich",
|
||||
"Phone": "043 321 22 22",
|
||||
"ReceiptNumber": "1-81-202",
|
||||
"DateTime": "2025-12-16T14:58:00",
|
||||
"Guests": 2,
|
||||
"Total": 160.00,
|
||||
"Currency": "CHF",
|
||||
"TotalInAlternateCurrency": 172.80,
|
||||
"AlternateCurrency": "EUR",
|
||||
"PaymentMethod": "MASTER",
|
||||
"PaymentAmount": 160.00,
|
||||
"WaiterName": "Yves",
|
||||
"Terminal": "Hauptkasse ZH",
|
||||
"TableNumber": "12",
|
||||
"VatNumber": "CHE-449.635.880 MWST",
|
||||
"ThankYouMessage": "Das Team bedankt sich herzlich fur Ihren",
|
||||
"GoodbyeMessageLine1": "Besuch.",
|
||||
"GoodbyeMessageLine2": "Auf Wiedersehen.",
|
||||
"Items": [
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "San Pellegrino 50cl",
|
||||
"UnitPrice": 6.50,
|
||||
"TotalPrice": 6.50,
|
||||
"TaxCategory": "A"
|
||||
},
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "Panna 50cl",
|
||||
"UnitPrice": 6.50,
|
||||
"TotalPrice": 6.50,
|
||||
"TaxCategory": "A"
|
||||
},
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "Granini Tomatensaft",
|
||||
"UnitPrice": 5.50,
|
||||
"TotalPrice": 5.50,
|
||||
"TaxCategory": "A"
|
||||
},
|
||||
{
|
||||
"Quantity": 2,
|
||||
"Description": "Business Lunch Menu Seco",
|
||||
"UnitPrice": 44.00,
|
||||
"TotalPrice": 88.00,
|
||||
"TaxCategory": "A"
|
||||
},
|
||||
{
|
||||
"Quantity": 2,
|
||||
"Description": "Brunello di Montalcino 1",
|
||||
"UnitPrice": 16.00,
|
||||
"TotalPrice": 32.00,
|
||||
"TaxCategory": "A"
|
||||
},
|
||||
{
|
||||
"Quantity": 2,
|
||||
"Description": "Espresso",
|
||||
"UnitPrice": 5.50,
|
||||
"TotalPrice": 11.00,
|
||||
"TaxCategory": "A"
|
||||
},
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "Tip",
|
||||
"UnitPrice": 10.50,
|
||||
"TotalPrice": 10.50,
|
||||
"TaxCategory": "B"
|
||||
}
|
||||
],
|
||||
"TaxBreakdown": [
|
||||
{
|
||||
"Category": "A",
|
||||
"Rate": 8.1,
|
||||
"Gross": 149.50,
|
||||
"Net": 138.30,
|
||||
"TaxAmount": 11.20,
|
||||
"Currency": "CHF"
|
||||
},
|
||||
{
|
||||
"Category": "B",
|
||||
"Rate": 0,
|
||||
"Gross": 10.50,
|
||||
"Net": 10.50,
|
||||
"TaxAmount": 0.00,
|
||||
"Currency": "CHF"
|
||||
}
|
||||
],
|
||||
"TerminalReceipt": {
|
||||
"ReceiptType": "*** Kundenbeleg ***",
|
||||
"BookingType": "Buchung",
|
||||
"PaymentSystem": "TWINT",
|
||||
"TransactionNumber": "XXXXXXXXXXXXXXX1494",
|
||||
"TransactionDateTime": "2025-11-11T12:34:49",
|
||||
"TerminalId": "31108834",
|
||||
"AID": "A0000015749E",
|
||||
"TransactionSeqCount": "6127",
|
||||
"TransactionRefNo": "99036644599",
|
||||
"AuthCode": "0d3396",
|
||||
"AcquirerId": "2",
|
||||
"EftAmount": 67.00,
|
||||
"TipAmount": 6.70,
|
||||
"TotalEftAmount": 73.70,
|
||||
"Currency": "CHF"
|
||||
}
|
||||
}
|
||||
60
Inspectron.Epson.Templates/Samples/kitchen-default.json
Normal file
60
Inspectron.Epson.Templates/Samples/kitchen-default.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"Title": "Warme Kuche",
|
||||
"TransactionDateTime": "2025-10-23T12:18:00",
|
||||
"ReceiptNumber": "132018166",
|
||||
"WaiterName": "Mariano Amato",
|
||||
"WaiterId": "32 (VK Restaurant)",
|
||||
"TableNumber": "22",
|
||||
"SpecialInstruction": "Next dish",
|
||||
"Gangs": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "Gang",
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Avocado Sashimi",
|
||||
"Modifications": {
|
||||
"Removed": ["Wasabi"],
|
||||
"Added": ["Extra Ginger"]
|
||||
},
|
||||
"Comment": "No soy sauce"
|
||||
},
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Baby Spinach Salad with Truffl",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": []
|
||||
},
|
||||
"Comment": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"Name": "Gang",
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Beef Tataki",
|
||||
"Modifications": {
|
||||
"Removed": ["Onion"],
|
||||
"Added": ["Extra Sauce"]
|
||||
},
|
||||
"Comment": "Medium rare"
|
||||
},
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Salmon Taco",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": ["Extra Lime"]
|
||||
},
|
||||
"Comment": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Dishes": []
|
||||
}
|
||||
39
Inspectron.Epson.Templates/Samples/kitchen-u220.json
Normal file
39
Inspectron.Epson.Templates/Samples/kitchen-u220.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Title": "Kalte Kuche",
|
||||
"TransactionDateTime": "2025-11-15T18:45:00",
|
||||
"ReceiptNumber": "132018200",
|
||||
"WaiterName": "Sofia Mueller",
|
||||
"WaiterId": "45 (VK Restaurant)",
|
||||
"TableNumber": "8",
|
||||
"SpecialInstruction": null,
|
||||
"Gangs": [],
|
||||
"Dishes": [
|
||||
{
|
||||
"Number": 2,
|
||||
"Name": "Caesar Salad",
|
||||
"Modifications": {
|
||||
"Removed": ["Croutons"],
|
||||
"Added": ["Extra Parmesan"]
|
||||
},
|
||||
"Comment": "Dressing on the side"
|
||||
},
|
||||
{
|
||||
"Number": 1,
|
||||
"Name": "Carpaccio",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": []
|
||||
},
|
||||
"Comment": null
|
||||
},
|
||||
{
|
||||
"Number": 3,
|
||||
"Name": "Bruschetta",
|
||||
"Modifications": {
|
||||
"Removed": [],
|
||||
"Added": ["Extra Basil"]
|
||||
},
|
||||
"Comment": null
|
||||
}
|
||||
]
|
||||
}
|
||||
58
Inspectron.Epson.Templates/Templates/final-receipt.template
Normal file
58
Inspectron.Epson.Templates/Templates/final-receipt.template
Normal file
@@ -0,0 +1,58 @@
|
||||
#center# {CompanyName} #
|
||||
#center# {Address1} #
|
||||
#center# {Address2} #
|
||||
#center# {Phone} #
|
||||
|
||||
|
||||
#bold# {left:Rechnung Nr. {ReceiptNumber}} {right:{DateTime:HH:mm dd.MM.yyyy}} #
|
||||
Guests: {Guests}
|
||||
|
||||
@foreach item in Items
|
||||
{item.Quantity}x {item.Description} {right:{item.UnitPrice:F2} {item.TotalPrice:F2} {item.TaxCategory}}
|
||||
@end
|
||||
|
||||
{right:---------}
|
||||
|
||||
#big,bold,center# Summe: {Total:F2} {Currency} #
|
||||
|
||||
@if TotalInAlternateCurrency
|
||||
{right:{TotalInAlternateCurrency:F2} {AlternateCurrency}}
|
||||
|
||||
@end
|
||||
#bold# {PaymentMethod} {right:{PaymentAmount:F2} {Currency}} #
|
||||
|
||||
@if TaxBreakdown.count > 0
|
||||
MwSt % Brutto Netto MwSt
|
||||
@foreach tax in TaxBreakdown
|
||||
{tax.Category}: {tax.Rate}% {tax.Gross:F2} {tax.Currency} {tax.Net:F2} {tax.Currency} {tax.TaxAmount:F2} {tax.Currency}
|
||||
@end
|
||||
@end
|
||||
|
||||
{right:Bedient von:} {WaiterName}
|
||||
{right:Terminal:} {Terminal}
|
||||
{right:Tisch:} {TableNumber}
|
||||
|
||||
|
||||
#center# {VatNumber} #
|
||||
|
||||
@if TerminalReceipt
|
||||
#center# {TerminalReceipt.ReceiptType} #
|
||||
#center# {TerminalReceipt.BookingType} #
|
||||
#center# {TerminalReceipt.PaymentSystem} #
|
||||
{TerminalReceipt.TransactionNumber}
|
||||
{TerminalReceipt.TransactionDateTime:dd.MM.yyyy} {right:{TerminalReceipt.TransactionDateTime:HH:mm:ss}}
|
||||
Trm-Id: {right:{TerminalReceipt.TerminalId}}
|
||||
AID: {right:{TerminalReceipt.AID}}
|
||||
Trx. Seq-Cnt: {right:{TerminalReceipt.TransactionSeqCount}}
|
||||
Trx. Ref-No: {right:{TerminalReceipt.TransactionRefNo}}
|
||||
Auth. Code: {right:{TerminalReceipt.AuthCode}}
|
||||
Acq-Id: {right:{TerminalReceipt.AcquirerId}}
|
||||
EFT {TerminalReceipt.Currency}: {right:{TerminalReceipt.EftAmount:F2}}
|
||||
Trinkgeld {TerminalReceipt.Currency}: {right:{TerminalReceipt.TipAmount:F2}}
|
||||
Total-EFT {TerminalReceipt.Currency}: {right:{TerminalReceipt.TotalEftAmount:F2}}
|
||||
---
|
||||
@end
|
||||
|
||||
#center# {ThankYouMessage} #
|
||||
#center# {GoodbyeMessageLine1} #
|
||||
#center# {GoodbyeMessageLine2} #
|
||||
Reference in New Issue
Block a user