Skip to content

Commit e8a0b0f

Browse files
committed
Add Support Tickets page with clickable dashboard navigation
Features: - New Support Tickets page (/support-tickets route) - Filtering by status (All, Open, InProgress, Resolved, Closed) - Filtering by priority (All, Critical, High, Medium, Low) - Stats cards showing ticket counts by status - Table view with ticket details (ID, customer, category, subject, priority, status, date) - Category icons and priority/status badges - Loading, error, and empty states Dashboard Integration: - Enhanced MetricCard component to support optional click handlers - Made 'Open Tickets' metric clickable, navigates to Support Tickets page - Added hover effects and arrow indicator for clickable metrics - Integrated CSS for professional styling Technical Details: - Uses SupportTicketListItemDto from FabrikamContracts - Fetches data via ApiClient.GetSupportTicketsAsync() - Responsive design with mobile-friendly filters - Consistent styling with dashboard theme Aligned with beginner challenge requirements for customer service scenarios.
1 parent 22babc8 commit e8a0b0f

File tree

6 files changed

+638
-2
lines changed

6 files changed

+638
-2
lines changed

FabrikamDashboard/Components/App.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
99
<link rel="stylesheet" href="@Assets["app.css"]" />
1010
<link rel="stylesheet" href="@Assets["dashboard.css"]" />
11+
<link rel="stylesheet" href="@Assets["css/support-tickets.css"]" />
1112
<link rel="stylesheet" href="@Assets["FabrikamDashboard.styles.css"]" />
1213
<ImportMap />
1314
<link rel="icon" type="image/png" href="favicon.png" />

FabrikamDashboard/Components/Pages/Home.razor

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060
<MetricCard Title="Open Tickets"
6161
Value="@_dashboardData.OpenTickets.ToString()"
6262
Icon="🎫"
63-
Color="orange" />
63+
Color="orange"
64+
OnClick="NavigateToSupportTickets" />
6465
<MetricCard Title="Total Revenue"
6566
Value="@($"${_dashboardData.TotalRevenue:N0}")"
6667
Icon="💰"
@@ -304,6 +305,11 @@
304305
};
305306
}
306307

