Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions CulinaryCommandApp/Components/Layout/FeedbackButton.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
@using Amazon.Runtime.Internal.UserAgent
@using Microsoft.AspNetCore.Components
@using CulinaryCommand.Services.UserContextSpace
@using CulinaryCommand.Data.Entities
@inject IUserContextService UserCtx
@inject NavigationManager Nav
@inject IJSRuntime JSRuntime
@inject IFeedbackService FeedbackSvc

<div>
<button class="feedback-btn" @onclick="ShowDialog">Feedback</button>

@if (showDialog)
{
<div class="feedback-dialog">
<div class="feedback-content">
<h3>Submit Feedback</h3>

<div>
<label>Type</label>
<div class="feedback-type-buttons">
<button class="feedback-type-btn @(feedbackType == "Bug" ? "active" : "")"
@onclick='() => feedbackType = "Bug"'>
Bug
</button>
<button class="feedback-type-btn @(feedbackType == "Feature" ? "active" : "")"
@onclick='() => feedbackType = "Feature"'>
Feature Request
</button>
<button class="feedback-type-btn @(feedbackType == "General" ? "active" : "")"
@onclick='() => feedbackType = "General"'>
General
</button>
</div>
</div>

<div>
<label>Message</label>
<textarea @bind="message" placeholder="@GetPlaceholder()" />
</div>

@* <div>
label>Screenshot (optional)</label>
<input type="file" @onchange="OnScreenshotChange" />
</div> *@

<div>
<label>Screenshot (optional)</label>
<InputFile OnChange="OnScreenshotChange" accept="image/*" />
</div>

<div>
<button @onclick="SubmitFeedback" disabled="@(feedbackType == null || string.IsNullOrWhiteSpace(message))">Submit</button>
<button @onclick="CloseDialog">Cancel</button>
</div>
</div>
</div>
}

@if (showConfirmation)
{
<div class="feedback-confirmation">
Feedback submitted!
</div>
}
</div>


@code {
private bool showDialog;
private bool showConfirmation;
private UserContext? _ctx;

private string? userRole;
private string? message;

private string? screenshotBase64;
private string currentPage => Nav?.Uri ?? "";
private string deviceType = "Unknown";
private string timestamp => DateTime.Now.ToString("yyyy-MM-dd HH:mm");

private string? feedbackType;

private string GetPlaceholder() => feedbackType switch
{
"Bug" => "Describe what happened and steps to reproduce...",
"Feature" => "Describe the feature and why it would be helpful...",
"General" => "Share your thoughts...",
_ => "Select a type above first..."
};

protected override async Task OnInitializedAsync()
{
if (UserCtx != null)
{
_ctx = await UserCtx.GetAsync();
}
}

protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender)
{
deviceType = await GetDeviceTypeAsync();
}
}

private void ShowDialog()
{
showDialog = true;
showConfirmation = false;
}

private void CloseDialog()
{
showDialog = false;
feedbackType = null;
}

private async Task SubmitFeedback()
{
Console.WriteLine($"DBG: Screenshot length at submit: {screenshotBase64?.Length}");
// Don't hide dialog yet — keep screenshotBase64 alive
await FeedbackSvc.SubmitFeedbackAsync(new Feedback
{
UserId = _ctx?.User?.Id,
UserEmail = _ctx?.User?.Email,
UserRole = _ctx?.User?.Role,
FeedbackType = feedbackType!,
Page = currentPage,
Device = deviceType,
Message = message!,
ScreenshotBase64 = screenshotBase64
});

// Now safe to hide and show confirmation
showDialog = false;
showConfirmation = true;
StateHasChanged();

await Task.Delay(3500);

feedbackType = null;
showConfirmation = false;
userRole = string.Empty;
message = string.Empty;
screenshotBase64 = null;

StateHasChanged();
}

@* private void OnScreenshotChange(ChangeEventArgs e)
{
var file = e.Value as Microsoft.AspNetCore.Components.Forms.IBrowserFile;
if (file != null)
{
using var stream = file.OpenReadStream();
var buffer = new byte[file.Size];
stream.Read(buffer, 0, (int)file.Size);
screenshotBase64 = Convert.ToBase64String(buffer);
}
} *@

@* private async Task OnScreenshotChange(ChangeEventArgs e)
{
var file = e.Value as Microsoft.AspNetCore.Components.Forms.IBrowserFile;
if (file != null)
{
using var stream = file.OpenReadStream(maxAllowedSize: 5 * 1024 * 1024); // 5MB limit
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
screenshotBase64 = Convert.ToBase64String(ms.ToArray());
}
} *@

private async Task OnScreenshotChange(InputFileChangeEventArgs e)
{
var file = e.File;
Console.WriteLine($"DBG: File received: {file?.Name}, Size: {file?.Size}");

if (file != null && file.Size > 0)
{
try
{
using var stream = file.OpenReadStream(maxAllowedSize: 5 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
screenshotBase64 = Convert.ToBase64String(ms.ToArray());
Console.WriteLine($"DBG: Base64 length: {screenshotBase64?.Length}");
}
catch (Exception ex)
{
Console.WriteLine($"DBG: Screenshot read error: {ex.Message}");
}
}
}

private async Task<string> GetDeviceTypeAsync()
{
string? userAgent = await JSRuntime.InvokeAsync<string>("eval", "navigator.userAgent");
return userAgent;
}
}
3 changes: 3 additions & 0 deletions CulinaryCommandApp/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
@Body
</article>
</main>
<div class="feedback-fixed">
<FeedbackButton />
</div>
</div>
</Authorized>

