using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; // DateTime.TryParse("5.5") succeeds (reads as month/day of the current year) — so a // plain numeric string must never be handed to it, or a stray decimal like a // TaskPlannedHours/TLeaveNumberOfDays value stored as a JSON string would silently // turn into a bogus date under ParticularsFormatter's date-format fallback below. namespace GB5Shared.WorkFlow { public static class ParticularsFormatter { // Matches DTO.{AnySegment}.Path.Here optionally followed by :dd-MMM-yyyy // The middle segment (parsedData / DataJson / etc.) is ignored — always resolved from DataJson. // Group 1 = dot-path Group 2 = format specifier (optional, no spaces allowed) private static readonly Regex DtoPathPattern = new(@"DTO\.[A-Za-z][A-Za-z0-9_]*\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)(?::([^\s/|]+))?", RegexOptions.Compiled); // A bare number (int or decimal, optional sign) — excluded from date-parsing so // "5.5" is never misread as 05-May. private static readonly Regex PlainNumberPattern = new(@"^-?\d+(\.\d+)?$", RegexOptions.Compiled); /// /// Resolves a MWORKFLOWCONFIG PARTICULARSTEMPLATE against DataJson. /// /// Token syntax /// ───────────────────────────────────────────────────────────── /// DTO.{Namespace}.FieldName /// Root-level scalar. The middle segment is free-form — use any name /// (DataJson / parsedData / Leave / etc.); it is always resolved from DataJson. /// e.g. DTO.DataJson.TLeaveType → "Loss of Pay" /// /// DTO.{Namespace}.ArrayField.NestedField /// First-array-element navigation — /// e.g. DTO.DataJson.TLeaveDetailArray.EmployeeName → "Sudhakar S" /// /// DTO.{Namespace}.DateField:dd-MMM-yyyy /// Date/time with a format specifier — /// e.g. DTO.DataJson.TLeaveDetailArray.TLeaveDetailLeaveDate:dd-MMM-yyyy → "01-Mar-2026" /// Any standard .NET date format string is accepted. /// /// ##FieldName## (legacy) /// Root-level property, case-insensitive. Kept for backward compatibility. /// /// Literal text between tokens is preserved as-is. /// Unresolved tokens resolve to an empty string (never throws). /// Returns null when template or dataJson is absent. /// /// Example PARTICULARSTEMPLATE for Leave: /// DTO.parsedData.BizTransactionTypeName / DTO.parsedData.EmployeeCode / DTO.parsedData.TLeaveDetailArray.EmployeeName / DTO.parsedData.OUName / DTO.parsedData.TLeaveNumberOfDays Days / DTO.parsedData.TLeaveDetailArray.TLeaveDetailLeaveDate:dd-MMM-yyyy To DTO.parsedData.TLeaveDetailArray.TLeaveDetailValidTill:dd-MMM-yyyy / DTO.parsedData.TLeaveDayType / Available: (DTO.parsedData.TAvailabeLeave)-- DTO.parsedData.LeaveDays /// Produces: /// Casual Leave Request / 21211 / Sudhakar S / Human Resource / 1 Days / 01-Mar-2026 To 01-Mar-2026 / Onduty / Available: (29)-- 0 /// /// — LoginDTO.DateFormat (e.g. "dd/MM/yyyy"). /// Applied to any resolved value that parses as a date/time and whose token has no /// explicit :format specifier — so a bare DTO.DataJson.TaskDateOnly renders per the /// logged-in user's session date format instead of the raw JSON value. A token with /// an explicit :format always keeps taking precedence over this default. Never applied /// to numeric tokens — the field type, not the format string, decides that branch. /// public static string? Apply(string? template, object? dataJson, string? defaultDateFormat = null) { if (string.IsNullOrWhiteSpace(template) || dataJson is null) return null; var jsonString = dataJson is string s ? s : dataJson.ToString(); if (string.IsNullOrWhiteSpace(jsonString)) return null; using var doc = JsonDocument.Parse(jsonString); var root = doc.RootElement; // ── DTO.parsedData.{path}[:format] tokens ──────────────────────── template = DtoPathPattern.Replace(template, match => { var path = match.Groups[1].Value; var format = match.Groups[2].Success ? match.Groups[2].Value : null; return ResolvePath(root, path, format, defaultDateFormat); }); // ── Legacy ##FieldName## tokens ─────────────────────────────────── var legacyRoot = root; if (legacyRoot.ValueKind == JsonValueKind.Array) legacyRoot = legacyRoot.GetArrayLength() > 0 ? legacyRoot[0] : default; if (legacyRoot.ValueKind != JsonValueKind.Object) return template.Trim(); foreach (var prop in legacyRoot.EnumerateObject()) { var token = $"##{prop.Name}##"; if (!template.Contains(token, StringComparison.OrdinalIgnoreCase)) continue; var value = prop.Value.ValueKind switch { JsonValueKind.String => prop.Value.GetString() ?? string.Empty, JsonValueKind.Null => string.Empty, _ => prop.Value.ToString() }; template = template.Replace(token, value, StringComparison.OrdinalIgnoreCase); } return template.Trim(); } private static string ResolvePath(JsonElement root, string path, string? format, string? defaultDateFormat) { var current = root; foreach (var segment in path.Split('.')) { if (current.ValueKind == JsonValueKind.Array) { if (current.GetArrayLength() == 0) return string.Empty; current = current[0]; } if (current.ValueKind != JsonValueKind.Object) return string.Empty; if (!current.TryGetProperty(segment, out var next)) { var found = false; foreach (var prop in current.EnumerateObject()) { if (!string.Equals(prop.Name, segment, StringComparison.OrdinalIgnoreCase)) continue; next = prop.Value; found = true; break; } if (!found) return string.Empty; } current = next; } return ElementToString(current, format, defaultDateFormat); } private static string ElementToString(JsonElement el, string? format = null, string? defaultDateFormat = null) { switch (el.ValueKind) { case JsonValueKind.Null: return string.Empty; case JsonValueKind.String: var raw = el.GetString() ?? string.Empty; // Explicit :format on the token always wins; otherwise fall back to the // session's LoginDTO.DateFormat — but only once the value is confirmed to // actually be a date, so plain text fields are never touched. var effectiveDateFormat = format ?? defaultDateFormat; if (effectiveDateFormat is not null && !PlainNumberPattern.IsMatch(raw) && DateTime.TryParse(raw, null, DateTimeStyles.RoundtripKind, out var dt)) return dt.ToString(effectiveDateFormat, CultureInfo.InvariantCulture); return raw; case JsonValueKind.Number: if (format is not null) { if (el.TryGetDecimal(out var d)) return d.ToString(format, CultureInfo.InvariantCulture); } return el.TryGetInt64(out var l) ? l.ToString() : el.TryGetDecimal(out var dv) ? (dv % 1 == 0 ? ((long)dv).ToString() : dv.ToString()) : el.GetRawText(); case JsonValueKind.True: return "true"; case JsonValueKind.False: return "false"; case JsonValueKind.Array: return el.GetArrayLength() > 0 ? ElementToString(el[0], format, defaultDateFormat) : string.Empty; default: return el.GetRawText(); } } } }