Initial commit
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
@model PowderCoating.Application.DTOs.BugReport.EditBugReportDto
|
||||
@using PowderCoating.Core.Enums
|
||||
@using PowderCoating.Application.DTOs.BugReport
|
||||
@{
|
||||
ViewData["Title"] = "Edit Bug Report";
|
||||
ViewData["PageIcon"] = "bi-bug";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<p class="text-muted mb-0">
|
||||
Submitted by <strong>@Model.SubmittedByUserName</strong> on @Model.CreatedAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt")
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CompanyName))
|
||||
{
|
||||
<span class="ms-2"><i class="bi bi-building"></i> <strong>@Model.CompanyName</strong></span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Bug Reports
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-pencil-square"></i> Report Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit" asp-route-id="@Model.Id" method="post">
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<input type="hidden" asp-for="SubmittedByUserName" />
|
||||
<input type="hidden" asp-for="CompanyName" />
|
||||
<input type="hidden" asp-for="CreatedAt" />
|
||||
<input type="hidden" asp-for="CompanyId" />
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Title" class="form-label fw-semibold">
|
||||
Title <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input asp-for="Title" class="form-control" maxlength="200" />
|
||||
<span asp-validation-for="Title" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label fw-semibold">
|
||||
Description <span class="text-danger">*</span>
|
||||
</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="6" maxlength="4000"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Priority" class="form-label fw-semibold">Priority</label>
|
||||
<select asp-for="Priority" class="form-select">
|
||||
@foreach (var p in Enum.GetValues<BugReportPriority>())
|
||||
{
|
||||
<option value="@((int)p)">@p</option>
|
||||
}
|
||||
</select>
|
||||
<span asp-validation-for="Priority" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Status" class="form-label fw-semibold">Status</label>
|
||||
<select asp-for="Status" class="form-select">
|
||||
<option value="@((int)BugReportStatus.New)">New</option>
|
||||
<option value="@((int)BugReportStatus.InProgress)">In Progress</option>
|
||||
<option value="@((int)BugReportStatus.Completed)">Completed</option>
|
||||
<option value="@((int)BugReportStatus.Cancelled)">Cancelled</option>
|
||||
</select>
|
||||
<span asp-validation-for="Status" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="ResolutionNotes" class="form-label fw-semibold">Resolution Notes</label>
|
||||
<textarea asp-for="ResolutionNotes" class="form-control" rows="4" maxlength="4000"
|
||||
placeholder="Describe the resolution or any notes about this report..."></textarea>
|
||||
<span asp-validation-for="ResolutionNotes" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Save Changes
|
||||
</button>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Attachments.Count > 0)
|
||||
{
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-paperclip"></i> Attachments (@Model.Attachments.Count)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
@foreach (var att in Model.Attachments)
|
||||
{
|
||||
var attUrl = Url.Action("Attachment", "BugReport", new { id = att.Id });
|
||||
var isImage = att.ContentType.StartsWith("image/");
|
||||
var isVideo = att.ContentType.StartsWith("video/");
|
||||
var sizeMb = (att.FileSizeBytes / 1024.0 / 1024.0).ToString("F1");
|
||||
|
||||
if (isImage)
|
||||
{
|
||||
<div class="attachment-thumb" title="@att.FileName (@sizeMb MB)"
|
||||
data-type="image" data-src="@attUrl" data-name="@att.FileName"
|
||||
style="cursor:pointer;">
|
||||
<img src="@attUrl" alt="@att.FileName"
|
||||
style="width:120px;height:90px;object-fit:cover;border-radius:6px;border:2px solid #dee2e6;" />
|
||||
<div class="small text-muted text-truncate mt-1" style="max-width:120px;">@att.FileName</div>
|
||||
</div>
|
||||
}
|
||||
else if (isVideo)
|
||||
{
|
||||
<div class="attachment-thumb" title="@att.FileName (@sizeMb MB)"
|
||||
data-type="video" data-src="@attUrl" data-name="@att.FileName"
|
||||
data-content-type="@att.ContentType"
|
||||
style="cursor:pointer;">
|
||||
<div style="width:120px;height:90px;border-radius:6px;border:2px solid #dee2e6;background:#1a1a2e;display:flex;align-items:center;justify-content:center;">
|
||||
<i class="bi bi-play-circle-fill text-white" style="font-size:2.5rem;"></i>
|
||||
</div>
|
||||
<div class="small text-muted text-truncate mt-1" style="max-width:120px;">@att.FileName</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex align-items-center gap-2 border rounded p-2" style="min-width:200px;">
|
||||
<i class="bi bi-file-earmark fs-4 text-secondary"></i>
|
||||
<div>
|
||||
<div class="small fw-semibold text-truncate" style="max-width:150px;">@att.FileName</div>
|
||||
<div class="small text-muted">@sizeMb MB</div>
|
||||
</div>
|
||||
<a href="@attUrl" class="btn btn-sm btn-outline-secondary ms-auto" target="_blank">
|
||||
<i class="bi bi-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox modal -->
|
||||
<div class="modal fade" id="attachmentModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header border-secondary py-2">
|
||||
<h6 class="modal-title text-white mb-0" id="attachmentModalLabel"></h6>
|
||||
<div class="d-flex gap-2 ms-3">
|
||||
<a id="attachmentDownloadLink" href="#" class="btn btn-sm btn-outline-light" target="_blank">
|
||||
<i class="bi bi-download"></i> Download
|
||||
</a>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body p-2 text-center" id="attachmentModalBody">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
@if (Model.Attachments.Any(a => a.ContentType.StartsWith("image/") || a.ContentType.StartsWith("video/")))
|
||||
{
|
||||
<script>
|
||||
document.querySelectorAll('.attachment-thumb').forEach(el => {
|
||||
el.addEventListener('click', function () {
|
||||
const src = this.dataset.src;
|
||||
const name = this.dataset.name;
|
||||
const type = this.dataset.type;
|
||||
const contentType = this.dataset.contentType || '';
|
||||
|
||||
document.getElementById('attachmentModalLabel').textContent = name;
|
||||
document.getElementById('attachmentDownloadLink').href = src;
|
||||
|
||||
const body = document.getElementById('attachmentModalBody');
|
||||
if (type === 'image') {
|
||||
body.innerHTML = `<img src="${src}" alt="${name}" style="max-width:100%;max-height:80vh;border-radius:4px;" />`;
|
||||
} else {
|
||||
body.innerHTML = `<video controls autoplay style="max-width:100%;max-height:80vh;border-radius:4px;">
|
||||
<source src="${src}" type="${contentType}">
|
||||
Your browser does not support video playback.
|
||||
</video>`;
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('attachmentModal')).show();
|
||||
});
|
||||
});
|
||||
|
||||
// Stop video playback when modal closes
|
||||
document.getElementById('attachmentModal')?.addEventListener('hidden.bs.modal', function () {
|
||||
document.getElementById('attachmentModalBody').innerHTML = '';
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
@model List<PowderCoating.Application.DTOs.BugReport.BugReportDto>
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
ViewData["Title"] = "Bug Reports";
|
||||
ViewData["PageIcon"] = "bi-bug";
|
||||
var sortCol = ViewBag.SortColumn as string ?? "CreatedAt";
|
||||
var sortDir = ViewBag.SortDirection as string ?? "desc";
|
||||
int totalCount = ViewBag.TotalCount ?? 0;
|
||||
int pageNumber = ViewBag.PageNumber ?? 1;
|
||||
int pageSize = ViewBag.PageSize ?? 25;
|
||||
int totalPages = ViewBag.TotalPages ?? 1;
|
||||
|
||||
string NextDir(string col) => sortCol == col && sortDir == "asc" ? "desc" : "asc";
|
||||
string SortIcon(string col) => sortCol == col
|
||||
? (sortDir == "asc" ? "bi-sort-up" : "bi-sort-down")
|
||||
: "bi-arrow-down-up text-muted";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
[data-bs-theme="dark"] .table-light th,
|
||||
[data-bs-theme="dark"] .table-light td {
|
||||
background-color: var(--bs-secondary-bg);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
[data-bs-theme="dark"] .card {
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .sort-link {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .border-top {
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
[data-bs-theme="dark"] .pagination .page-link {
|
||||
background-color: var(--bs-body-bg);
|
||||
border-color: var(--bs-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
[data-bs-theme="dark"] .pagination .page-item.active .page-link {
|
||||
background-color: var(--bs-primary);
|
||||
border-color: var(--bs-primary);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="mb-4"></div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> @TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small mb-1">Search</label>
|
||||
<input type="text" name="searchTerm" value="@ViewBag.SearchTerm" class="form-control form-control-sm"
|
||||
placeholder="Title, description, or submitter..." />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small mb-1">Status</label>
|
||||
<select name="statusFilter" class="form-select form-select-sm">
|
||||
<option value="">All Statuses</option>
|
||||
@foreach (var s in Enum.GetValues<BugReportStatus>())
|
||||
{
|
||||
<option value="@s" selected="@(ViewBag.StatusFilter == s.ToString())">@s.ToString()</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small mb-1">Priority</label>
|
||||
<select name="priorityFilter" class="form-select form-select-sm">
|
||||
<option value="">All Priorities</option>
|
||||
@foreach (var p in Enum.GetValues<BugReportPriority>())
|
||||
{
|
||||
<option value="@p" selected="@(ViewBag.PriorityFilter == p.ToString())">@p.ToString()</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small mb-1">Per page</label>
|
||||
<select name="pageSize" class="form-select form-select-sm">
|
||||
@foreach (var n in new[] { 10, 25, 50, 100 })
|
||||
{
|
||||
<option value="@n" selected="@(pageSize == n)">@n</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="sortColumn" value="@sortCol" />
|
||||
<input type="hidden" name="sortDirection" value="@sortDir" />
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100">
|
||||
<i class="bi bi-search"></i> Filter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-check-circle display-4 d-block mb-2"></i>
|
||||
<p>No bug reports found.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>
|
||||
<a asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-sortColumn="Title"
|
||||
asp-route-sortDirection="@NextDir("Title")"
|
||||
class="text-decoration-none sort-link">
|
||||
Title <i class="bi @SortIcon("Title")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-sortColumn="Priority"
|
||||
asp-route-sortDirection="@NextDir("Priority")"
|
||||
class="text-decoration-none sort-link">
|
||||
Priority <i class="bi @SortIcon("Priority")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-sortColumn="Status"
|
||||
asp-route-sortDirection="@NextDir("Status")"
|
||||
class="text-decoration-none sort-link">
|
||||
Status <i class="bi @SortIcon("Status")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-sortColumn="Submitted"
|
||||
asp-route-sortDirection="@NextDir("Submitted")"
|
||||
class="text-decoration-none sort-link">
|
||||
Submitted By <i class="bi @SortIcon("Submitted")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Company</th>
|
||||
<th>
|
||||
<a asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-sortColumn="CreatedAt"
|
||||
asp-route-sortDirection="@NextDir("CreatedAt")"
|
||||
class="text-decoration-none sort-link">
|
||||
Submitted <i class="bi @SortIcon("CreatedAt")"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var report in Model)
|
||||
{
|
||||
<tr style="cursor:pointer" onclick="window.location='@Url.Action("Edit", new { id = report.Id })'">
|
||||
<td>
|
||||
<div class="fw-semibold">@report.Title</div>
|
||||
<div class="text-muted small text-truncate" style="max-width:320px;" title="@report.Description">
|
||||
@report.Description
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@{
|
||||
var priClass = report.Priority switch
|
||||
{
|
||||
BugReportPriority.Critical => "bg-danger",
|
||||
BugReportPriority.High => "bg-warning text-dark",
|
||||
BugReportPriority.Normal => "bg-primary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
}
|
||||
<span class="badge @priClass">@report.Priority</span>
|
||||
</td>
|
||||
<td>
|
||||
@{
|
||||
var statusClass = report.Status switch
|
||||
{
|
||||
BugReportStatus.New => "bg-info text-dark",
|
||||
BugReportStatus.InProgress => "bg-warning text-dark",
|
||||
BugReportStatus.Completed => "bg-success",
|
||||
BugReportStatus.Cancelled => "bg-secondary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
var statusLabel = report.Status switch
|
||||
{
|
||||
BugReportStatus.InProgress => "In Progress",
|
||||
_ => report.Status.ToString()
|
||||
};
|
||||
}
|
||||
<span class="badge @statusClass">@statusLabel</span>
|
||||
</td>
|
||||
<td class="small">@report.SubmittedByUserName</td>
|
||||
<td class="small text-muted">@report.CompanyId</td>
|
||||
<td class="small text-muted text-nowrap">@report.CreatedAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt")</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<a asp-action="Edit" asp-route-id="@report.Id" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile card view — shown on screens < 992px -->
|
||||
<div class="mobile-card-view">
|
||||
<div class="mobile-card-list">
|
||||
@foreach (var report in Model)
|
||||
{
|
||||
var mPriClass = report.Priority switch
|
||||
{
|
||||
BugReportPriority.Critical => "bg-danger",
|
||||
BugReportPriority.High => "bg-warning text-dark",
|
||||
BugReportPriority.Normal => "bg-primary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
var mStatusClass = report.Status switch
|
||||
{
|
||||
BugReportStatus.New => "bg-info text-dark",
|
||||
BugReportStatus.InProgress => "bg-warning text-dark",
|
||||
BugReportStatus.Completed => "bg-success",
|
||||
BugReportStatus.Cancelled => "bg-secondary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
var mStatusLabel = report.Status switch
|
||||
{
|
||||
BugReportStatus.InProgress => "In Progress",
|
||||
_ => report.Status.ToString()
|
||||
};
|
||||
<a href="@Url.Action("Edit", new { id = report.Id })" class="mobile-data-card text-decoration-none">
|
||||
<div class="mobile-card-header">
|
||||
<div class="mobile-card-icon bg-danger"><i class="bi bi-bug"></i></div>
|
||||
<div class="mobile-card-title">
|
||||
<h6>@report.Title</h6>
|
||||
<small>@report.CreatedAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt")</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-card-body">
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Status</span>
|
||||
<span class="mobile-card-value"><span class="badge @mStatusClass">@mStatusLabel</span></span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Priority</span>
|
||||
<span class="mobile-card-value"><span class="badge @mPriClass">@report.Priority</span></span>
|
||||
</div>
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Reporter</span>
|
||||
<span class="mobile-card-value">@report.SubmittedByUserName</span>
|
||||
</div>
|
||||
@if (report.CompanyId > 0)
|
||||
{
|
||||
<div class="mobile-card-row">
|
||||
<span class="mobile-card-label">Company ID</span>
|
||||
<span class="mobile-card-value">@report.CompanyId</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="mobile-card-footer">
|
||||
<span class="btn btn-sm btn-outline-primary">Edit →</span>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalPages > 1)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top">
|
||||
<small class="text-muted">
|
||||
Showing @((pageNumber - 1) * pageSize + 1)–@(Math.Min(pageNumber * pageSize, totalCount)) of @totalCount
|
||||
</small>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item @(pageNumber <= 1 ? "disabled" : "")">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-sortColumn="@sortCol"
|
||||
asp-route-sortDirection="@sortDir"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-pageNumber="@(pageNumber - 1)">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
@for (int i = Math.Max(1, pageNumber - 2); i <= Math.Min(totalPages, pageNumber + 2); i++)
|
||||
{
|
||||
<li class="page-item @(i == pageNumber ? "active" : "")">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-sortColumn="@sortCol"
|
||||
asp-route-sortDirection="@sortDir"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-pageNumber="@i">@i</a>
|
||||
</li>
|
||||
}
|
||||
<li class="page-item @(pageNumber >= totalPages ? "disabled" : "")">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-searchTerm="@ViewBag.SearchTerm"
|
||||
asp-route-statusFilter="@ViewBag.StatusFilter"
|
||||
asp-route-priorityFilter="@ViewBag.PriorityFilter"
|
||||
asp-route-sortColumn="@sortCol"
|
||||
asp-route-sortDirection="@sortDir"
|
||||
asp-route-pageSize="@pageSize"
|
||||
asp-route-pageNumber="@(pageNumber + 1)">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
@model PowderCoating.Application.DTOs.BugReport.CreateBugReportDto
|
||||
@using PowderCoating.Core.Enums
|
||||
@{
|
||||
ViewData["Title"] = "Report a Bug";
|
||||
ViewData["PageIcon"] = "bi-bug";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-end align-items-center mb-4">
|
||||
<a asp-controller="Tools" asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Tools
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle"></i> @TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-pencil-square"></i> Bug Report Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Submit" method="post" enctype="multipart/form-data">
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Title" class="form-label fw-semibold">
|
||||
Title <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input asp-for="Title" class="form-control" placeholder="Brief summary of the issue" maxlength="200" />
|
||||
<span asp-validation-for="Title" class="text-danger small"></span>
|
||||
<div class="form-text">Provide a short, descriptive title (e.g., "Invoice PDF fails to generate").</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label fw-semibold">
|
||||
Description <span class="text-danger">*</span>
|
||||
</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="7"
|
||||
placeholder="Describe what happened, what you expected to happen, and any steps to reproduce the issue..." maxlength="4000"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger small"></span>
|
||||
<div class="form-text">Include steps to reproduce, what you expected, and what actually happened.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="Priority" class="form-label fw-semibold">Priority</label>
|
||||
<select asp-for="Priority" class="form-select">
|
||||
<option value="@((int)BugReportPriority.Low)">Low – Minor inconvenience, workaround exists</option>
|
||||
<option value="@((int)BugReportPriority.Normal)" selected>Normal – Affects workflow but not critical</option>
|
||||
<option value="@((int)BugReportPriority.High)">High – Significantly impacts operations</option>
|
||||
<option value="@((int)BugReportPriority.Critical)">Critical – System unusable or data loss risk</option>
|
||||
</select>
|
||||
<span asp-validation-for="Priority" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="attachments" class="form-label fw-semibold">
|
||||
<i class="bi bi-paperclip"></i> Attachments <span class="text-muted fw-normal">(optional)</span>
|
||||
</label>
|
||||
<input type="file" id="attachments" name="attachments" class="form-control"
|
||||
multiple accept=".jpg,.jpeg,.png,.gif,.webp,.mp4,.mov,.avi,.mkv,.webm"
|
||||
onchange="updateFileList(this)" />
|
||||
<div class="form-text">Photos or videos up to 100 MB each. Accepted: JPG, PNG, GIF, WEBP, MP4, MOV, AVI, MKV, WEBM.</div>
|
||||
<ul id="fileList" class="list-unstyled mt-2 small text-muted"></ul>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-send"></i> Submit Report
|
||||
</button>
|
||||
<a asp-controller="Tools" asp-action="Index" class="btn btn-outline-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
<script>
|
||||
function updateFileList(input) {
|
||||
const list = document.getElementById('fileList');
|
||||
list.innerHTML = '';
|
||||
const maxBytes = 100 * 1024 * 1024;
|
||||
Array.from(input.files).forEach(f => {
|
||||
const li = document.createElement('li');
|
||||
const sizeMb = (f.size / 1024 / 1024).toFixed(1);
|
||||
if (f.size > maxBytes) {
|
||||
li.innerHTML = `<i class="bi bi-exclamation-triangle text-danger"></i> ${f.name} (${sizeMb} MB) — exceeds 100 MB limit`;
|
||||
} else {
|
||||
li.innerHTML = `<i class="bi bi-file-earmark text-secondary"></i> ${f.name} (${sizeMb} MB)`;
|
||||
}
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user