Expand Down
5 changes: 5 additions & 0 deletions CulinaryCommandApp/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
<span class="bi bi-people nav-icon"></span> Manage Users
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="/feedback">
<span class="bi bi-chat-square-dots nav-icon" aria-hidden="true"></span> Feedback
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="/purchase-orders">
<span class="bi bi-cart3 nav-icon" aria-hidden="true"></span> Purchase Orders
Expand Down
165 changes: 165 additions & 0 deletions CulinaryCommandApp/Components/Pages/FeedbackPage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
@page "/feedback"
@using CulinaryCommand.Data.Entities
@using CulinaryCommand.Services
@inject IFeedbackService FeedbackSvc

<div class="feedback-page">
<h2>Feedback</h2>

<div class="feedback-filters">
<div class="filter-group">
<label>From</label>
<input type="date" @bind="filterFrom" />
</div>
<div class="filter-group">
<label>To</label>
<input type="date" @bind="filterTo" />
</div>
<div class="filter-group">
<label>Type</label>
<select @bind="filterType">
<option value="">All</option>
<option value="Bug">🐛 Bug</option>
<option value="Feature">✨ Feature Request</option>
<option value="General">💬 General</option>
</select>
</div>
<div class="filter-group">
<label>Role</label>
<select @bind="filterRole">
<option value="">All</option>
<option value="Employee">Employee</option>
<option value="Manager">Manager</option>
<option value="Admin">Admin</option>
</select>
</div>
<button class="filter-clear-btn" @onclick="ClearFilters">Clear</button>
</div>

@if (isLoading)
{
<p class="feedback-loading">Loading feedback...</p>
}
else if (!Filtered.Any())
{
<p class="feedback-empty">No feedback matches your filters.</p>
}
else
{
<div class="feedback-summary">
Showing <strong>@Filtered.Count()</strong> of <strong>@allFeedback.Count</strong> submissions
</div>

<div class="feedback-table-wrapper">
<table class="feedback-table">
<thead>
<tr>
<th>Time</th>
<th>Type</th>
<th>Role</th>
<th>User</th>
<th>Page</th>
<th>Message</th>
<th>Screenshot</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var f in Filtered)
{
<tr>
<td class="feedback-td-time">@f.SubmittedAt.ToLocalTime().ToString("MM/dd/yy HH:mm")</td>
<td><span class="feedback-badge @GetTypeBadgeClass(f.FeedbackType)">@GetTypeEmoji(f.FeedbackType) @f.FeedbackType</span></td>
<td>@f.UserRole</td>
<td class="feedback-td-email">@f.UserEmail</td>
<td class="feedback-td-page" title="@f.Page">@TrimPage(f.Page)</td>
<td class="feedback-td-message">@f.Message</td>
<td>
@if (!string.IsNullOrEmpty(f.ScreenshotBase64))
{
<img class="feedback-thumb" src="data:image/png;base64,@f.ScreenshotBase64" @onclick="() => OpenScreenshot(f.ScreenshotBase64)" />
}
else
{
<span class="feedback-no-screenshot">—</span>
}
</td>
<td>
<button class="feedback-delete-btn" @onclick="() => DeleteFeedback(f.Id)">🗑</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>

@if (screenshotModal != null)
{
<div class="screenshot-overlay" @onclick="CloseScreenshot">
<img class="screenshot-modal-img" src="data:image/png;base64,@screenshotModal" />
</div>
}

@code {
private List<Feedback> allFeedback = new();
private bool isLoading = true;

private DateTime? filterFrom;
private DateTime? filterTo;
private string filterType = "";
private string filterRole = "";
private string? screenshotModal;

private IEnumerable<Feedback> Filtered => allFeedback
.Where(f => filterFrom == null || f.SubmittedAt.ToLocalTime().Date >= filterFrom.Value.Date)
.Where(f => filterTo == null || f.SubmittedAt.ToLocalTime().Date <= filterTo.Value.Date)
.Where(f => string.IsNullOrEmpty(filterType) || f.FeedbackType == filterType)
.Where(f => string.IsNullOrEmpty(filterRole) || f.UserRole == filterRole);

protected override async Task OnInitializedAsync()
{
allFeedback = await FeedbackSvc.GetAllFeedbackAsync();
isLoading = false;
}

private async Task DeleteFeedback(int id)
{
await FeedbackSvc.DeleteFeedbackAsync(id);
allFeedback = await FeedbackSvc.GetAllFeedbackAsync();
}

private void ClearFilters()
{
filterFrom = null;
filterTo = null;
filterType = "";
filterRole = "";
}

private void OpenScreenshot(string base64) => screenshotModal = base64;
private void CloseScreenshot() => screenshotModal = null;

private string TrimPage(string page)
{
try { return "/" + new Uri(page).AbsolutePath.TrimStart('/'); }
catch { return page; }
}

private string GetTypeBadgeClass(string type) => type switch
{
"Bug" => "badge-bug",
"Feature" => "badge-feature",
"General" => "badge-general",
_ => ""
};

private string GetTypeEmoji(string type) => type switch
{
"Bug" => "🐛",
"Feature" => "✨",
"General" => "💬",
_ => ""
};
}
Loading
Loading