Adds comprehensive customer detail view components
Implements full customer detail page with multiple feature-rich components including overview, economy, statistics, journal, appointments, giftcards, and activity sections Creates reusable ViewComponents for different customer detail aspects with robust data modeling and presentation logic
This commit is contained in:
parent
38e9243bcd
commit
1b25978d9b
26 changed files with 3792 additions and 956 deletions
|
|
@ -0,0 +1,106 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlanTempus.Application.Features.Localization.Services;
|
||||
|
||||
namespace PlanTempus.Application.Features.Customers.Components;
|
||||
|
||||
public class CustomerDetailJournalViewComponent : ViewComponent
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
|
||||
public CustomerDetailJournalViewComponent(ILocalizationService localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
public IViewComponentResult Invoke(string customerId)
|
||||
{
|
||||
var customer = CustomerDetailCatalog.Get(customerId);
|
||||
|
||||
// Group entries by type
|
||||
var notes = customer.Journal.Where(j => j.Type == "note").ToList();
|
||||
var colorFormulas = customer.Journal.Where(j => j.Type == "colorFormula").ToList();
|
||||
var analyses = customer.Journal.Where(j => j.Type == "analysis").ToList();
|
||||
|
||||
var model = new CustomerDetailJournalViewModel
|
||||
{
|
||||
AllCount = customer.Journal.Count,
|
||||
NotesCount = notes.Count,
|
||||
ColorFormulasCount = colorFormulas.Count,
|
||||
AnalysesCount = analyses.Count,
|
||||
Notes = notes.Select(MapEntry).ToList(),
|
||||
ColorFormulas = colorFormulas.Select(MapEntry).ToList(),
|
||||
Analyses = analyses.Select(MapEntry).ToList(),
|
||||
NotesTitle = "Noter",
|
||||
ColorFormulasTitle = "Farveformler",
|
||||
AnalysesTitle = "Analyser",
|
||||
AddNoteText = "+ Tilføj note",
|
||||
AddColorFormulaText = "+ Tilføj",
|
||||
AddAnalysisText = "+ Tilføj"
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private JournalEntryViewModel MapEntry(CustomerJournalEntry entry)
|
||||
{
|
||||
// Format the date (e.g., "2025-12-09" -> "9. dec 2025")
|
||||
var formattedDate = entry.Date;
|
||||
if (DateTime.TryParse(entry.Date, out var date))
|
||||
{
|
||||
formattedDate = date.ToString("d. MMM yyyy", new System.Globalization.CultureInfo("da-DK"));
|
||||
}
|
||||
|
||||
return new JournalEntryViewModel
|
||||
{
|
||||
Id = entry.Id,
|
||||
Type = entry.Type,
|
||||
Tag = entry.Tag,
|
||||
Subtags = entry.Subtags,
|
||||
Text = entry.Text,
|
||||
FormattedDate = $"{formattedDate} - Af: {entry.Author}",
|
||||
Author = entry.Author,
|
||||
TypeClass = GetTypeClass(entry.Tag)
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTypeClass(string tag)
|
||||
{
|
||||
return tag.ToLowerInvariant() switch
|
||||
{
|
||||
"note" => "note",
|
||||
"advarsel" => "advarsel",
|
||||
"farveformel" => "farveformel",
|
||||
"haranalyse" or "analyse" => "analyse",
|
||||
_ => "note"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomerDetailJournalViewModel
|
||||
{
|
||||
public int AllCount { get; init; }
|
||||
public int NotesCount { get; init; }
|
||||
public int ColorFormulasCount { get; init; }
|
||||
public int AnalysesCount { get; init; }
|
||||
public List<JournalEntryViewModel> Notes { get; init; } = new();
|
||||
public List<JournalEntryViewModel> ColorFormulas { get; init; } = new();
|
||||
public List<JournalEntryViewModel> Analyses { get; init; } = new();
|
||||
public required string NotesTitle { get; init; }
|
||||
public required string ColorFormulasTitle { get; init; }
|
||||
public required string AnalysesTitle { get; init; }
|
||||
public required string AddNoteText { get; init; }
|
||||
public required string AddColorFormulaText { get; init; }
|
||||
public required string AddAnalysisText { get; init; }
|
||||
}
|
||||
|
||||
public class JournalEntryViewModel
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Tag { get; init; }
|
||||
public List<string> Subtags { get; init; } = new();
|
||||
public required string Text { get; init; }
|
||||
public required string FormattedDate { get; init; }
|
||||
public required string Author { get; init; }
|
||||
public required string TypeClass { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
@model PlanTempus.Application.Features.Customers.Components.CustomerDetailJournalViewModel
|
||||
|
||||
<!-- Mini Tabs for quick filter -->
|
||||
<swp-journal-mini-tabs>
|
||||
<swp-journal-mini-tab class="active" data-filter="all">
|
||||
<span class="tab-dot blue"></span>
|
||||
Alle
|
||||
<span class="tab-count">@Model.AllCount</span>
|
||||
</swp-journal-mini-tab>
|
||||
<swp-journal-mini-tab data-filter="note">
|
||||
<span class="tab-dot blue"></span>
|
||||
Noter
|
||||
<span class="tab-count">@Model.NotesCount</span>
|
||||
</swp-journal-mini-tab>
|
||||
<swp-journal-mini-tab data-filter="colorFormula">
|
||||
<span class="tab-dot amber"></span>
|
||||
Farveformler
|
||||
<span class="tab-count">@Model.ColorFormulasCount</span>
|
||||
</swp-journal-mini-tab>
|
||||
<swp-journal-mini-tab data-filter="analysis">
|
||||
<span class="tab-dot purple"></span>
|
||||
Analyser
|
||||
<span class="tab-count">@Model.AnalysesCount</span>
|
||||
</swp-journal-mini-tab>
|
||||
</swp-journal-mini-tabs>
|
||||
|
||||
<swp-detail-grid>
|
||||
<!-- Left Column -->
|
||||
<swp-card-column>
|
||||
<!-- Noter header card -->
|
||||
<swp-card data-journal-type="note">
|
||||
<swp-card-header>
|
||||
<swp-card-title>
|
||||
<span class="col-dot blue"></span>
|
||||
<span>@Model.NotesTitle</span>
|
||||
</swp-card-title>
|
||||
<swp-section-action>@Model.AddNoteText</swp-section-action>
|
||||
</swp-card-header>
|
||||
@foreach (var entry in Model.Notes)
|
||||
{
|
||||
<swp-journal-entry data-entry-id="@entry.Id">
|
||||
<swp-journal-entry-header>
|
||||
<swp-journal-entry-type class="@entry.TypeClass">@entry.Tag</swp-journal-entry-type>
|
||||
@if (entry.Subtags.Any())
|
||||
{
|
||||
<swp-journal-entry-tags>
|
||||
@foreach (var subtag in entry.Subtags)
|
||||
{
|
||||
<swp-journal-tag class="@subtag.ToLowerInvariant()">@subtag</swp-journal-tag>
|
||||
}
|
||||
</swp-journal-entry-tags>
|
||||
}
|
||||
<swp-journal-entry-delete><i class="ph ph-trash"></i></swp-journal-entry-delete>
|
||||
</swp-journal-entry-header>
|
||||
<swp-journal-entry-body>@Html.Raw(entry.Text.Replace("\n", "<br>"))</swp-journal-entry-body>
|
||||
<swp-journal-entry-footer>
|
||||
<swp-journal-entry-date>@entry.FormattedDate</swp-journal-entry-date>
|
||||
@if (entry.Tag == "Advarsel")
|
||||
{
|
||||
<swp-journal-entry-visibility class="warning">
|
||||
<i class="ph ph-warning"></i>
|
||||
<span>Advarsel</span>
|
||||
</swp-journal-entry-visibility>
|
||||
}
|
||||
else
|
||||
{
|
||||
<swp-journal-entry-visibility>
|
||||
<i class="ph ph-eye"></i>
|
||||
<span>Alle</span>
|
||||
</swp-journal-entry-visibility>
|
||||
}
|
||||
</swp-journal-entry-footer>
|
||||
</swp-journal-entry>
|
||||
}
|
||||
</swp-card>
|
||||
|
||||
<!-- Farveformler card -->
|
||||
<swp-card data-journal-type="colorFormula">
|
||||
<swp-card-header>
|
||||
<swp-card-title>
|
||||
<span class="col-dot amber"></span>
|
||||
<span>@Model.ColorFormulasTitle</span>
|
||||
</swp-card-title>
|
||||
<swp-section-action>@Model.AddColorFormulaText</swp-section-action>
|
||||
</swp-card-header>
|
||||
@foreach (var entry in Model.ColorFormulas)
|
||||
{
|
||||
<swp-journal-entry data-entry-id="@entry.Id">
|
||||
<swp-journal-entry-header>
|
||||
<swp-journal-entry-type class="@entry.TypeClass">@entry.Tag</swp-journal-entry-type>
|
||||
<swp-journal-entry-delete><i class="ph ph-trash"></i></swp-journal-entry-delete>
|
||||
</swp-journal-entry-header>
|
||||
<swp-journal-entry-body>
|
||||
@{
|
||||
var lines = entry.Text.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Contains(':'))
|
||||
{
|
||||
var parts = line.Split(':', 2);
|
||||
<text><span class="label">@parts[0]:</span> <span class="mono">@parts[1].Trim()</span><br></text>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
@line<br>
|
||||
}
|
||||
else
|
||||
{
|
||||
<br>
|
||||
}
|
||||
}
|
||||
}
|
||||
</swp-journal-entry-body>
|
||||
<swp-journal-entry-footer>
|
||||
<swp-journal-entry-date>@entry.FormattedDate</swp-journal-entry-date>
|
||||
</swp-journal-entry-footer>
|
||||
</swp-journal-entry>
|
||||
}
|
||||
</swp-card>
|
||||
</swp-card-column>
|
||||
|
||||
<!-- Right Column -->
|
||||
<swp-card-column>
|
||||
<!-- Analyser card -->
|
||||
<swp-card data-journal-type="analysis">
|
||||
<swp-card-header>
|
||||
<swp-card-title>
|
||||
<span class="col-dot purple"></span>
|
||||
<span>@Model.AnalysesTitle</span>
|
||||
</swp-card-title>
|
||||
<swp-section-action>@Model.AddAnalysisText</swp-section-action>
|
||||
</swp-card-header>
|
||||
@foreach (var entry in Model.Analyses)
|
||||
{
|
||||
<swp-journal-entry data-entry-id="@entry.Id">
|
||||
<swp-journal-entry-header>
|
||||
<swp-journal-entry-type class="@entry.TypeClass">@entry.Tag</swp-journal-entry-type>
|
||||
<swp-journal-entry-delete><i class="ph ph-trash"></i></swp-journal-entry-delete>
|
||||
</swp-journal-entry-header>
|
||||
<swp-journal-entry-body>
|
||||
@{
|
||||
var analysisLines = entry.Text.Split('\n');
|
||||
foreach (var line in analysisLines)
|
||||
{
|
||||
if (line.Contains(':'))
|
||||
{
|
||||
var parts = line.Split(':', 2);
|
||||
<text><span class="label">@parts[0]:</span> @parts[1].Trim()<br></text>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
@line<br>
|
||||
}
|
||||
else
|
||||
{
|
||||
<br>
|
||||
}
|
||||
}
|
||||
}
|
||||
</swp-journal-entry-body>
|
||||
<swp-journal-entry-footer>
|
||||
<swp-journal-entry-date>@entry.FormattedDate</swp-journal-entry-date>
|
||||
</swp-journal-entry-footer>
|
||||
</swp-journal-entry>
|
||||
}
|
||||
</swp-card>
|
||||
</swp-card-column>
|
||||
</swp-detail-grid>
|
||||
Loading…
Add table
Add a link
Reference in a new issue