308+
private void NavigateToSupportTickets()
309+
{
310+
Navigation.NavigateTo("/support-tickets");
311+
}
312+
307313
public async ValueTask DisposeAsync()
308314
{
309315
// Unsubscribe from state service
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
@page "/support-tickets"
2+
@using FabrikamContracts.DTOs.Support
3+
@using FabrikamDashboard.Services
4+
@inject FabrikamApiClient ApiClient
5+
@inject ILogger<SupportTickets> Logger
6+
7+
<PageTitle>Support Tickets - Fabrikam Dashboard</PageTitle>
8+
9+
<div class="tickets-container">
10+
<div class="tickets-header">
11+
<h1 class="tickets-title">🎫 Support Tickets</h1>
12+
<div class="tickets-filters">
13+
<select class="filter-select" @onchange="FilterByStatus">
14+
<option value="">All Statuses</option>
15+
<option value="Open">Open</option>
16+
<option value="InProgress">In Progress</option>
17+
<option value="Resolved">Resolved</option>
18+
<option value="Closed">Closed</option>
19+
</select>
20+
<select class="filter-select" @onchange="FilterByPriority">
21+
<option value="">All Priorities</option>
22+
<option value="Critical">Critical</option>
23+
<option value="High">High</option>
24+
<option value="Medium">Medium</option>
25+
<option value="Low">Low</option>
26+
</select>
27+
<button class="refresh-button" @onclick="LoadTickets">🔄 Refresh</button>
28+
</div>
29+
</div>
30+
31+
@if (_isLoading)
32+
{
33+
<div class="loading-container">
34+
<div class="loading-spinner">⏳</div>
35+
<p>Loading support tickets...</p>
36+
</div>
37+
}
38+
else if (_hasError)
39+
{
40+
<div class="error-container">
41+
<div class="error-icon">⚠️</div>
42+
<h3>Unable to Load Tickets</h3>
43+
<p>@_errorMessage</p>
44+
<button class="retry-button" @onclick="LoadTickets">Try Again</button>
45+
</div>
46+
}
47+
else if (_filteredTickets.Count == 0)
48+
{
49+
<div class="empty-state">
50+
<div class="empty-icon">✅</div>
51+
<h3>No Tickets Found</h3>
52+
<p>@(_selectedStatus != "" || _selectedPriority != "" ? "Try adjusting your filters" : "All caught up! No support tickets at the moment.")</p>
53+
</div>
54+
}
55+
else
56+
{
57+
<div class="tickets-stats">
58+
<div class="stat-card stat-open">
59+
<div class="stat-value">@_tickets.Count(t => t.Status == "Open")</div>
60+
<div class="stat-label">Open</div>
61+
</div>
62+
<div class="stat-card stat-inprogress">
63+
<div class="stat-value">@_tickets.Count(t => t.Status == "InProgress")</div>
64+
<div class="stat-label">In Progress</div>
65+
</div>
66+
<div class="stat-card stat-resolved">
67+
<div class="stat-value">@_tickets.Count(t => t.Status == "Resolved")</div>
68+
<div class="stat-label">Resolved</div>
69+
</div>
70+
<div class="stat-card stat-closed">
71+
<div class="stat-value">@_tickets.Count(t => t.Status == "Closed")</div>
72+
<div class="stat-label">Closed</div>
73+
</div>
74+
</div>
75+
76+
<div class="tickets-table">
77+
<table>
78+
<thead>
79+
<tr>
80+
<th>Ticket #</th>
81+
<th>Customer</th>
82+
<th>Category</th>
83+
<th>Subject</th>
84+
<th>Priority</th>
85+
<th>Status</th>
86+
<th>Created</th>
87+
</tr>
88+
</thead>
89+
<tbody>
90+
@foreach (var ticket in _filteredTickets.OrderByDescending(t => t.CreatedDate))
91+
{
92+
<tr class="ticket-row @GetPriorityClass(ticket.Priority)">
93+
<td class="ticket-id">
94+
<span class="ticket-number">TKT-@ticket.Id.ToString("D4")</span>
95+
</td>
96+
<td>
97+
<div class="customer-info">
98+
<div class="customer-name">@ticket.Customer.Name</div>
99+
@if (!string.IsNullOrEmpty(ticket.Customer.Region))
100+
{
101+
<div class="order-reference">Region: @ticket.Customer.Region</div>
102+
}
103+
</div>
104+
</td>
105+
<td>
106+
<span class="category-badge category-@ticket.Category.ToLower()">
107+
@GetCategoryIcon(ticket.Category) @ticket.Category
108+
</span>
109+
</td>
110+
<td class="ticket-subject">@ticket.Subject</td>
111+
<td>
112+
<span class="priority-badge priority-@ticket.Priority.ToLower()">
113+
@ticket.Priority
114+
</span>
115+
</td>
116+
<td>
117+
<span class="status-badge status-@ticket.Status.ToLower()">
118+
@ticket.Status
119+
</span>
120+
</td>
121+
<td class="ticket-date">@ticket.CreatedDate.ToString("MMM d, yyyy")</td>
122+
</tr>
123+
}
124+
</tbody>
125+
</table>
126+
</div>
127+
128+
<div class="tickets-summary">
129+
Showing @_filteredTickets.Count of @_tickets.Count tickets
130+
</div>
131+
}
132+
</div>
133+
134+
@code {
135+
private List<SupportTicketListItemDto> _tickets = new();
136+
private List<SupportTicketListItemDto> _filteredTickets = new();
137+
private bool _isLoading = true;
138+
private bool _hasError = false;
139+
private string _errorMessage = "";
140+
private string _selectedStatus = "";
141+
private string _selectedPriority = "";
142+
143+
protected override async Task OnInitializedAsync()
144+
{
145+
await LoadTickets();
146+
}
147+
148+
private async Task LoadTickets()
149+
{
150+
_isLoading = true;
151+
_hasError = false;
152+
153+
try
154+
{
155+
_tickets = await ApiClient.GetSupportTicketsAsync();
156+
ApplyFilters();
157+
Logger.LogInformation("Loaded {Count} support tickets", _tickets.Count);
158+
}
159+
catch (Exception ex)
160+
{
161+
_hasError = true;
162+
_errorMessage = "Failed to load support tickets. Please try again.";
163+
Logger.LogError(ex, "Error loading support tickets");
164+
}
165+
finally
166+
{
167+
_isLoading = false;
168+
}
169+
}
170+
171+
private void FilterByStatus(ChangeEventArgs e)
172+
{
173+
_selectedStatus = e.Value?.ToString() ?? "";
174+
ApplyFilters();
175+
}
176+
177+
private void FilterByPriority(ChangeEventArgs e)
178+
{
179+
_selectedPriority = e.Value?.ToString() ?? "";
180+
ApplyFilters();
181+
}
182+
183+
private void ApplyFilters()
184+
{
185+
_filteredTickets = _tickets
186+
.Where(t => string.IsNullOrEmpty(_selectedStatus) || t.Status == _selectedStatus)
187+
.Where(t => string.IsNullOrEmpty(_selectedPriority) || t.Priority == _selectedPriority)
188+
.ToList();
189+
}
190+
191+
private string GetPriorityClass(string priority)
192+
{
193+
return priority.ToLower() switch
194+
{
195+
"critical" => "priority-critical-row",
196+
"high" => "priority-high-row",
197+
_ => ""
198+
};
199+
}
200+
201+
private string GetCategoryIcon(string category)
202+
{
203+
return category switch
204+
{
205+
"OrderInquiry" => "📦",
206+
"DeliveryIssue" => "🚚",
207+
"ProductDefect" => "⚠️",
208+
"Installation" => "🔧",
209+
"Billing" => "💰",
210+
"Technical" => "🔌",
211+
"Complaint" => "😠",
212+
_ => "📋"
213+
};
214+
}
215+
}

FabrikamDashboard/Components/Shared/MetricCard.razor

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@* Reusable component for displaying a metric card with icon, value, and title *@
22

3-
<div class="metric-card metric-@Color">
3+
<div class="metric-card metric-@Color @(OnClick.HasDelegate ? "metric-clickable" : "")"
4+
@onclick="HandleClick">
45
<div class="metric-icon">@Icon</div>
56
<div class="metric-content">
67
<div class="metric-value">@Value</div>
@@ -32,4 +33,19 @@
3233
/// </summary>
3334
[Parameter]
3435
public string Color { get; set; } = "blue";
36+
37+
/// <summary>
38+
/// Optional click handler to make the metric card interactive
39+
/// </summary>
40+
[Parameter]
41+
public EventCallback OnClick { get; set; }
42+
43+
private Task HandleClick()
44+
{
45+
if (OnClick.HasDelegate)
46+
{
47+
return OnClick.InvokeAsync();
48+
}
49+
return Task.CompletedTask;
50+
}
3551
}

0 commit comments

Comments
 (0)