Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf6acc125f | |||
| f467862877 | |||
| 7ad7d84016 | |||
| 75b0a8afe2 | |||
| 38748c2152 | |||
| 4ec55e7290 | |||
| 3eda91f170 |
@@ -16,6 +16,7 @@ public class GiftCertificateListDto
|
|||||||
public GiftCertificateStatus Status { get; set; }
|
public GiftCertificateStatus Status { get; set; }
|
||||||
public DateTime IssueDate { get; set; }
|
public DateTime IssueDate { get; set; }
|
||||||
public DateTime? ExpiryDate { get; set; }
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
public Guid? BatchId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GiftCertificateDto : GiftCertificateListDto
|
public class GiftCertificateDto : GiftCertificateListDto
|
||||||
@@ -87,3 +88,27 @@ public class RedeemGiftCertificateDto
|
|||||||
[Range(0.01, 9999.99)]
|
[Range(0.01, 9999.99)]
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BulkCreateGiftCertificateDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[Range(1, 500, ErrorMessage = "Quantity must be between 1 and 500.")]
|
||||||
|
[Display(Name = "Number of Certificates")]
|
||||||
|
public int Quantity { get; set; } = 25;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Range(1.00, 9999.99, ErrorMessage = "Amount must be between $1.00 and $9,999.99.")]
|
||||||
|
[Display(Name = "Face Value (each)")]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Display(Name = "Issued Reason")]
|
||||||
|
public GiftCertificateIssuedReason IssuedReason { get; set; } = GiftCertificateIssuedReason.Promotional;
|
||||||
|
|
||||||
|
[Display(Name = "Expiry Date (optional)")]
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
[StringLength(1000)]
|
||||||
|
[Display(Name = "Event / Notes (applied to all certificates)")]
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,4 +51,10 @@ public interface IPdfService
|
|||||||
byte[]? companyLogo,
|
byte[]? companyLogo,
|
||||||
string? companyLogoContentType,
|
string? companyLogoContentType,
|
||||||
CompanyInfoDto companyInfo);
|
CompanyInfoDto companyInfo);
|
||||||
|
|
||||||
|
Task<byte[]> GenerateBulkGiftCertificatePdfAsync(
|
||||||
|
IList<GiftCertificateDto> certs,
|
||||||
|
byte[]? companyLogo,
|
||||||
|
string? companyLogoContentType,
|
||||||
|
CompanyInfoDto companyInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1858,6 +1858,50 @@ public class PdfService : IPdfService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a multi-page PDF containing one gift certificate per page, all using the same
|
||||||
|
/// branded layout as the single-certificate download. Used for bulk print runs (car shows,
|
||||||
|
/// promotions) so staff can hand-cut and distribute a full batch from one print job.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> GenerateBulkGiftCertificatePdfAsync(
|
||||||
|
IList<GiftCertificateDto> certs,
|
||||||
|
byte[]? companyLogo,
|
||||||
|
string? companyLogoContentType,
|
||||||
|
CompanyInfoDto companyInfo)
|
||||||
|
{
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
const string accent = "#7c3aed";
|
||||||
|
const string gold = "#b45309";
|
||||||
|
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var doc = Document.Create(container =>
|
||||||
|
{
|
||||||
|
foreach (var cert in certs)
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Size(PageSizes.Letter);
|
||||||
|
page.Margin(0.75f, Unit.Inch);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(10).FontFamily("Arial"));
|
||||||
|
|
||||||
|
page.Content().Element(c => ComposeGiftCertificateContent(c, cert, companyInfo, companyLogo, accent, gold));
|
||||||
|
|
||||||
|
page.Footer().AlignCenter().Text(text =>
|
||||||
|
{
|
||||||
|
text.Span(companyInfo.CompanyName).FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
if (!string.IsNullOrWhiteSpace(companyInfo.Phone))
|
||||||
|
text.Span($" · {FormatPhoneNumber(companyInfo.Phone)}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return doc.GeneratePdf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Composes the gift certificate body with a decorative double-border frame (outer purple 3pt,
|
/// Composes the gift certificate body with a decorative double-border frame (outer purple 3pt,
|
||||||
/// inner gold 1pt) that gives the document a premium printed-certificate appearance. Inside the
|
/// inner gold 1pt) that gives the document a premium printed-certificate appearance. Inside the
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ public class GiftCertificate : BaseEntity
|
|||||||
/// <summary>Set when this GC was sold via an invoice line item.</summary>
|
/// <summary>Set when this GC was sold via an invoice line item.</summary>
|
||||||
public int? SourceInvoiceItemId { get; set; }
|
public int? SourceInvoiceItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Groups all certificates created in a single bulk run. Null for individually issued certs.</summary>
|
||||||
|
public Guid? BatchId { get; set; }
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
public virtual Customer? RecipientCustomer { get; set; }
|
public virtual Customer? RecipientCustomer { get; set; }
|
||||||
public virtual Customer? PurchasingCustomer { get; set; }
|
public virtual Customer? PurchasingCustomer { get; set; }
|
||||||
|
|||||||
Generated
+10754
File diff suppressed because it is too large
Load Diff
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddGiftCertificateBatchId : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "BatchId",
|
||||||
|
table: "GiftCertificates",
|
||||||
|
type: "uniqueidentifier",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 15, 0, 30, 26, 297, DateTimeKind.Utc).AddTicks(7656));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 15, 0, 30, 26, 297, DateTimeKind.Utc).AddTicks(7662));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 15, 0, 30, 26, 297, DateTimeKind.Utc).AddTicks(7664));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BatchId",
|
||||||
|
table: "GiftCertificates");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7475));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7481));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7482));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3290,6 +3290,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
|
|
||||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<Guid?>("BatchId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<string>("CertificateCode")
|
b.Property<string>("CertificateCode")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("nvarchar(450)");
|
.HasColumnType("nvarchar(450)");
|
||||||
@@ -6708,7 +6711,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7475),
|
CreatedAt = new DateTime(2026, 5, 15, 0, 30, 26, 273, DateTimeKind.Utc).AddTicks(2464),
|
||||||
Description = "Standard pricing for regular customers",
|
Description = "Standard pricing for regular customers",
|
||||||
DiscountPercent = 0m,
|
DiscountPercent = 0m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -6719,7 +6722,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7481),
|
CreatedAt = new DateTime(2026, 5, 15, 0, 30, 26, 273, DateTimeKind.Utc).AddTicks(2473),
|
||||||
Description = "5% discount for preferred customers",
|
Description = "5% discount for preferred customers",
|
||||||
DiscountPercent = 5m,
|
DiscountPercent = 5m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -6730,7 +6733,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 5, 14, 20, 43, 43, 116, DateTimeKind.Utc).AddTicks(7482),
|
CreatedAt = new DateTime(2026, 5, 15, 0, 30, 26, 273, DateTimeKind.Utc).AddTicks(2474),
|
||||||
Description = "10% discount for premium customers",
|
Description = "10% discount for premium customers",
|
||||||
DiscountPercent = 10m,
|
DiscountPercent = 10m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ public class GiftCertificatesController : Controller
|
|||||||
IssuedReason = gc.IssuedReason,
|
IssuedReason = gc.IssuedReason,
|
||||||
Status = gc.Status,
|
Status = gc.Status,
|
||||||
IssueDate = gc.IssueDate,
|
IssueDate = gc.IssueDate,
|
||||||
ExpiryDate = gc.ExpiryDate
|
ExpiryDate = gc.ExpiryDate,
|
||||||
|
BatchId = gc.BatchId
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -440,6 +441,183 @@ public class GiftCertificatesController : Controller
|
|||||||
return acct?.Id;
|
return acct?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the bulk certificate creation form. Defaults to Promotional reason and 25 certificates
|
||||||
|
/// since the primary use case is car shows and events where a batch of same-value certificates
|
||||||
|
/// is distributed to attendees.
|
||||||
|
/// </summary>
|
||||||
|
public IActionResult BulkCreate()
|
||||||
|
{
|
||||||
|
return View(new BulkCreateGiftCertificateDto());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates N gift certificates in a single batch, records GL entries for each, then redirects
|
||||||
|
/// to a confirmation page where the user can download the full batch as a single print-ready PDF.
|
||||||
|
/// Certificate codes are generated sequentially so the batch occupies a contiguous range (e.g.
|
||||||
|
/// GC-2506-0012 through GC-2506-0036), making it easy to audit which codes belong to each event.
|
||||||
|
/// GL treatment mirrors single-certificate issuance: Sold certs debit Checking, all others debit
|
||||||
|
/// Sales Discounts (4950) and credit GC Liability (2500).
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> BulkCreate(BulkCreateGiftCertificateDto dto)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
return View(dto);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = await _userManager.GetUserAsync(User);
|
||||||
|
var companyId = currentUser?.CompanyId ?? 0;
|
||||||
|
|
||||||
|
var gcLiabilityAcctId = await GetGcLiabilityAccountIdAsync(companyId);
|
||||||
|
int? checkingAcctId = null;
|
||||||
|
int? discountAcctId = null;
|
||||||
|
|
||||||
|
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
||||||
|
{
|
||||||
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
|
a => a.IsActive && (a.AccountSubType == AccountSubTypeEnum.Checking
|
||||||
|
|| a.AccountSubType == AccountSubTypeEnum.Cash));
|
||||||
|
checkingAcctId = acct?.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var acct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||||
|
a => a.IsActive && a.AccountNumber == "4950");
|
||||||
|
discountAcctId = acct?.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var batchId = Guid.NewGuid();
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
for (int i = 0; i < dto.Quantity; i++)
|
||||||
|
{
|
||||||
|
var code = await GenerateCertificateCodeAsync(companyId);
|
||||||
|
|
||||||
|
var cert = new GiftCertificate
|
||||||
|
{
|
||||||
|
CertificateCode = code,
|
||||||
|
OriginalAmount = dto.Amount,
|
||||||
|
RedeemedAmount = 0,
|
||||||
|
IssuedReason = dto.IssuedReason,
|
||||||
|
Status = GiftCertificateStatus.Active,
|
||||||
|
IssueDate = now,
|
||||||
|
ExpiryDate = dto.ExpiryDate,
|
||||||
|
Notes = dto.Notes,
|
||||||
|
IssuedById = currentUser?.Id,
|
||||||
|
CompanyId = companyId,
|
||||||
|
CreatedAt = now,
|
||||||
|
CreatedBy = currentUser?.Email,
|
||||||
|
BatchId = batchId
|
||||||
|
};
|
||||||
|
|
||||||
|
await _unitOfWork.GiftCertificates.AddAsync(cert);
|
||||||
|
await _unitOfWork.CompleteAsync();
|
||||||
|
|
||||||
|
await _accountBalanceService.CreditAsync(gcLiabilityAcctId, cert.OriginalAmount);
|
||||||
|
if (dto.IssuedReason == GiftCertificateIssuedReason.Sold)
|
||||||
|
await _accountBalanceService.DebitAsync(checkingAcctId, cert.OriginalAmount);
|
||||||
|
else
|
||||||
|
await _accountBalanceService.DebitAsync(discountAcctId, cert.OriginalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(BulkResult), new { batchId });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error creating bulk gift certificates");
|
||||||
|
this.ToastError("An error occurred creating the certificates.");
|
||||||
|
return View(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Displays the batch confirmation page. Driven by BatchId so it is bookmarkable and survives
|
||||||
|
/// browser back/refresh — the user can return here any time to re-download the batch PDF.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IActionResult> BulkResult(Guid batchId)
|
||||||
|
{
|
||||||
|
if (batchId == Guid.Empty)
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
|
||||||
|
var certs = await _unitOfWork.GiftCertificates.FindAsync(
|
||||||
|
gc => gc.BatchId == batchId, false);
|
||||||
|
|
||||||
|
if (!certs.Any())
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
|
||||||
|
return View(certs.OrderBy(c => c.CertificateCode).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams a multi-page PDF for an entire batch identified by BatchId. GET endpoint so the
|
||||||
|
/// user can bookmark or re-open it at any time after the batch was originally created.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IActionResult> BatchDownloadPdf(Guid batchId)
|
||||||
|
{
|
||||||
|
if (batchId == Guid.Empty)
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
var currentUser = await _userManager.GetUserAsync(User);
|
||||||
|
var companyId = currentUser?.CompanyId ?? 0;
|
||||||
|
var company = await _unitOfWork.Companies.GetByIdAsync(companyId);
|
||||||
|
|
||||||
|
var companyInfo = new Application.DTOs.Company.CompanyInfoDto
|
||||||
|
{
|
||||||
|
CompanyName = company?.CompanyName ?? string.Empty,
|
||||||
|
Phone = company?.Phone,
|
||||||
|
Address = company?.Address,
|
||||||
|
City = company?.City,
|
||||||
|
State = company?.State,
|
||||||
|
ZipCode = company?.ZipCode,
|
||||||
|
PrimaryContactEmail = company?.PrimaryContactEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
var certs = await _unitOfWork.GiftCertificates.FindAsync(
|
||||||
|
gc => gc.BatchId == batchId, false,
|
||||||
|
gc => gc.RecipientCustomer);
|
||||||
|
|
||||||
|
if (!certs.Any())
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var dtos = certs.OrderBy(c => c.CertificateCode).Select(cert => new GiftCertificateDto
|
||||||
|
{
|
||||||
|
Id = cert.Id,
|
||||||
|
CertificateCode = cert.CertificateCode,
|
||||||
|
OriginalAmount = cert.OriginalAmount,
|
||||||
|
RedeemedAmount = cert.RedeemedAmount,
|
||||||
|
RemainingBalance = cert.RemainingBalance,
|
||||||
|
RecipientName = cert.RecipientCustomer != null
|
||||||
|
? (cert.RecipientCustomer.CompanyName ?? $"{cert.RecipientCustomer.ContactFirstName} {cert.RecipientCustomer.ContactLastName}".Trim())
|
||||||
|
: cert.RecipientName,
|
||||||
|
RecipientEmail = cert.RecipientEmail,
|
||||||
|
IssuedReason = cert.IssuedReason,
|
||||||
|
Status = cert.Status,
|
||||||
|
IssueDate = cert.IssueDate,
|
||||||
|
ExpiryDate = cert.ExpiryDate,
|
||||||
|
Notes = cert.Notes
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (logoData, logoContentType) = await LoadCompanyLogoAsync(company);
|
||||||
|
var pdfBytes = await _pdfService.GenerateBulkGiftCertificatePdfAsync(dtos, logoData, logoContentType, companyInfo);
|
||||||
|
var first = dtos.First().CertificateCode;
|
||||||
|
var last = dtos.Last().CertificateCode;
|
||||||
|
var fileName = dtos.Count == 1
|
||||||
|
? $"GiftCertificate-{first}.pdf"
|
||||||
|
: $"GiftCertificates-{first}-to-{last}.pdf";
|
||||||
|
return File(pdfBytes, "application/pdf", fileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error generating batch gift certificate PDF for batch {BatchId}", batchId);
|
||||||
|
TempData["Error"] = "Could not generate PDF.";
|
||||||
|
return RedirectToAction(nameof(BulkResult), new { batchId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company)
|
private async Task<(byte[]? LogoData, string? LogoContentType)> LoadCompanyLogoAsync(Company? company)
|
||||||
{
|
{
|
||||||
if (company == null) return (null, null);
|
if (company == null) return (null, null);
|
||||||
|
|||||||
@@ -916,8 +916,13 @@ public class KioskController : Controller
|
|||||||
ViewBag.SessionToken = session.SessionToken;
|
ViewBag.SessionToken = session.SessionToken;
|
||||||
ViewBag.SessionType = session.SessionType;
|
ViewBag.SessionType = session.SessionType;
|
||||||
|
|
||||||
// Reset to Welcome screen after 45 s of inactivity on any intake step.
|
// In-person kiosk: reset to Welcome screen after 45 s of inactivity so an
|
||||||
// The Welcome screen itself stays on indefinitely (no timeout override there).
|
// abandoned tablet doesn't stay on a customer's half-filled form indefinitely.
|
||||||
ViewBag.InactivityTimeoutMs = 45_000;
|
// Remote sessions: customer is on their own phone — never redirect; they may
|
||||||
|
// take several minutes between steps and have no KioskDevice cookie anyway.
|
||||||
|
if (session.SessionType == KioskSessionType.InPerson)
|
||||||
|
ViewBag.InactivityTimeoutMs = 45_000;
|
||||||
|
else
|
||||||
|
ViewBag.ShowInactivityTimer = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,4 +232,3 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,69 @@
|
|||||||
<span class="fw-semibold">Per-Company Breakdown</span>
|
<span class="fw-semibold">Per-Company Breakdown</span>
|
||||||
<span class="text-muted small">@Model.Rows.Count companies total</span>
|
<span class="text-muted small">@Model.Rows.Count companies total</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var row in Model.Rows)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);">
|
||||||
|
<i class="bi bi-robot"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@row.CompanyName @if (!row.IsActive) { <span class="badge bg-secondary ms-1">Inactive</span> }</h6>
|
||||||
|
<small><span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">@row.Plan</span></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Today</span>
|
||||||
|
<span class="mobile-card-value @(row.Today > 0 ? "fw-semibold" : "text-muted")">
|
||||||
|
@if (row.Today > 0) { @row.Today.ToString("N0") } else { <span>—</span> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">30 Days</span>
|
||||||
|
<span class="mobile-card-value @(row.Last30Days > 0 ? "fw-semibold" : "text-muted")">
|
||||||
|
@if (row.Last30Days > 0) { @row.Last30Days.ToString("N0") } else { <span>—</span> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">All Time</span>
|
||||||
|
<span class="mobile-card-value @(row.AllTime > 0 ? "" : "text-muted")">
|
||||||
|
@if (row.AllTime > 0) { @row.AllTime.ToString("N0") } else { <span>—</span> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (row.TopFeature != null)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Top Feature</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
<i class="bi @FeatureIcon(row.TopFeature) me-1 text-muted"></i>@row.FeatureDisplayName(row.TopFeature)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Tier</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge @row.TierBadgeClass">@row.UsageTier</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-controller="Companies" asp-action="Details" asp-route-id="@row.CompanyId" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-building me-1"></i>Company
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!Model.Rows.Any())
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-robot fs-1 d-block mb-2 opacity-25"></i>
|
||||||
|
No AI usage logged yet.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle" id="aiUsageTable">
|
<table class="table table-hover mb-0 align-middle" id="aiUsageTable">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -176,6 +176,60 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@if (Model.Items.Any())
|
@if (Model.Items.Any())
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var appointment in Model.Items)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
|
||||||
|
<i class="bi bi-calendar-event"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@appointment.Title</h6>
|
||||||
|
<small>@appointment.ScheduledStartTime.ToString("MMM dd, yyyy")<br />@(!appointment.IsAllDay ? $"{appointment.ScheduledStartTime:h:mm tt} – {appointment.ScheduledEndTime:h:mm tt}" : "All Day")</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 bg-@appointment.StatusColorClass">@appointment.StatusDisplayName</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Type</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
<span class="badge bg-@appointment.TypeColorClass">@appointment.TypeDisplayName</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(appointment.CustomerName))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Customer</span>
|
||||||
|
<span class="mobile-card-value">@appointment.CustomerName</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(appointment.AssignedWorkerName))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Worker</span>
|
||||||
|
<span class="mobile-card-value">@appointment.AssignedWorkerName</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
<a asp-action="Edit" asp-route-id="@appointment.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil me-1"></i>Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -21,6 +21,64 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var br in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #14b8a6 0%, #0f766e 100%);">
|
||||||
|
<i class="bi bi-bank"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@br.Account?.Name</h6>
|
||||||
|
<small>Statement: @br.StatementDate.ToString("MMM d, yyyy")</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">
|
||||||
|
@if (br.Status == BankReconciliationStatus.Completed)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Completed</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">In Progress</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Ending Balance</span>
|
||||||
|
<span class="mobile-card-value fw-semibold">@br.EndingBalance.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
@if (br.CompletedAt.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Completed By</span>
|
||||||
|
<span class="mobile-card-value">@br.CompletedBy</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
@if (br.Status == BankReconciliationStatus.Completed)
|
||||||
|
{
|
||||||
|
<a asp-action="Report" asp-route-id="@br.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-file-earmark-text me-1"></i>Report
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a asp-action="Reconcile" asp-route-id="@br.Id" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-check2-square me-1"></i>Continue
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -60,6 +60,59 @@
|
|||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
@if (active.Any())
|
@if (active.Any())
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var ban in active)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);">
|
||||||
|
<i class="bi bi-slash-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6 class="font-monospace">@ban.IpAddress</h6>
|
||||||
|
<small class="text-muted">@(ban.Reason ?? "No reason given")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Banned</span>
|
||||||
|
<span class="mobile-card-value">@ban.BannedAt.ToString("MMM d, yyyy HH:mm")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Expires</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@if (ban.ExpiresAt.HasValue)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">@ban.ExpiresAt.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Permanent</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<form asp-action="Lift" asp-route-id="@ban.Id" method="post" class="d-inline"
|
||||||
|
onsubmit="return confirm('Lift the ban on @ban.IpAddress?')">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-success">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Lift
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form asp-action="Delete" asp-route-id="@ban.Id" method="post" class="d-inline"
|
||||||
|
onsubmit="return confirm('Delete ban record for @ban.IpAddress?')">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@@ -130,6 +183,55 @@
|
|||||||
<h6 class="mb-0 text-muted"><i class="bi bi-clock-history"></i> Lifted / Expired Bans</h6>
|
<h6 class="mb-0 text-muted"><i class="bi bi-clock-history"></i> Lifted / Expired Bans</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var ban in inactive)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);">
|
||||||
|
<i class="bi bi-clock-history"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6 class="font-monospace">@ban.IpAddress</h6>
|
||||||
|
<small>
|
||||||
|
@if (!ban.IsActive)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Lifted</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Expired</span>
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
@if (!string.IsNullOrEmpty(ban.Reason))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Reason</span>
|
||||||
|
<span class="mobile-card-value text-muted">@ban.Reason</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Banned</span>
|
||||||
|
<span class="mobile-card-value text-muted">@ban.BannedAt.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<form asp-action="Delete" asp-route-id="@ban.Id" method="post" class="d-inline"
|
||||||
|
onsubmit="return confirm('Delete ban record for @ban.IpAddress?')">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash me-1"></i>Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover mb-0">
|
<table class="table table-sm table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -101,6 +101,73 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var m in Model)
|
||||||
|
{
|
||||||
|
var expired2 = m.ExpiryDate.HasValue && m.ExpiryDate.Value < DateTime.UtcNow
|
||||||
|
&& m.Status != CreditMemoStatus.FullyApplied
|
||||||
|
&& m.Status != CreditMemoStatus.Voided;
|
||||||
|
var (cmBadge, cmLabel) = m.Status switch
|
||||||
|
{
|
||||||
|
CreditMemoStatus.Active => ("bg-success-subtle text-success", "Active"),
|
||||||
|
CreditMemoStatus.PartiallyApplied => ("bg-warning-subtle text-warning", "Partial"),
|
||||||
|
CreditMemoStatus.FullyApplied => ("bg-secondary-subtle text-secondary", "Applied"),
|
||||||
|
CreditMemoStatus.Voided => ("bg-danger-subtle text-danger", "Voided"),
|
||||||
|
_ => ("bg-secondary-subtle text-secondary", m.Status.ToString())
|
||||||
|
};
|
||||||
|
var cmCustomer = string.IsNullOrWhiteSpace(m.Customer?.CompanyName)
|
||||||
|
? $"{m.Customer?.ContactFirstName} {m.Customer?.ContactLastName}".Trim()
|
||||||
|
: m.Customer!.CompanyName;
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = m.Id })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);">
|
||||||
|
<i class="bi bi-journal-minus"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@m.MemoNumber</h6>
|
||||||
|
<small>@cmCustomer</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 @cmBadge">@cmLabel</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Amount</span>
|
||||||
|
<span class="mobile-card-value">@m.Amount.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Remaining</span>
|
||||||
|
<span class="mobile-card-value @(m.RemainingBalance > 0 && m.Status != CreditMemoStatus.Voided ? "text-success fw-semibold" : "text-muted")">
|
||||||
|
@m.RemainingBalance.ToString("C")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Issued</span>
|
||||||
|
<span class="mobile-card-value">@m.IssueDate.ToLocalTime().ToString("MM/dd/yy")</span>
|
||||||
|
</div>
|
||||||
|
@if (m.ExpiryDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Expires</span>
|
||||||
|
<span class="mobile-card-value @(expired2 ? "text-danger fw-semibold" : "")">
|
||||||
|
@m.ExpiryDate.Value.ToLocalTime().ToString("MM/dd/yy")
|
||||||
|
@if (expired2) { <small>(Expired)</small> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@m.Id" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -118,6 +118,63 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var a in Model)
|
||||||
|
{
|
||||||
|
var fd = a.AccumulatedDepreciation >= (a.PurchaseCost - a.SalvageValue);
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = a.Id })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);">
|
||||||
|
<i class="bi bi-building-gear"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@a.Name</h6>
|
||||||
|
<small>Purchased @a.PurchaseDate.ToLocalTime().ToString("MM/dd/yyyy")</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">
|
||||||
|
@if (a.IsDisposed)
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Disposed</span>
|
||||||
|
}
|
||||||
|
else if (fd)
|
||||||
|
{
|
||||||
|
<span class="badge bg-light text-dark border">Fully Depreciated</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Cost</span>
|
||||||
|
<span class="mobile-card-value">@a.PurchaseCost.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Book Value</span>
|
||||||
|
<span class="mobile-card-value @(a.BookValue <= 0 ? "text-muted" : "text-success fw-semibold")">
|
||||||
|
@a.BookValue.ToString("C")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Monthly Depr.</span>
|
||||||
|
<span class="mobile-card-value">@a.MonthlyDepreciation.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@a.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
@model PowderCoating.Application.DTOs.GiftCertificate.BulkCreateGiftCertificateDto
|
||||||
|
@using PowderCoating.Core.Enums
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Bulk Create Gift Certificates";
|
||||||
|
ViewData["PageIcon"] = "bi-gift";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-header bg-white border-bottom py-3">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-collection me-2 text-primary"></i>Bulk Gift Certificate Generator
|
||||||
|
</h5>
|
||||||
|
<p class="text-muted small mb-0 mt-1">
|
||||||
|
Create a batch of certificates for car shows, events, or promotions. All certificates will have the same
|
||||||
|
face value and be generated with sequential codes ready to print.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<form asp-action="BulkCreate" method="post">
|
||||||
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label asp-for="Quantity" class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-123 me-1 text-muted"></i>@Html.DisplayNameFor(m => m.Quantity)
|
||||||
|
</label>
|
||||||
|
<input asp-for="Quantity" type="number" class="form-control form-control-lg"
|
||||||
|
min="1" max="500" placeholder="25" />
|
||||||
|
<span asp-validation-for="Quantity" class="text-danger small"></span>
|
||||||
|
<div class="form-text">Max 500 per batch.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-7">
|
||||||
|
<label asp-for="Amount" class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-currency-dollar me-1 text-muted"></i>@Html.DisplayNameFor(m => m.Amount)
|
||||||
|
</label>
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-text">$</span>
|
||||||
|
<input asp-for="Amount" type="number" class="form-control"
|
||||||
|
min="1" max="9999.99" step="0.01" placeholder="50.00" />
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="Amount" class="text-danger small"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label asp-for="IssuedReason" class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-tag me-1 text-muted"></i>@Html.DisplayNameFor(m => m.IssuedReason)
|
||||||
|
</label>
|
||||||
|
<select asp-for="IssuedReason" class="form-select">
|
||||||
|
@foreach (var reason in Enum.GetValues<GiftCertificateIssuedReason>())
|
||||||
|
{
|
||||||
|
<option value="@reason">@reason</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<span asp-validation-for="IssuedReason" class="text-danger small"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label asp-for="ExpiryDate" class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-calendar-x me-1 text-muted"></i>@Html.DisplayNameFor(m => m.ExpiryDate)
|
||||||
|
</label>
|
||||||
|
<input asp-for="ExpiryDate" type="date" class="form-control" />
|
||||||
|
<span asp-validation-for="ExpiryDate" class="text-danger small"></span>
|
||||||
|
<div class="form-text">Leave blank for no expiration.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label asp-for="Notes" class="form-label fw-semibold">
|
||||||
|
<i class="bi bi-chat-left-text me-1 text-muted"></i>@Html.DisplayNameFor(m => m.Notes)
|
||||||
|
</label>
|
||||||
|
<textarea asp-for="Notes" class="form-control" rows="2"
|
||||||
|
placeholder="e.g. Awarded at the 2026 Summer Car Show — thanks for attending!"></textarea>
|
||||||
|
<span asp-validation-for="Notes" class="text-danger small"></span>
|
||||||
|
<div class="form-text">Printed on every certificate in the batch.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview summary -->
|
||||||
|
<div id="batchPreview" class="alert alert-primary mt-4 mb-0" style="display:none">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
You are about to create <strong id="prevQty"></strong> certificates worth
|
||||||
|
<strong id="prevAmt"></strong> each — total face value
|
||||||
|
<strong id="prevTotal"></strong>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-4 pt-3 border-top">
|
||||||
|
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>Create Certificates
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="~/js/gift-certificate-bulk.js" asp-append-version="true"></script>
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
@model List<PowderCoating.Core.Entities.GiftCertificate>
|
||||||
|
@using PowderCoating.Core.Enums
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Batch Gift Certificates";
|
||||||
|
ViewData["PageIcon"] = "bi-gift";
|
||||||
|
var batchId = Model.FirstOrDefault()?.BatchId ?? Guid.Empty;
|
||||||
|
var count = Model.Count;
|
||||||
|
var amount = Model.FirstOrDefault()?.OriginalAmount ?? 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="alert alert-success alert-permanent mb-4">
|
||||||
|
<i class="bi bi-check-circle-fill me-2"></i>
|
||||||
|
<strong>@count gift certificates created</strong> — each worth @amount.ToString("C").
|
||||||
|
Download the PDF below to print the full batch. This page is bookmarkable — you can return here any time to re-download.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center py-3">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-collection me-2 text-primary"></i>Batch Certificates (@count)
|
||||||
|
<span class="text-muted small fw-normal ms-2 font-monospace">@batchId.ToString("N")[..8]…</span>
|
||||||
|
</h5>
|
||||||
|
<a asp-action="BatchDownloadPdf" asp-route-batchId="@batchId" class="btn btn-primary">
|
||||||
|
<i class="bi bi-file-pdf me-2"></i>Download All as PDF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var cert in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
|
||||||
|
<i class="bi bi-gift"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6 class="font-monospace">@cert.CertificateCode</h6>
|
||||||
|
<small>@cert.OriginalAmount.ToString("C")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Issued</span>
|
||||||
|
<span class="mobile-card-value">@cert.IssueDate.ToLocalTime().ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Expiry</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@if (cert.ExpiryDate.HasValue) { @cert.ExpiryDate.Value.ToLocalTime().ToString("MMM d, yyyy") } else { <span class="text-muted">—</span> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Status</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-success">Active</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
<a asp-action="DownloadPdf" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-file-pdf me-1"></i>PDF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-3">Certificate Code</th>
|
||||||
|
<th>Face Value</th>
|
||||||
|
<th>Issued</th>
|
||||||
|
<th>Expiry</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var cert in Model)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="ps-3 fw-semibold font-monospace">@cert.CertificateCode</td>
|
||||||
|
<td>@cert.OriginalAmount.ToString("C")</td>
|
||||||
|
<td>@cert.IssueDate.ToLocalTime().ToString("MMM d, yyyy")</td>
|
||||||
|
<td>
|
||||||
|
@(cert.ExpiryDate.HasValue
|
||||||
|
? cert.ExpiryDate.Value.ToLocalTime().ToString("MMM d, yyyy")
|
||||||
|
: "—")
|
||||||
|
</td>
|
||||||
|
<td><span class="badge bg-success">Active</span></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a asp-action="Details" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-secondary" title="View details">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a asp-action="DownloadPdf" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-secondary" title="Download single PDF">
|
||||||
|
<i class="bi bi-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white border-top d-flex justify-content-between align-items-center py-3">
|
||||||
|
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Back to Gift Certificates
|
||||||
|
</a>
|
||||||
|
<a asp-action="BatchDownloadPdf" asp-route-batchId="@batchId" class="btn btn-primary">
|
||||||
|
<i class="bi bi-printer me-2"></i>Print Batch PDF (@count pages)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-@statusClass alert-permanent d-flex align-items-center mb-4">
|
<div class="alert alert-@statusClass d-flex align-items-center mb-4">
|
||||||
<i class="bi bi-gift me-2" style="font-size:1.4rem;"></i>
|
<i class="bi bi-gift me-2" style="font-size:1.4rem;"></i>
|
||||||
<div>
|
<div>
|
||||||
<strong>@statusLabel</strong>
|
<strong>@statusLabel</strong>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
@if (Model.ExpiryDate.HasValue)
|
@if (Model.ExpiryDate.HasValue)
|
||||||
{
|
{
|
||||||
<span class="ms-2 small">· Expires @Model.ExpiryDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMMM d, yyyy")</span>
|
<span class="ms-2 small">· Expires @Model.ExpiryDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMMM d, yyyy")</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,10 +7,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<p class="text-muted mb-0">@ViewBag.TotalActive active certificates — @((ViewBag.TotalValue as decimal? ?? 0m).ToString("C")) outstanding value</p>
|
<p class="text-muted mb-0">@ViewBag.TotalActive active certificates — @((ViewBag.TotalValue as decimal? ?? 0m).ToString("C")) outstanding value</p>
|
||||||
<a asp-action="Create" class="btn btn-primary">
|
<div class="d-flex gap-2">
|
||||||
<i class="bi bi-plus-circle me-2"></i>New Certificate
|
<a asp-action="BulkCreate" class="btn btn-outline-primary">
|
||||||
</a>
|
<i class="bi bi-collection me-2"></i>Bulk Create
|
||||||
|
</a>
|
||||||
|
<a asp-action="Create" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>New Certificate
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -52,6 +57,73 @@ else
|
|||||||
{
|
{
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var cert in Model)
|
||||||
|
{
|
||||||
|
var (gcBadge, gcLabel) = cert.Status switch
|
||||||
|
{
|
||||||
|
GiftCertificateStatus.Active => ("bg-success", "Active"),
|
||||||
|
GiftCertificateStatus.PartiallyRedeemed => ("bg-info text-dark", "Partial"),
|
||||||
|
GiftCertificateStatus.FullyRedeemed => ("bg-secondary", "Used"),
|
||||||
|
GiftCertificateStatus.Expired => ("bg-warning text-dark", "Expired"),
|
||||||
|
GiftCertificateStatus.Voided => ("bg-danger", "Voided"),
|
||||||
|
_ => ("bg-secondary", cert.Status.ToString())
|
||||||
|
};
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = cert.Id })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);">
|
||||||
|
<i class="bi bi-gift"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6 class="font-monospace">@cert.CertificateCode</h6>
|
||||||
|
<small>@(cert.RecipientName ?? cert.RecipientEmail ?? "No recipient")</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 @gcBadge">@gcLabel</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Face Value</span>
|
||||||
|
<span class="mobile-card-value">@cert.OriginalAmount.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Remaining</span>
|
||||||
|
<span class="mobile-card-value @(cert.RemainingBalance > 0 ? "text-success fw-semibold" : "text-muted")">
|
||||||
|
@cert.RemainingBalance.ToString("C")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Issued</span>
|
||||||
|
<span class="mobile-card-value">@cert.IssueDate.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy")</span>
|
||||||
|
</div>
|
||||||
|
@if (cert.ExpiryDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Expires</span>
|
||||||
|
<span class="mobile-card-value @(cert.ExpiryDate.Value < DateTime.Now ? "text-danger" : "")">
|
||||||
|
@cert.ExpiryDate.Value.ToString("MM/dd/yy")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@cert.Id" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
@if (cert.BatchId.HasValue)
|
||||||
|
{
|
||||||
|
<a asp-action="BulkResult" asp-route-batchId="@cert.BatchId" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-collection me-1"></i>Batch
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@@ -75,6 +147,14 @@ else
|
|||||||
<a asp-action="Details" asp-route-id="@cert.Id" class="fw-semibold text-decoration-none font-monospace">
|
<a asp-action="Details" asp-route-id="@cert.Id" class="fw-semibold text-decoration-none font-monospace">
|
||||||
@cert.CertificateCode
|
@cert.CertificateCode
|
||||||
</a>
|
</a>
|
||||||
|
@if (cert.BatchId.HasValue)
|
||||||
|
{
|
||||||
|
<a asp-action="BulkResult" asp-route-batchId="@cert.BatchId"
|
||||||
|
class="badge bg-primary-subtle text-primary text-decoration-none ms-1"
|
||||||
|
title="View & download batch">
|
||||||
|
<i class="bi bi-collection me-1"></i>Batch
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (!string.IsNullOrEmpty(cert.RecipientName))
|
@if (!string.IsNullOrEmpty(cert.RecipientName))
|
||||||
@@ -83,7 +163,7 @@ else
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrEmpty(cert.RecipientEmail))
|
@if (!string.IsNullOrEmpty(cert.RecipientEmail))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,61 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var n in items)
|
||||||
|
{
|
||||||
|
bool mIsRead = (bool)n.IsRead;
|
||||||
|
string mTitle = (string)n.Title;
|
||||||
|
string mMessage = (string)n.Message;
|
||||||
|
string? mLink = (string?)n.Link;
|
||||||
|
string mType = (string)n.NotificationType;
|
||||||
|
DateTime mCreatedAt = ((DateTime)n.CreatedAt).Tz(ViewBag.CompanyTimeZone as string);
|
||||||
|
<div class="mobile-data-card notif-history-row @(!mIsRead ? "notif-unread" : "")"
|
||||||
|
data-id="@n.Id"
|
||||||
|
data-title="@mTitle"
|
||||||
|
data-message="@mMessage"
|
||||||
|
data-link="@(mLink ?? "")"
|
||||||
|
data-type="@mType"
|
||||||
|
data-is-read="@(mIsRead ? "1" : "0")"
|
||||||
|
data-created-at="@mCreatedAt.ToString("MMM d, yyyy h:mm tt")">
|
||||||
|
<div class="mobile-card-header" style="@(!mIsRead ? "background:rgba(99,102,241,0.08);" : "")">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">
|
||||||
|
<i class="bi bi-bell"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6 class="@(!mIsRead ? "fw-semibold" : "text-muted")">
|
||||||
|
@if (!mIsRead)
|
||||||
|
{
|
||||||
|
<span style="display:inline-block;width:8px;height:8px;background:#6366f1;border-radius:50%;margin-right:6px;"></span>
|
||||||
|
}
|
||||||
|
@mTitle
|
||||||
|
</h6>
|
||||||
|
<small>@mCreatedAt.ToString("MMM d, yyyy h:mm tt")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Type</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-secondary bg-opacity-25 text-body small">@mType</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row" style="align-items:flex-start;">
|
||||||
|
<span class="mobile-card-label">Message</span>
|
||||||
|
<span class="mobile-card-value" style="white-space:normal;text-align:right;">@mMessage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(mLink))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a href="@mLink" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-arrow-right me-1"></i>Open
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -126,6 +126,77 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@{ lastMfr = null; }
|
||||||
|
@foreach (var item in needOrder)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedMfr) && item.Manufacturer != lastMfr)
|
||||||
|
{
|
||||||
|
lastMfr = item.Manufacturer;
|
||||||
|
<div class="text-uppercase fw-semibold text-muted small px-2 py-1 border-bottom mt-2">
|
||||||
|
@(string.IsNullOrWhiteSpace(item.Manufacturer) ? "No Manufacturer" : item.Manufacturer)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.ColorCode))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-icon" style="background: @(item.ColorCode.StartsWith("#") ? item.ColorCode : "#" + item.ColorCode); border: 1px solid var(--bs-border-color);"></div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #64748b 0%, #475569 100%);">
|
||||||
|
<i class="bi bi-palette"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@(item.ColorName ?? item.Name)</h6>
|
||||||
|
<small>@(item.Manufacturer ?? "No Manufacturer")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.ManufacturerPartNumber))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Part #</span>
|
||||||
|
<span class="mobile-card-value text-muted">@item.ManufacturerPartNumber</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.Finish))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Finish</span>
|
||||||
|
<span class="mobile-card-value">@item.Finish</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">In Stock</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@if (item.QuantityOnHand > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success bg-opacity-10 text-success">@item.QuantityOnHand.ToString("N2") @item.UnitOfMeasure</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<button class="btn btn-sm btn-outline-success btn-toggle-panel"
|
||||||
|
data-item-id="@item.Id" data-has-panel="true">
|
||||||
|
<i class="bi bi-check-lg me-1"></i>Got It
|
||||||
|
</button>
|
||||||
|
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0" id="needTable">
|
<table class="table table-hover mb-0" id="needTable">
|
||||||
<thead class="table-group-divider">
|
<thead class="table-group-divider">
|
||||||
@@ -220,6 +291,68 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@{ lastMfr = null; }
|
||||||
|
@foreach (var item in onHand)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedMfr) && item.Manufacturer != lastMfr)
|
||||||
|
{
|
||||||
|
lastMfr = item.Manufacturer;
|
||||||
|
<div class="text-uppercase fw-semibold text-muted small px-2 py-1 border-bottom mt-2">
|
||||||
|
@(string.IsNullOrWhiteSpace(item.Manufacturer) ? "No Manufacturer" : item.Manufacturer)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.ColorCode))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-icon" style="background: @(item.ColorCode.StartsWith("#") ? item.ColorCode : "#" + item.ColorCode); border: 1px solid var(--bs-border-color);"></div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #059669 0%, #047857 100%);">
|
||||||
|
<i class="bi bi-palette"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@(item.ColorName ?? item.Name)</h6>
|
||||||
|
<small>@(item.Manufacturer ?? "No Manufacturer")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.ManufacturerPartNumber))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Part #</span>
|
||||||
|
<span class="mobile-card-value text-muted">@item.ManufacturerPartNumber</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.Finish))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Finish</span>
|
||||||
|
<span class="mobile-card-value">@item.Finish</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Status</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>On Wall</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<button class="btn btn-sm btn-outline-danger btn-toggle-panel"
|
||||||
|
data-item-id="@item.Id" data-has-panel="false">
|
||||||
|
<i class="bi bi-x-lg me-1"></i>Remove
|
||||||
|
</button>
|
||||||
|
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-group-divider">
|
<thead class="table-group-divider">
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>@inv.InvoiceDate.ToString("MM/dd/yyyy")</td>
|
<td>@inv.InvoiceDate.ToString("MM/dd/yyyy")</td>
|
||||||
<td class="@(inv.IsOverdue ? "fw-bold text-danger" : "")">
|
<td class="@(inv.IsOverdue ? "fw-bold text-danger" : "")">
|
||||||
@(inv.DueDate.HasValue ? inv.DueDate.Value.ToString("MM/dd/yyyy") : "—")
|
@(inv.DueDate.HasValue ? inv.DueDate.Value.ToString("MM/dd/yyyy") : "—")
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">@inv.Total.ToString("C")</td>
|
<td class="text-end">@inv.Total.ToString("C")</td>
|
||||||
<td class="text-end @(inv.BalanceDue > 0 ? "fw-semibold" : "text-muted")">
|
<td class="text-end @(inv.BalanceDue > 0 ? "fw-semibold" : "text-muted")">
|
||||||
@@ -167,6 +167,77 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var inv in Model.Items)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Invoices", new { id = inv.Id })'">
|
||||||
|
<div class="mobile-card-header" style="@(inv.IsOverdue ? "background:#fee2e2;" : "")">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
|
||||||
|
<i class="bi bi-receipt"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@inv.InvoiceNumber</h6>
|
||||||
|
<small>@inv.CustomerName</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">
|
||||||
|
@await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.InvoiceStatus(inv.Status), Text: InvoicesController.GetStatusDisplay(inv.Status)))
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (inv.JobId.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Job</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@inv.JobId"
|
||||||
|
class="text-decoration-none" onclick="event.stopPropagation()">
|
||||||
|
@inv.JobNumber
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Date</span>
|
||||||
|
<span class="mobile-card-value">@inv.InvoiceDate.ToString("MM/dd/yy")</span>
|
||||||
|
</div>
|
||||||
|
@if (inv.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Due</span>
|
||||||
|
<span class="mobile-card-value @(inv.IsOverdue ? "fw-bold text-danger" : "")">
|
||||||
|
@inv.DueDate.Value.ToString("MM/dd/yy")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Total</span>
|
||||||
|
<span class="mobile-card-value">@inv.Total.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Balance Due</span>
|
||||||
|
<span class="mobile-card-value @(inv.BalanceDue > 0 ? "fw-semibold" : "text-muted")">
|
||||||
|
@inv.BalanceDue.ToString("C")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@inv.Id"
|
||||||
|
class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
<a asp-action="DownloadPdf" asp-route-id="@inv.Id"
|
||||||
|
class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-file-pdf me-1"></i>PDF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="px-3">
|
<div class="px-3">
|
||||||
@await Html.PartialAsync("_Pagination", Model)
|
@await Html.PartialAsync("_Pagination", Model)
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,6 +71,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var job in overdueJobs)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@job.JobNumber</h6>
|
||||||
|
<small>@job.CustomerName</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 bg-@job.StatusColorClass">@job.StatusDisplayName</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Priority</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-@job.PriorityColorClass">@job.PriorityDisplayName</span></span>
|
||||||
|
</div>
|
||||||
|
@if (job.ScheduledDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Scheduled</span>
|
||||||
|
<span class="mobile-card-value text-danger fw-bold">@job.ScheduledDate.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (job.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Due</span>
|
||||||
|
<span class="mobile-card-value text-danger">@job.DueDate.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.JobId" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openPriorityModal(@job.JobId, @job.JobPriorityId, '@job.JobNumber')">
|
||||||
|
<i class="bi bi-flag"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openWorkerModal(@job.JobId, '@(job.AssignedUserId ?? "")', '@job.JobNumber')">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -191,6 +244,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var job in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Jobs", new { id = job.JobId })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);">
|
||||||
|
<i class="bi bi-kanban"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@job.JobNumber</h6>
|
||||||
|
<small>@job.CustomerName</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 bg-@job.StatusColorClass" id="status-badge-@job.JobId">@job.StatusDisplayName</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Priority</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-@job.PriorityColorClass priority-badge-@job.JobId">@job.PriorityDisplayName</span></span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(job.AssignedWorkerName))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Worker</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-info"><i class="bi bi-person me-1"></i>@job.AssignedWorkerName</span></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (job.ScheduledDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Scheduled</span>
|
||||||
|
<span class="mobile-card-value">@job.ScheduledDate.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (job.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
var mJobOverdue = job.DueDate.Value.Date < DateTime.Today;
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Due</span>
|
||||||
|
<span class="mobile-card-value @(mJobOverdue ? "text-danger fw-bold" : "")">@job.DueDate.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-controller="Jobs" asp-action="Details" asp-route-id="@job.JobId" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openPriorityModal(@job.JobId, @job.JobPriorityId, '@job.JobNumber')" title="Change Priority">
|
||||||
|
<i class="bi bi-flag"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openWorkerModal(@job.JobId, '@(job.AssignedUserId ?? "")', '@job.JobNumber')" title="Assign Worker">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!Model.Any())
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-calendar-check fs-1 d-block mb-2 opacity-25"></i>
|
||||||
|
No jobs scheduled for @scheduledDate.ToString("MMMM dd, yyyy").
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0" id="jobsTable">
|
<table class="table table-hover mb-0" id="jobsTable">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -352,6 +473,65 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var item in maintenanceItems)
|
||||||
|
{
|
||||||
|
var mPriorityBg = item.Priority switch
|
||||||
|
{
|
||||||
|
MaintenancePriority.Critical => "danger",
|
||||||
|
MaintenancePriority.High => "warning",
|
||||||
|
MaintenancePriority.Normal => "info",
|
||||||
|
_ => "secondary"
|
||||||
|
};
|
||||||
|
var mStatusBgM = item.Status == MaintenanceStatus.InProgress ? "success" : "primary";
|
||||||
|
var mStatusLbl = item.Status == MaintenanceStatus.InProgress ? "In Progress" : "Scheduled";
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Maintenance", new { id = item.Id })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #b45309 100%);">
|
||||||
|
<i class="bi bi-tools"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@(item.Equipment?.EquipmentName ?? "Maintenance")</h6>
|
||||||
|
<small>@item.MaintenanceType</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Priority</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-@mPriorityBg">@item.Priority</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Status</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-@mStatusBgM">@mStatusLbl</span></span>
|
||||||
|
</div>
|
||||||
|
@if (item.AssignedUser != null)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Worker</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge bg-info text-dark"><i class="bi bi-person me-1"></i>@item.AssignedUser.FullName</span></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(item.Description))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Desc.</span>
|
||||||
|
<span class="mobile-card-value text-muted">@item.Description</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-controller="Maintenance" asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation(); openMaintenanceWorkerModal(@item.Id, '@(item.AssignedUserId ?? "")', '@(item.Equipment?.EquipmentName ?? "Maintenance")')" title="Assign Worker">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -47,6 +47,68 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var je in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = je.Id })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">
|
||||||
|
<i class="bi bi-journal-text"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>
|
||||||
|
@je.EntryNumber
|
||||||
|
@if (je.IsReversal)
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary ms-1">REV</span>
|
||||||
|
}
|
||||||
|
</h6>
|
||||||
|
<small>@je.EntryDate.ToString("MMM d, yyyy")</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">
|
||||||
|
@if (je.Status == JournalEntryStatus.Draft)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Draft</span>
|
||||||
|
}
|
||||||
|
else if (je.Status == JournalEntryStatus.Posted)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Posted</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Reversed</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(je.Description))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Description</span>
|
||||||
|
<span class="mobile-card-value" style="white-space:normal;text-align:right;">@je.Description</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(je.Reference))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Reference</span>
|
||||||
|
<span class="mobile-card-value">@je.Reference</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@je.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
string activeFilter = ViewBag.ActiveFilter as string ?? "all";
|
string activeFilter = ViewBag.ActiveFilter as string ?? "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="container-fluid px-4">
|
<div>
|
||||||
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-2">
|
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-2">
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<i class="bi bi-clipboard-check fs-3 text-primary"></i>
|
<i class="bi bi-clipboard-check fs-3 text-primary"></i>
|
||||||
@@ -53,17 +53,116 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var s in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);">
|
||||||
|
<i class="bi bi-clipboard-check"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@s.CustomerFullName</h6>
|
||||||
|
<small>@(s.SubmittedAt?.ToLocalTime().ToString("MM/dd/yy h:mm tt") ?? s.ExpiresAt.AddHours(-2).ToLocalTime().ToString("MM/dd/yy 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">
|
||||||
|
@if (s.Status == KioskSessionStatus.Submitted && s.IsConverted)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Converted</span>
|
||||||
|
}
|
||||||
|
else if (s.Status == KioskSessionStatus.Submitted)
|
||||||
|
{
|
||||||
|
<span class="badge bg-info text-dark">Submitted</span>
|
||||||
|
}
|
||||||
|
else if (s.Status == KioskSessionStatus.Active && !s.IsExpired)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">In Progress</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Expired</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Type</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@if (s.SessionType == KioskSessionType.InPerson)
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary-subtle text-primary"><i class="bi bi-tablet me-1"></i>In-Person</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge" style="background:#ede9fe;color:#6d28d9;"><i class="bi bi-envelope me-1"></i>Remote</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(s.CustomerPhone))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Phone</span>
|
||||||
|
<span class="mobile-card-value"><a href="tel:@s.CustomerPhone">@s.CustomerPhone</a></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(s.CustomerEmail))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Email</span>
|
||||||
|
<span class="mobile-card-value" style="white-space:normal;"><a href="mailto:@s.CustomerEmail">@s.CustomerEmail</a></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (s.LinkedCustomerId.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Matched</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
<a href="/Customers/Details/@s.LinkedCustomerId" class="text-success">
|
||||||
|
<i class="bi bi-person-check me-1"></i>Customer record
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
@if (s.LinkedJobId.HasValue)
|
||||||
|
{
|
||||||
|
<a href="/Jobs/Details/@s.LinkedJobId" class="btn btn-sm btn-outline-success">
|
||||||
|
<i class="bi bi-briefcase me-1"></i>Job
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (s.LinkedQuoteId.HasValue)
|
||||||
|
{
|
||||||
|
<a href="/Quotes/Details/@s.LinkedQuoteId" class="btn btn-sm btn-outline-info">
|
||||||
|
<i class="bi bi-file-earmark-text me-1"></i>Quote
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (s.LinkedCustomerId.HasValue)
|
||||||
|
{
|
||||||
|
<a href="/Customers/Details/@s.LinkedCustomerId" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-person me-1"></i>Customer
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<th class="d-none d-md-table-cell">Date</th>
|
||||||
<th>Customer</th>
|
<th>Customer</th>
|
||||||
<th>Contact</th>
|
<th class="d-none d-lg-table-cell">Contact</th>
|
||||||
<th>Project</th>
|
<th class="d-none d-lg-table-cell">Project</th>
|
||||||
<th>Type</th>
|
<th class="d-none d-sm-table-cell">Type</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>SMS</th>
|
<th class="d-none d-md-table-cell">SMS</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -71,7 +170,7 @@
|
|||||||
@foreach (var s in Model)
|
@foreach (var s in Model)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-nowrap text-muted small">
|
<td class="text-nowrap text-muted small d-none d-md-table-cell">
|
||||||
@(s.SubmittedAt?.ToLocalTime().ToString("MM/dd/yy h:mm tt") ?? s.ExpiresAt.AddHours(-2).ToLocalTime().ToString("MM/dd/yy h:mm tt"))
|
@(s.SubmittedAt?.ToLocalTime().ToString("MM/dd/yy h:mm tt") ?? s.ExpiresAt.AddHours(-2).ToLocalTime().ToString("MM/dd/yy h:mm tt"))
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -82,8 +181,12 @@
|
|||||||
<i class="bi bi-person-check me-1"></i>Customer matched
|
<i class="bi bi-person-check me-1"></i>Customer matched
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
@* Show date inline on mobile since the Date column is hidden *@
|
||||||
|
<div class="text-muted small d-md-none">
|
||||||
|
@(s.SubmittedAt?.ToLocalTime().ToString("MM/dd/yy h:mm tt") ?? s.ExpiresAt.AddHours(-2).ToLocalTime().ToString("MM/dd/yy h:mm tt"))
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="small text-muted">
|
<td class="small text-muted d-none d-lg-table-cell">
|
||||||
@if (!string.IsNullOrEmpty(s.CustomerPhone))
|
@if (!string.IsNullOrEmpty(s.CustomerPhone))
|
||||||
{
|
{
|
||||||
<div><i class="bi bi-telephone me-1"></i>@s.CustomerPhone</div>
|
<div><i class="bi bi-telephone me-1"></i>@s.CustomerPhone</div>
|
||||||
@@ -93,11 +196,11 @@
|
|||||||
<div><i class="bi bi-envelope me-1"></i>@s.CustomerEmail</div>
|
<div><i class="bi bi-envelope me-1"></i>@s.CustomerEmail</div>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td style="max-width:280px;">
|
<td class="d-none d-lg-table-cell" style="max-width:280px;">
|
||||||
<span class="text-truncate d-block" style="max-width:260px;"
|
<span class="text-truncate d-block" style="max-width:260px;"
|
||||||
title="@s.JobDescription">@s.JobDescriptionSnippet</span>
|
title="@s.JobDescription">@s.JobDescriptionSnippet</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="d-none d-sm-table-cell">
|
||||||
@if (s.SessionType == KioskSessionType.InPerson)
|
@if (s.SessionType == KioskSessionType.InPerson)
|
||||||
{
|
{
|
||||||
<span class="badge bg-primary-subtle text-primary">
|
<span class="badge bg-primary-subtle text-primary">
|
||||||
@@ -129,7 +232,7 @@
|
|||||||
<span class="badge bg-secondary">Expired</span>
|
<span class="badge bg-secondary">Expired</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="d-none d-md-table-cell">
|
||||||
@if (s.SmsOptIn)
|
@if (s.SmsOptIn)
|
||||||
{
|
{
|
||||||
<i class="bi bi-check-circle-fill text-success" title="SMS opt-in"></i>
|
<i class="bi bi-check-circle-fill text-success" title="SMS opt-in"></i>
|
||||||
@@ -143,19 +246,19 @@
|
|||||||
@if (s.LinkedJobId.HasValue)
|
@if (s.LinkedJobId.HasValue)
|
||||||
{
|
{
|
||||||
<a href="/Jobs/Details/@s.LinkedJobId" class="btn btn-sm btn-outline-success me-1">
|
<a href="/Jobs/Details/@s.LinkedJobId" class="btn btn-sm btn-outline-success me-1">
|
||||||
<i class="bi bi-briefcase me-1"></i>View Job
|
<i class="bi bi-briefcase me-1"></i><span class="d-none d-sm-inline">View Job</span><span class="d-sm-none">Job</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (s.LinkedQuoteId.HasValue)
|
@if (s.LinkedQuoteId.HasValue)
|
||||||
{
|
{
|
||||||
<a href="/Quotes/Details/@s.LinkedQuoteId" class="btn btn-sm btn-outline-info me-1">
|
<a href="/Quotes/Details/@s.LinkedQuoteId" class="btn btn-sm btn-outline-info me-1">
|
||||||
<i class="bi bi-file-earmark-text me-1"></i>View Quote
|
<i class="bi bi-file-earmark-text me-1"></i><span class="d-none d-sm-inline">View Quote</span><span class="d-sm-none">Quote</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@if (s.LinkedCustomerId.HasValue)
|
@if (s.LinkedCustomerId.HasValue)
|
||||||
{
|
{
|
||||||
<a href="/Customers/Details/@s.LinkedCustomerId" class="btn btn-sm btn-outline-primary">
|
<a href="/Customers/Details/@s.LinkedCustomerId" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-person me-1"></i>Customer
|
<i class="bi bi-person me-1"></i><span class="d-none d-sm-inline">Customer</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -134,6 +134,67 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var item in Model.Items)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: @(item.Channel == PowderCoating.Core.Enums.NotificationChannel.Email ? "linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)" : "linear-gradient(135deg, #06b6d4 0%, #0e7490 100%)");">
|
||||||
|
<i class="bi @(item.Channel == PowderCoating.Core.Enums.NotificationChannel.Email ? "bi-envelope" : "bi-phone")"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@item.RecipientName</h6>
|
||||||
|
<small>@item.Recipient</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Type</span>
|
||||||
|
<span class="mobile-card-value">@item.NotificationTypeDisplay</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Sent</span>
|
||||||
|
<span class="mobile-card-value">@item.SentAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd HH:mm")</span>
|
||||||
|
</div>
|
||||||
|
@if (item.JobId.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Job</span>
|
||||||
|
<span class="mobile-card-value">@item.JobNumber</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (item.QuoteId.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Quote</span>
|
||||||
|
<span class="mobile-card-value">@item.QuoteNumber</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Status</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@{
|
||||||
|
var (mStatusBadge, mStatusIcon) = item.Status switch
|
||||||
|
{
|
||||||
|
PowderCoating.Core.Enums.NotificationStatus.Sent => ("bg-success", "bi-check-circle"),
|
||||||
|
PowderCoating.Core.Enums.NotificationStatus.Failed => ("bg-danger", "bi-x-circle"),
|
||||||
|
_ => ("bg-secondary", "bi-dash-circle")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<span class="badge @mStatusBadge"><i class="bi @mStatusIcon me-1"></i>@item.StatusDisplay</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -44,6 +44,91 @@
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var row in Model.Rows)
|
||||||
|
{
|
||||||
|
var oPct = row.TotalSteps == 0 ? 0 : row.StepsCompleted * 100 / row.TotalSteps;
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Companies", new { id = row.CompanyId })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
|
||||||
|
<i class="bi bi-building"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@row.CompanyName</h6>
|
||||||
|
<small>
|
||||||
|
@switch (row.Status)
|
||||||
|
{
|
||||||
|
case OnboardingStatus.Complete:
|
||||||
|
<span class="badge bg-success">Complete</span>
|
||||||
|
break;
|
||||||
|
case OnboardingStatus.InProgress:
|
||||||
|
<span class="badge bg-warning text-dark">In Progress</span>
|
||||||
|
break;
|
||||||
|
case OnboardingStatus.Dismissed:
|
||||||
|
<span class="badge bg-secondary">Dismissed</span>
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
<span class="badge bg-light text-muted border">Not Started</span>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Wizard</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@if (row.WizardCompleted)
|
||||||
|
{
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
<span class="text-success ms-1">Done</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="bi bi-circle text-muted"></i>
|
||||||
|
<span class="text-muted ms-1">Pending</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Milestones</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="progress" style="height:5px; width:60px;">
|
||||||
|
<div class="progress-bar @(oPct == 100 ? "bg-success" : "bg-primary")" style="width:@oPct%"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">@row.StepsCompleted/@row.TotalSteps</small>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@{
|
||||||
|
var oFirstActivity = row.FirstJobCreatedAt ?? row.FirstQuoteCreatedAt;
|
||||||
|
}
|
||||||
|
@if (oFirstActivity.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">First Activity</span>
|
||||||
|
<span class="mobile-card-value text-muted">@oFirstActivity.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-controller="Companies" asp-action="Details" asp-route-id="@row.CompanyId" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-building me-1"></i>View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!Model.Rows.Any())
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-building fs-1 d-block mb-2 opacity-25"></i>
|
||||||
|
No companies found.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0" id="onboardingTable">
|
<table class="table table-hover align-middle mb-0" id="onboardingTable">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -164,6 +164,56 @@
|
|||||||
<!-- Grid -->
|
<!-- Grid -->
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var po in Model.Items)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = po.Id })'">
|
||||||
|
<div class="mobile-card-header" style="@(po.IsOverdue ? "background:#fee2e2;" : "")">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);">
|
||||||
|
<i class="bi bi-cart-check"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@po.PoNumber @(po.IsOverdue ? " — Overdue" : "")</h6>
|
||||||
|
<small>@po.VendorName</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 bg-@StatusBadge(po.Status)">@po.Status</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Order Date</span>
|
||||||
|
<span class="mobile-card-value">@po.OrderDate.ToString("MM/dd/yy")</span>
|
||||||
|
</div>
|
||||||
|
@if (po.ExpectedDeliveryDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Expected</span>
|
||||||
|
<span class="mobile-card-value @(po.IsOverdue ? "text-danger fw-semibold" : "")">
|
||||||
|
@po.ExpectedDeliveryDate.Value.ToString("MM/dd/yy")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Items</span>
|
||||||
|
<span class="mobile-card-value">@po.ItemCount</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Total</span>
|
||||||
|
<span class="mobile-card-value fw-semibold">$@po.TotalAmount.ToString("N2")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@po.Id" class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -38,6 +38,96 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var t in Model)
|
||||||
|
{
|
||||||
|
var isOverdueRT = t.IsActive && t.NextFireDate.Date < DateTime.Today;
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);">
|
||||||
|
<i class="bi bi-arrow-repeat"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@t.Name</h6>
|
||||||
|
<small>
|
||||||
|
@if (t.TemplateType == RecurringTemplateType.Bill)
|
||||||
|
{
|
||||||
|
<span>Bill</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Expense</span>
|
||||||
|
}
|
||||||
|
—
|
||||||
|
@(t.IntervalCount == 1 ? t.Frequency.ToString() : $"Every {t.IntervalCount} × {t.Frequency}")
|
||||||
|
</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">
|
||||||
|
@if (t.IsActive)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success"><i class="bi bi-play-fill me-1"></i>Active</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary"><i class="bi bi-pause-fill me-1"></i>Paused</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (t.IsActive)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Next Fire</span>
|
||||||
|
<span class="mobile-card-value @(isOverdueRT ? "text-danger fw-semibold" : "")">
|
||||||
|
@t.NextFireDate.ToString("MM/dd/yyyy")
|
||||||
|
@if (isOverdueRT) { <i class="bi bi-exclamation-circle ms-1"></i> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Occurrences</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@t.OccurrenceCount
|
||||||
|
@if (t.MaxOccurrences.HasValue) { <span class="text-muted"> / @t.MaxOccurrences</span> }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(t.LastError))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Error</span>
|
||||||
|
<span class="mobile-card-value text-danger small">@t.LastError</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Edit" asp-route-id="@t.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form asp-action="ToggleActive" asp-route-id="@t.Id" method="post" style="display:inline;">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm @(t.IsActive ? "btn-outline-warning" : "btn-outline-success")">
|
||||||
|
<i class="bi @(t.IsActive ? "bi-pause" : "bi-play")"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@if (t.IsActive)
|
||||||
|
{
|
||||||
|
<form asp-action="GenerateNow" asp-route-id="@t.Id" method="post" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Generate one occurrence now?')">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-lightning-charge"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -65,6 +65,77 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var note in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);">
|
||||||
|
<i class="bi bi-journal-text"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6><code>v@(note.Version)</code> — @note.Title</h6>
|
||||||
|
<small>
|
||||||
|
<span class="badge @TagBadge(note.Tag)">@note.Tag</span>
|
||||||
|
|
||||||
|
@if (note.IsPublished)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Published</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Draft</span>
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Released</span>
|
||||||
|
<span class="mobile-card-value text-muted">@note.ReleasedAt.ToString("MM/dd/yyyy")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Created By</span>
|
||||||
|
<span class="mobile-card-value text-muted">@note.CreatedByUserName</span>
|
||||||
|
</div>
|
||||||
|
@if (note.Body.Length > 0)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Preview</span>
|
||||||
|
<span class="mobile-card-value text-muted">@(note.Body.Length > 60 ? note.Body[..60] + "…" : note.Body)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Edit" asp-route-id="@note.Id" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form asp-action="TogglePublish" asp-route-id="@note.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm @(note.IsPublished ? "btn-outline-warning" : "btn-outline-success")">
|
||||||
|
<i class="bi @(note.IsPublished ? "bi-eye-slash" : "bi-eye")"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form asp-action="Delete" asp-route-id="@note.Id" method="post" class="d-inline"
|
||||||
|
onsubmit="return confirm('Delete v@(note.Version)?')">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!Model.Any())
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-journal-x fs-1 d-block mb-2 opacity-25"></i>
|
||||||
|
No release notes yet. <a asp-action="Create">Create the first one.</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0 small">
|
<table class="table table-hover align-middle mb-0 small">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -112,6 +112,101 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var row in Model)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);">
|
||||||
|
<i class="bi bi-building"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>
|
||||||
|
@row.CompanyName
|
||||||
|
@if (row.IsDeleted) { <span class="badge bg-secondary ms-1">Deleted</span> }
|
||||||
|
</h6>
|
||||||
|
<small>
|
||||||
|
@if (row.SmsDisabledByAdmin)
|
||||||
|
{
|
||||||
|
<span class="text-danger"><i class="bi bi-slash-circle me-1"></i>Admin-Disabled</span>
|
||||||
|
}
|
||||||
|
else if (row.SmsEnabled)
|
||||||
|
{
|
||||||
|
<span class="text-success"><i class="bi bi-chat-dots me-1"></i>SMS Enabled</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">SMS Off</span>
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Terms</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
@{
|
||||||
|
var dispAgreement = row.CurrentAgreement ?? row.LatestAgreement;
|
||||||
|
}
|
||||||
|
@if (row.CurrentAgreement != null)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">v@(row.CurrentAgreement.TermsVersion)</span>
|
||||||
|
}
|
||||||
|
else if (row.LatestAgreement != null)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Stale (v@(row.LatestAgreement.TermsVersion))</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-light text-muted border">Never</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (dispAgreement != null)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Accepted By</span>
|
||||||
|
<span class="mobile-card-value @(row.CurrentAgreement == null ? "text-muted" : "")">
|
||||||
|
@dispAgreement.AgreedByUserName
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Accepted At</span>
|
||||||
|
<span class="mobile-card-value @(row.CurrentAgreement == null ? "text-muted" : "")">
|
||||||
|
@dispAgreement.AgreedAt.ToString("MM/dd/yy")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (row.AllAgreements.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">History</span>
|
||||||
|
<span class="mobile-card-value">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#historyModal"
|
||||||
|
data-company="@row.CompanyName"
|
||||||
|
data-history="@System.Text.Json.JsonSerializer.Serialize(row.AllAgreements.Select(a => new {
|
||||||
|
a.TermsVersion,
|
||||||
|
a.AgreedByUserName,
|
||||||
|
a.AgreedByUserId,
|
||||||
|
AgreedAt = a.AgreedAt.ToString("MMM d, yyyy 'at' h:mm tt") + " UTC",
|
||||||
|
IpAddress = a.IpAddress ?? "—",
|
||||||
|
UserAgent = a.UserAgent ?? "—"
|
||||||
|
}), new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase })"
|
||||||
|
onclick="event.stopPropagation()">
|
||||||
|
@row.AllAgreements.Count <i class="bi bi-clock-history ms-1"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -110,6 +110,64 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var row in Model.Rows)
|
||||||
|
{
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", "Customers", new { id = row.CustomerId })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #ec4899 0%, #be185d 100%);">
|
||||||
|
<i class="bi bi-phone-vibrate"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@row.CustomerName</h6>
|
||||||
|
<small>@(row.MobilePhone ?? row.Phone ?? "No phone")</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">SMS Status</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge @row.StatusBadgeClass">@row.StatusLabel</span></span>
|
||||||
|
</div>
|
||||||
|
@if (row.ConsentedAt.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Consented</span>
|
||||||
|
<span class="mobile-card-value">@row.ConsentedAt.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(row.ConsentMethod))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Method</span>
|
||||||
|
<span class="mobile-card-value">@row.ConsentMethod</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (row.OptedOutAt.HasValue)
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Opted Out</span>
|
||||||
|
<span class="mobile-card-value text-danger">@row.OptedOutAt.Value.ToString("MMM d, yyyy")</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-controller="Customers" asp-action="Details" asp-route-id="@row.CustomerId"
|
||||||
|
class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation()">
|
||||||
|
<i class="bi bi-person me-1"></i>Customer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!Model.Rows.Any())
|
||||||
|
{
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-phone-vibrate fs-1 d-block mb-2 opacity-25"></i>
|
||||||
|
No customers match the current filter.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0 align-middle">
|
<table class="table table-hover mb-0 align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -84,6 +84,46 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var file in Model.Files.OrderBy(f => f.Status).ThenBy(f => f.RelativePath))
|
||||||
|
{
|
||||||
|
var fStatusBadge = file.Status switch
|
||||||
|
{
|
||||||
|
MigrationFileStatus.Migrated => "bg-success",
|
||||||
|
MigrationFileStatus.Skipped => "bg-secondary",
|
||||||
|
_ => "bg-danger"
|
||||||
|
};
|
||||||
|
var fStatusLabel = file.Status switch
|
||||||
|
{
|
||||||
|
MigrationFileStatus.Migrated => "Migrated",
|
||||||
|
MigrationFileStatus.Skipped => "Already in Azure",
|
||||||
|
_ => "Failed"
|
||||||
|
};
|
||||||
|
<div class="mobile-data-card">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #0369a1 0%, #075985 100%);">
|
||||||
|
<i class="bi bi-file-earmark"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6 class="font-monospace" style="font-size:.75rem;">@file.RelativePath</h6>
|
||||||
|
<small><span class="badge bg-light text-dark border">@file.Container</span></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-body">
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Size</span>
|
||||||
|
<span class="mobile-card-value text-muted">@FormatBytes(file.FileSize)</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Status</span>
|
||||||
|
<span class="mobile-card-value"><span class="badge @fStatusBadge">@fStatusLabel</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover mb-0">
|
<table class="table table-sm table-hover mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -68,6 +68,64 @@
|
|||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
<div class="mobile-card-view">
|
||||||
|
<div class="mobile-card-list">
|
||||||
|
@foreach (var vc in Model)
|
||||||
|
{
|
||||||
|
var (vcBadge, vcLabel) = vc.Status switch
|
||||||
|
{
|
||||||
|
VendorCreditStatus.Open => ("bg-success", "Open"),
|
||||||
|
VendorCreditStatus.PartiallyApplied => ("bg-warning text-dark", "Partial"),
|
||||||
|
VendorCreditStatus.Applied => ("bg-secondary", "Applied"),
|
||||||
|
VendorCreditStatus.Voided => ("bg-danger", "Voided"),
|
||||||
|
_ => ("bg-secondary", vc.Status.ToString())
|
||||||
|
};
|
||||||
|
<div class="mobile-data-card" onclick="window.location='@Url.Action("Details", new { id = vc.Id })'">
|
||||||
|
<div class="mobile-card-header">
|
||||||
|
<div class="mobile-card-icon" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
|
||||||
|
<i class="bi bi-credit-card"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-title">
|
||||||
|
<h6>@vc.CreditNumber</h6>
|
||||||
|
<small>@vc.Vendor?.CompanyName</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 @vcBadge">@vcLabel</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Date</span>
|
||||||
|
<span class="mobile-card-value">@vc.CreditDate.ToString("MM/dd/yy")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Total</span>
|
||||||
|
<span class="mobile-card-value">@vc.Total.ToString("C")</span>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Remaining</span>
|
||||||
|
<span class="mobile-card-value @(vc.RemainingAmount > 0 ? "text-success fw-semibold" : "text-muted")">
|
||||||
|
@(vc.RemainingAmount > 0 ? vc.RemainingAmount.ToString("C") : "—")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(vc.Memo))
|
||||||
|
{
|
||||||
|
<div class="mobile-card-row">
|
||||||
|
<span class="mobile-card-label">Memo</span>
|
||||||
|
<span class="mobile-card-value">@vc.Memo</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="mobile-card-footer">
|
||||||
|
<a asp-action="Details" asp-route-id="@vc.Id" class="btn btn-sm btn-outline-secondary" onclick="event.stopPropagation()">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
/* Hide desktop table view on mobile */
|
/* Hide desktop table only when a mobile card view sibling is present */
|
||||||
.table-responsive {
|
.mobile-card-view ~ .table-responsive,
|
||||||
|
.table-responsive:has(~ .mobile-card-view) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
(function () {
|
||||||
|
var qtyInput = document.getElementById('Quantity');
|
||||||
|
var amtInput = document.getElementById('Amount');
|
||||||
|
var preview = document.getElementById('batchPreview');
|
||||||
|
var prevQty = document.getElementById('prevQty');
|
||||||
|
var prevAmt = document.getElementById('prevAmt');
|
||||||
|
var prevTotal = document.getElementById('prevTotal');
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
var qty = parseInt(qtyInput.value, 10);
|
||||||
|
var amt = parseFloat(amtInput.value);
|
||||||
|
if (qty > 0 && amt > 0) {
|
||||||
|
prevQty.textContent = qty;
|
||||||
|
prevAmt.textContent = '$' + amt.toFixed(2);
|
||||||
|
prevTotal.textContent = '$' + (qty * amt).toFixed(2);
|
||||||
|
preview.style.display = '';
|
||||||
|
} else {
|
||||||
|
preview.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qtyInput && amtInput) {
|
||||||
|
qtyInput.addEventListener('input', updatePreview);
|
||||||
|
amtInput.addEventListener('input', updatePreview);
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable submit button after first click to prevent double-submit during long creation
|
||||||
|
var form = document.querySelector('form');
|
||||||
|
var submitBtn = document.getElementById('submitBtn');
|
||||||
|
if (form && submitBtn) {
|
||||||
|
form.addEventListener('submit', function () {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Creating...';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}());
|
||||||
Reference in New Issue
Block a user