diff --git a/Components/Pages/Duplicates.razor b/Components/Pages/Duplicates.razor index 7eff966..52cce6e 100644 --- a/Components/Pages/Duplicates.razor +++ b/Components/Pages/Duplicates.razor @@ -111,5 +111,5 @@ private void Cancel() => _cts?.Cancel(); private async Task ExportCsv() { await WebExport.DownloadCsvAsync(CsvExport.BuildCsv(_results), $"duplicates_{DateTime.Now:yyyyMMdd_HHmmss}.csv"); } - private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results), $"duplicates_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } + private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results, Session.CurrentBranding), $"duplicates_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } } diff --git a/Components/Pages/Permissions.razor b/Components/Pages/Permissions.razor index 168f8f0..df92e53 100644 --- a/Components/Pages/Permissions.razor +++ b/Components/Pages/Permissions.razor @@ -133,7 +133,7 @@ private async Task ExportHtml() { - var html = HtmlExport.BuildHtml(_results); + var html = HtmlExport.BuildHtml(_results, Session.CurrentBranding); await WebExport.DownloadHtmlAsync(html, $"permissions_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } } diff --git a/Components/Pages/Profiles.razor b/Components/Pages/Profiles.razor index 2b5070f..3cc4351 100644 --- a/Components/Pages/Profiles.razor +++ b/Components/Pages/Profiles.razor @@ -146,6 +146,12 @@ } +
+ + Shown top-right on exported reports for this client. + +
+
@@ -214,7 +220,7 @@ private void EditProfile(TenantProfile p) { _editing = p; - _form = new TenantProfile { Id = p.Id, Name = p.Name, TenantUrl = p.TenantUrl, TenantId = p.TenantId, ClientId = p.ClientId }; + _form = new TenantProfile { Id = p.Id, Name = p.Name, TenantUrl = p.TenantUrl, TenantId = p.TenantId, ClientId = p.ClientId, ClientLogo = p.ClientLogo }; _showForm = true; _formError = _pageError = string.Empty; } diff --git a/Components/Pages/Search.razor b/Components/Pages/Search.razor index e856345..cb8e025 100644 --- a/Components/Pages/Search.razor +++ b/Components/Pages/Search.razor @@ -124,5 +124,5 @@ private void Cancel() => _cts?.Cancel(); private async Task ExportCsv() { await WebExport.DownloadCsvAsync(CsvExport.BuildCsv(_results), $"search_{DateTime.Now:yyyyMMdd_HHmmss}.csv"); } - private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results), $"search_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } + private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results, Session.CurrentBranding), $"search_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } } diff --git a/Components/Pages/Settings.razor b/Components/Pages/Settings.razor index 26a48b5..ecf4ce6 100644 --- a/Components/Pages/Settings.razor +++ b/Components/Pages/Settings.razor @@ -35,11 +35,21 @@
+
+
Report Branding
+
+ +

Shown top-left on exported HTML reports. The client's logo (top-right) is set per profile.

