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;
}
}
}