using ClosedXML.Excel; using GB5Shared.Export; namespace GB5Shared.ExcelExport { /// /// Shared, format-agnostic Excel styling/formatting helpers used by both the flat report exporter /// () and the pivot exporter (), /// so both give a consistent "professional" visual result instead of two divergent hand-rolled styles. /// public static class ExcelStyleHelpers { // Fixed, theme-independent totals colors — deliberately not derived from the partner theme // (arbitrary partner hex values have no safe, verified "lighten" operation in this ClosedXML // version), so Sub Total/Grand Total stay visually distinct and legible for every partner. private static readonly XLColor SubtotalFill = XLColor.FromHtml("#FFF2CC"); private static readonly XLColor GrandTotalFill = XLColor.FromHtml("#D9E1F2"); private static readonly XLColor BandedRowFill = XLColor.FromHtml("#F2F2F2"); /// Parses a hex color string, falling back safely when null/empty/unparseable. public static XLColor SafeColor(string? hex, XLColor fallback) { if (string.IsNullOrWhiteSpace(hex)) return fallback; try { return XLColor.FromHtml(hex); } catch { return fallback; } } /// The bold, colored, bordered style for the actual column-title row (e.g. "Voucher Number", "Debit"). public static void ApplyProfessionalHeaderStyle(IXLStyle style, ReportTheme theme) { style.Font.Bold = true; style.Font.FontColor = XLColor.White; style.Font.FontName = theme.FontFamily; style.Fill.BackgroundColor = SafeColor(theme.PrimaryColorHex, XLColor.FromHtml("#1F4E78")); style.Border.OutsideBorder = XLBorderStyleValues.Thin; style.Border.InsideBorder = XLBorderStyleValues.Thin; style.Alignment.Vertical = XLAlignmentVerticalValues.Center; } /// /// Styles an existing report-header/title row (from MREPORTHEADERDETAIL, via /// ). Respects that row's own /// explicit Font/FontSize/Color when the report designer set one — that's an intentional, /// per-report author choice and should win over the generic partner theme. /// public static void ApplyReportHeaderRowStyle(IXLStyle style, GB5Shared.DTO.Report.ReportViewExcelHeaderDTO h, ReportTheme theme) { style.Font.Bold = true; style.Font.FontName = string.IsNullOrWhiteSpace(h.Font) ? theme.FontFamily : h.Font; style.Font.FontSize = h.FontSize > 0 ? h.FontSize : 14; style.Font.FontColor = SafeColor(h.Color, SafeColor(theme.PrimaryColorHex, XLColor.FromHtml("#1F4E78"))); } /// Styles the "Generated on {timestamp}" sub-row this rewrite adds under the report banner. public static void ApplyGeneratedOnRowStyle(IXLStyle style, ReportTheme theme) { style.Font.Italic = true; style.Font.FontName = theme.FontFamily; style.Font.FontSize = 9; style.Font.FontColor = SafeColor(theme.AccentColorHex, XLColor.Gray); } // Fixed to match the PDF/HTML template's .group-header CSS class exactly (HybridReport/ // Templates/default.txt) so a report looks the same across export formats — not theme-derived, // same rationale as the totals fills above. private static readonly XLColor GroupHeaderFill = XLColor.FromHtml("#DBE4ED"); private static readonly XLColor GroupHeaderFont = XLColor.FromHtml("#1A252F"); /// /// The full-width "LevelTitle: LevelValue" banner row written before a group's rows/subgroups — /// the Excel equivalent of PDF/HTML's .group-header row (ReportExport.cs's RenderNode). Indent /// reflects nesting depth (0 = outermost), matching that row's left-padding-by-level. /// public static void ApplyGroupHeaderStyle(IXLStyle style, int level) { style.Font.Bold = true; style.Font.FontColor = GroupHeaderFont; style.Font.FontSize = 11; style.Fill.BackgroundColor = GroupHeaderFill; style.Alignment.Indent = Math.Max(0, level); } public static void ApplySubtotalStyle(IXLStyle style) { style.Font.Bold = true; style.Fill.BackgroundColor = SubtotalFill; style.Border.TopBorder = XLBorderStyleValues.Thin; } public static void ApplyGrandTotalStyle(IXLStyle style) { style.Font.Bold = true; style.Fill.BackgroundColor = GrandTotalFill; style.Border.TopBorder = XLBorderStyleValues.Double; } /// Alternating light-gray/white banding for plain data rows — call with a 0-based running data-row index. public static void ApplyBandedRowStyle(IXLStyle style, int zebraIndex) { if (zebraIndex % 2 == 1) style.Fill.BackgroundColor = BandedRowFill; } public static void ApplyFreezeHeader(IXLWorksheet ws, int headerRowIndex) => ws.SheetView.FreezeRows(headerRowIndex); /// /// Translates ReportViewFieldsCalFormat into an Excel-native cell number format. GB5's /// CalFormat strings (e.g. "##,##,##0.00" for Indian lakh/crore grouping) are already valid Excel /// custom number-format syntax — Excel respects literal comma positions the same way, so this is /// a direct pass-through rather than a re-derivation through .NET culture formatting. /// fieldType: 3 = date, 0/6 = numeric (matches ReportExport.cs's own field-type switch). /// public static string ToExcelNumberFormat(string? calFormat, byte fieldType) { // ReportViewFieldsCalFormat is historically authored for PDF/HTML's string-based // FormatWithCalFormat (e.g. "dd-MMM-yyyy HH:mm:ss" for a legacy ledger display) — a date // field's own Excel cell should never carry the time-of-day portion regardless of what // that string says, so treat any CalFormat containing a time component (':' reliably // indicates one — every H/HH/h/hh:m/mm[:s/ss] pattern uses it) as unset for this purpose. if (fieldType == 3) // date return string.IsNullOrWhiteSpace(calFormat) || calFormat.Contains(':') ? "dd/mm/yyyy" : calFormat; if (fieldType == 0 || fieldType == 6) // int / decimal return string.IsNullOrWhiteSpace(calFormat) ? "#,##0.00" : calFormat; return "General"; } /// /// ReportViewFieldsAlignment convention: 1 = Left, 2 = Right (corrected — see Phase 2's Grid /// alignment fix). Falls back to type-based auto-detect (numeric right, else left) when /// unconfigured (0). /// public static XLAlignmentHorizontalValues ToExcelAlignment(byte alignment, byte fieldType) { if (alignment == 1) return XLAlignmentHorizontalValues.Left; if (alignment == 2) return XLAlignmentHorizontalValues.Right; bool isNumeric = fieldType == 0 || fieldType == 6; return isNumeric ? XLAlignmentHorizontalValues.Right : XLAlignmentHorizontalValues.Left; } } }