+ +
+
+ @if (_saved) {
Settings saved.
} @code { private string _lang = "en", _theme = "System"; private bool _autoTakeOwnership, _saved; + private LogoData? _mspLogo; protected override void OnInitialized() { @@ -47,11 +57,18 @@ _lang = s.Lang; _theme = s.Theme is "System" or "Light" ? s.Theme : "System"; _autoTakeOwnership = s.AutoTakeOwnership; + _mspLogo = s.MspLogo; + } + + private async Task OnMspLogoChanged(LogoData? logo) + { + _mspLogo = logo; + await Save(); } private async Task Save() { - Session.UpdateSettings(new AppSettings { Lang = _lang, Theme = _theme, AutoTakeOwnership = _autoTakeOwnership }); + Session.UpdateSettings(new AppSettings { Lang = _lang, Theme = _theme, AutoTakeOwnership = _autoTakeOwnership, MspLogo = _mspLogo }); SharepointToolbox.Web.Localization.TranslationSource.Instance.SetCulture(_lang); await JS.InvokeVoidAsync("sptb.setTheme", _theme); _saved = true; diff --git a/Components/Pages/Storage.razor b/Components/Pages/Storage.razor index 61674b1..316f362 100644 --- a/Components/Pages/Storage.razor +++ b/Components/Pages/Storage.razor @@ -21,6 +21,13 @@ +
+
+ + + 0 = libraries only. 1+ = drill into subfolders that many levels deep. +
+
@@ -81,6 +88,7 @@ @code { private string _siteUrl = string.Empty; private bool _includeSubsites, _includeHidden = true, _includeRecycleBin = true; + private int _folderDepth; private bool _running; private string _status = string.Empty, _error = string.Empty; private int _current, _total; private List _results = new(); @@ -94,7 +102,7 @@ var progress = new Progress(p => { _status = p.Message; _current = p.Current; _total = p.Total; InvokeAsync(StateHasChanged); }); try { - var opts = new StorageScanOptions(IncludeSubsites: _includeSubsites, IncludeHiddenLibraries: _includeHidden, IncludeRecycleBin: _includeRecycleBin); + var opts = new StorageScanOptions(IncludeSubsites: _includeSubsites, FolderDepth: Math.Clamp(_folderDepth, 0, 20), IncludeHiddenLibraries: _includeHidden, IncludeRecycleBin: _includeRecycleBin); _results = (await Elevation.RunAsync(async c => { var ctx = await SessionMgr.GetOrCreateContextAsync(siteUrl, Session.CurrentProfile!, c); @@ -116,7 +124,7 @@ } private async Task ExportHtml() { - var html = HtmlExport.BuildHtml(_results); + var html = HtmlExport.BuildHtml(_results, Session.CurrentBranding); await WebExport.DownloadHtmlAsync(html, $"storage_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } } diff --git a/Components/Pages/UserAccessAudit.razor b/Components/Pages/UserAccessAudit.razor index da55a3e..f68f4cb 100644 --- a/Components/Pages/UserAccessAudit.razor +++ b/Components/Pages/UserAccessAudit.razor @@ -103,5 +103,5 @@ private void Cancel() => _cts?.Cancel(); private async Task ExportCsv() { await WebExport.DownloadCsvAsync(CsvExport.BuildCsv(_results.FirstOrDefault()?.UserDisplayName ?? "Users", _results.FirstOrDefault()?.UserLogin ?? "", _results), $"user_audit_{DateTime.Now:yyyyMMdd_HHmmss}.csv"); } - private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results), $"user_audit_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } + private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results, branding: Session.CurrentBranding), $"user_audit_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } } diff --git a/Components/Pages/VersionCleanup.razor b/Components/Pages/VersionCleanup.razor index fe4efab..9cf47e8 100644 --- a/Components/Pages/VersionCleanup.razor +++ b/Components/Pages/VersionCleanup.razor @@ -144,5 +144,5 @@ } private void Cancel() => _cts?.Cancel(); - private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results), $"versions_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } + private async Task ExportHtml() { await WebExport.DownloadHtmlAsync(HtmlExport.BuildHtml(_results, Session.CurrentBranding), $"versions_{DateTime.Now:yyyyMMdd_HHmmss}.html"); } } diff --git a/Components/Shared/LogoUpload.razor b/Components/Shared/LogoUpload.razor new file mode 100644 index 0000000..f0cccda --- /dev/null +++ b/Components/Shared/LogoUpload.razor @@ -0,0 +1,64 @@ +@using Microsoft.AspNetCore.Components.Forms +@using SharepointToolbox.Web.Core.Models + +@* Reusable logo picker. Reads an image into a base64 LogoData (no disk/blob storage). *@ +
+ @if (Value is not null) + { +
+ + +
+ } + else + { + + PNG, JPEG, SVG or GIF — max @(MaxBytes / 1024) KB. + } + @if (!string.IsNullOrEmpty(_error)) {
@_error
} +
+ +@code { + [Parameter] public LogoData? Value { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } + + /// Upload size cap. Logos are stored inline as base64 in JSON, so keep small. + [Parameter] public long MaxBytes { get; set; } = 512 * 1024; + + private string _error = string.Empty; + + private async Task OnChange(InputFileChangeEventArgs e) + { + _error = string.Empty; + var file = e.File; + if (file is null) return; + + if (file.Size > MaxBytes) + { + _error = $"File too large ({file.Size / 1024} KB). Max {MaxBytes / 1024} KB."; + return; + } + + try + { + using var ms = new MemoryStream(); + await file.OpenReadStream(MaxBytes).CopyToAsync(ms); + var mime = string.IsNullOrWhiteSpace(file.ContentType) ? "image/png" : file.ContentType; + var logo = new LogoData { Base64 = Convert.ToBase64String(ms.ToArray()), MimeType = mime }; + Value = logo; + await ValueChanged.InvokeAsync(logo); + } + catch (Exception ex) + { + _error = $"Could not read image: {ex.Message}"; + } + } + + private async Task Remove() + { + Value = null; + _error = string.Empty; + await ValueChanged.InvokeAsync(null); + } +} diff --git a/Core/Models/AppSettings.cs b/Core/Models/AppSettings.cs index a6d141e..1401701 100644 --- a/Core/Models/AppSettings.cs +++ b/Core/Models/AppSettings.cs @@ -6,4 +6,7 @@ public class AppSettings public string Lang { get; set; } = "en"; public bool AutoTakeOwnership { get; set; } = false; public string Theme { get; set; } = "System"; + + /// MSP logo shown top-left on exported reports. Null = none. + public LogoData? MspLogo { get; set; } } diff --git a/Services/Session/IUserSessionService.cs b/Services/Session/IUserSessionService.cs index 31b2dcb..ee1c4ca 100644 --- a/Services/Session/IUserSessionService.cs +++ b/Services/Session/IUserSessionService.cs @@ -12,6 +12,9 @@ public interface IUserSessionService bool HasProfile { get; } AppSettings Settings { get; } + /// Branding for exported reports: MSP logo (settings) + active profile's client logo. + ReportBranding CurrentBranding { get; } + void SetProfile(TenantProfile profile); Task ClearSessionAsync(); void UpdateSettings(AppSettings settings); diff --git a/Services/Session/UserSessionService.cs b/Services/Session/UserSessionService.cs index 8587a74..d7ec810 100644 --- a/Services/Session/UserSessionService.cs +++ b/Services/Session/UserSessionService.cs @@ -15,6 +15,8 @@ public class UserSessionService : IUserSessionService public bool HasProfile => _currentProfile is not null; public AppSettings Settings => _settings; + public ReportBranding CurrentBranding => new(_settings.MspLogo, _currentProfile?.ClientLogo); + public event Action? ProfileChanged; public UserSessionService(ISessionManager sessionManager, SettingsRepository settingsRepo)